Skip to content

Commit

Permalink
Implement custom GameInstance initialization so that game knows wheth…
Browse files Browse the repository at this point in the history
…er it is dedicated server by the time UGameInstance::Init is called
  • Loading branch information
slonopotamus committed Aug 21, 2024
1 parent 98043de commit c5dfc5e
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 8 deletions.
33 changes: 26 additions & 7 deletions Source/UEST/Private/ScopedGame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "Iris/ReplicationSystem/ObjectReplicationBridge.h"
#include "Iris/ReplicationSystem/ReplicationSystem.h"
#include "Net/OnlineEngineInterface.h"
#include "UESTGameInstance.h"

struct TGWorldGuard final : FNoncopyable
{
Expand Down Expand Up @@ -58,7 +59,7 @@ static TUniquePtr<FCVarsGuard> CVarsGuard;

FScopedGameInstance::FScopedGameInstance(TSubclassOf<UGameInstance> GameInstanceClass, const bool bGarbageCollectOnDestroy, const TMap<FString, FCVarConfig>& CVars)
: GameInstanceClass{MoveTemp(GameInstanceClass)}
, LastInstanceId{0}
, LastPIEInstance{0}
, bGarbageCollectOnDestroy{bGarbageCollectOnDestroy}
{
if (NumScopedGames == 0)
Expand All @@ -77,7 +78,7 @@ FScopedGameInstance::FScopedGameInstance(TSubclassOf<UGameInstance> GameInstance
FScopedGameInstance::FScopedGameInstance(FScopedGameInstance&& Other)
: GameInstanceClass{MoveTemp(Other.GameInstanceClass)}
, Games{MoveTemp(Other.Games)}
, LastInstanceId{Other.LastInstanceId}
, LastPIEInstance{Other.LastPIEInstance}
, bGarbageCollectOnDestroy{Other.bGarbageCollectOnDestroy}
{
++NumScopedGames;
Expand Down Expand Up @@ -109,14 +110,32 @@ FScopedGameInstance::~FScopedGameInstance()

UGameInstance* FScopedGameInstance::CreateGame(const EScopedGameType Type, const FString& MapToLoad, const bool bWaitForConnect)
{
auto Game = NewObject<UGameInstance>(GEngine, GameInstanceClass);
auto* Game = NewObject<UGameInstance>(GEngine, GameInstanceClass);
if (!ensureAlwaysMsgf(Game, TEXT("Failed to create game instance")))
{
return nullptr;
}

Games.Emplace(Game);
Game->InitializeStandalone();

const auto bRunAsDedicated = Type == EScopedGameType::Server;
const auto PIEInstance = ++LastPIEInstance;

if (auto* TestableGame = Cast<IUESTGameInstance>(Game))
{
TestableGame->InitializeForTests(bRunAsDedicated, PIEInstance);
}
else
{
IUESTGameInstance::DefaultInitializeForTests(*Game, bRunAsDedicated, PIEInstance);
}

auto* WorldContext = Game->GetWorldContext();
WorldContext->PIEInstance = ++LastInstanceId;
WorldContext->PIEPrefix = UWorld::BuildPIEPackagePrefix(WorldContext->PIEInstance);
WorldContext->RunAsDedicated = Type == EScopedGameType::Server;
if (!ensureAlways(WorldContext))
{
DestroyGame(Game);
return nullptr;
}

if (Type == EScopedGameType::Client)
{
Expand Down
1 change: 1 addition & 0 deletions Source/UEST/Private/ScopedGameTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ TEST(UEST, ScopedGame, Simple)
Tester.CreateClientFor(Server);
}


// You can access game worlds
UWorld* ServerWorld = Server->GetWorld();
ASSERT_THAT(ServerWorld, Is::Not::Null);
Expand Down
36 changes: 36 additions & 0 deletions Source/UEST/Private/UESTGameInstance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "UESTGameInstance.h"

// God forgive me
// UGameInstance::WorldContext is not public, and neither InitializeStandalone nor InitializeForPlayInEditor is suitable for us.
// So we use this template hack to access WorldContext field.
// Source: https://ledas.com/post/857-how-to-hack-c-with-templates-and-friends/
template<FWorldContext* UGameInstance::* WorldContext>
struct Stealer {
static friend FWorldContext*& FieldGetter(UGameInstance& GameInstance) {
return GameInstance.*WorldContext;
}
};

template struct Stealer<&UGameInstance::WorldContext>;
static FWorldContext*& FieldGetter(UGameInstance&);

void IUESTGameInstance::DefaultInitializeForTests(UGameInstance& GameInstance, const bool bRunAsDedicated, const int32 PIEInstance)
{
constexpr auto WorldType = EWorldType::Game;

auto& WorldContext = GameInstance.GetEngine()->CreateNewWorldContext(WorldType);
WorldContext.OwningGameInstance = &GameInstance;
WorldContext.PIEInstance = PIEInstance;
WorldContext.PIEPrefix = UWorld::BuildPIEPackagePrefix(PIEInstance);

// We want to init this before calling UGameInstance::Init
WorldContext.RunAsDedicated = bRunAsDedicated;

FieldGetter(GameInstance) = &WorldContext;

auto* DummyWorld = UWorld::CreateWorld(WorldType, true);
DummyWorld->SetGameInstance(&GameInstance);
WorldContext.SetCurrentWorld(DummyWorld);

GameInstance.Init();
}
2 changes: 1 addition & 1 deletion Source/UEST/Public/ScopedGame.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class UEST_API FScopedGameInstance : FNoncopyable

static void DestroyGameInternal(UGameInstance& Game);

int32 LastInstanceId;
int32 LastPIEInstance;

bool bGarbageCollectOnDestroy;

Expand Down
23 changes: 23 additions & 0 deletions Source/UEST/Public/UESTGameInstance.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include "UESTGameInstance.generated.h"

UINTERFACE()
class UEST_API UUESTGameInstance : public UInterface
{
GENERATED_BODY()
};

/**
* Your GameInstance class can implement this interface if you want custom initialization in tests.
* If you don't, IUESTGameInstance::DefaultInitializeForTests is used.
*/
class UEST_API IUESTGameInstance
{
GENERATED_BODY()
public:

virtual void InitializeForTests(const bool bRunAsDedicated, int32 PIEInstance) = 0;

static void DefaultInitializeForTests(UGameInstance& GameInstance, bool bRunAsDedicated, int32 PIEInstance);
};

0 comments on commit c5dfc5e

Please sign in to comment.