Files
PalUIExtension/Api/Pal/PalGameMode.hpp
2026-09-07 19:33:58 -04:00

302 lines
12 KiB
C++

#pragma once
#include <optional>
#include <Unreal/FString.hpp>
#include <Unreal/NameTypes.hpp>
#include <Api/UnrealWrapper.hpp>
namespace PalUIExtension::Api::Pal
{
using namespace RC;
using namespace RC::Unreal;
// ==================================================================
// EPalGameModeType -- confirmed from the dump, UENUM(BlueprintType)
// over uint8, two values. Mirrored here for the same reason
// ESlateVisibility_ is: a params struct needs a concrete type of the
// right width, and pulling in the game's generated header is not an
// option.
// ==================================================================
enum class EPalGameModeType_ : uint8
{
Title = 0,
InGame = 1,
};
// ==================================================================
// APalGameModeBase.
//
// Two things exist on this class and one of them matters:
// GameModeType, an FEnumProperty over uint8. Read it to tell the
// title screen apart from a live world without inspecting the map
// name.
//
// ---- THE THING TO INTERNALISE ABOUT GAME MODES -------------------
//
// A GameMode exists ONLY ON THE AUTHORITY. On a listen host and in
// single-player that is the local process, so this class is
// reachable. On a client connected to a dedicated server, no
// APalGameMode is ever instantiated locally and nothing in this file
// will ever resolve to anything.
//
// That is not a limitation of this wrapper, it is what a game mode
// IS, and it decides what you may and may not build on top of it. A
// client-side UI mod that depends on a game-mode hook for
// correctness is broken for every dedicated-server player. Use these
// as a supplement to an engine-level signal, never as the only one.
// ==================================================================
class PalGameModeBase : public UnrealWrapper
{
public:
static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalGameModeBase"); }
static auto DebugTag() -> const TCHAR* { return STR("PalGameModeBase"); }
PalGameModeBase() = default;
explicit PalGameModeBase(UObject* Obj)
: UnrealWrapper(Obj, ClassOf<PalGameModeBase>(), DebugTag()) {}
// FEnumProperty over uint8 -- GetSize() reports 1, and
// EPalGameModeType_ is 1 byte, so the size check in PropertyPtr is
// meaningful here rather than vacuous.
auto GameModeType() const -> std::optional<EPalGameModeType_>
{
return PropertyValue<EPalGameModeType_>(STR("GameModeType"));
}
auto IsInGame() const -> bool
{
const auto T = GameModeType();
return T.has_value() && *T == EPalGameModeType_::InGame;
}
auto IsTitle() const -> bool
{
const auto T = GameModeType();
return T.has_value() && *T == EPalGameModeType_::Title;
}
protected:
PalGameModeBase(UObject* Obj, UClass* Expected, const TCHAR* Tag)
: UnrealWrapper(Obj, Expected, Tag) {}
};
// ==================================================================
// APalGameMode -- the in-world game mode.
//
// ---- WHAT IS WORTH HOOKING HERE, AND WHAT IS NOT -----------------
//
// RestartGame is the one this tree cares about: it is the game
// deciding the current world is finished, fired BEFORE the travel
// that destroys it. That ordering is the whole value -- a cache of
// widget pointers wants to be dropped while those pointers are still
// merely useless, not after they are dangling.
//
// READ THE CAVEAT BLOCK ON RestartGamePath() BEFORE RELYING ON IT.
// It has two failure modes that are invisible in testing if you only
// test single-player.
//
// The session/auth callbacks (OnUpdateSession, OnCompleteAuth,
// OnCompleteCreateSession, OnEOSLoginDedicatedServerComplete) are
// dedicated-server plumbing. They are wrapped as PATHS ONLY, with
// frames, because they are occasionally useful for diagnosing "why
// did my server mod not initialise" -- but nothing in a client-side
// UI mod should be reading them, and none of them fire in
// single-player at all.
//
// ---- WHY THERE IS NO Current() -----------------------------------
//
// The obvious convenience -- a static that hands back the live game
// mode -- is deliberately absent. Every way to get one without a
// UWorld in hand goes through UObjectGlobals::FindObject or
// FindObjects, and for a Blueprint-reachable native class that is a
// linear walk of the entire GUObjectArray (see the note in
// PalBoxNightWorkIndicator on what that cost actually is). A helper
// that looks like a property read and is really a million-object scan
// is the kind of convenience that ends up on a per-frame path.
//
// Get the instance from a hook that already has it instead: every
// callback below receives it as Ctx.Context, and
// Hook::RegisterInitGameStatePostCallback hands over an
// AGameModeBase* directly at world start.
// ==================================================================
class PalGameMode : public PalGameModeBase
{
public:
static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalGameMode"); }
static auto DebugTag() -> const TCHAR* { return STR("PalGameMode"); }
PalGameMode() = default;
explicit PalGameMode(UObject* Obj)
: PalGameModeBase(Obj, ClassOf<PalGameMode>(), DebugTag()) {}
// ---- properties ------------------------------------------------
// APlayerStart*. No wrapper for it in this tree, so the raw
// pointer is the honest return type.
auto CachePlayerStart() const -> UObject*
{
return TryObjectProperty(STR("CachePlayerStart"));
}
// ==============================================================
// HOOK TARGETS
// ==============================================================
// ==============================================================
// RestartGame. READ THIS BEFORE BUILDING ON IT.
//
// Two failure modes, both silent, both invisible if you only
// test single-player:
//
// 1. AUTHORITY ONLY. No APalGameMode is instantiated on a client
// connected to a dedicated server, so this hook never installs
// and never fires there. For a client-side UI mod that is the
// majority multiplayer case.
//
// 2. REFLECTION ONLY. This is a native UFUNCTION(BlueprintCallable).
// UE4SS hooks UFunction execution, which catches invocations
// that go through ProcessEvent -- a Blueprint node, a console
// exec, a reflected call. It does NOT catch native C++ code
// calling APalGameMode::RestartGame() directly, because that
// path never touches the reflection system. Whether Palworld
// restarts via Blueprint or via C++ is not answerable from a
// header dump; it has to be observed.
//
// The practical consequence: treat a RestartGame callback as an
// OPPORTUNISTIC EARLY WARNING, not as the teardown signal. The
// load-bearing one is Hook::RegisterLoadMapPreCallback, which is
// engine-level, fires on every map transition regardless of what
// triggered it, and works identically on clients and hosts.
//
// Wiring both is correct and costs nothing: teardown must be
// idempotent anyway, since a restart that goes through both paths
// will call it twice.
// ==============================================================
static auto RestartGamePath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:RestartGame");
}
static auto InitDedicatedServerPath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:InitDedicatedServer");
}
static auto OnServerLobbyUpdatePath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:OnServerLobbyUpdate");
}
static auto OnUpdateSessionPath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:OnUpdateSession");
}
static auto OnCompleteAuthPath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:OnCompleteAuth");
}
static auto OnCompleteCreateSessionPath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:OnCompleteCreateSession");
}
static auto OnEOSLoginDedicatedServerCompletePath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:OnEOSLoginDedicatedServerComplete");
}
static auto CreateSessionPath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:CreateSession");
}
static auto FindPlayerStartWithTagPath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameMode:FindPlayerStartWithTag");
}
// ---- frames ---------------------------------------------------
//
// Leading parameters only, matching the convention in
// PalCommonCharacterSlot.hpp: hook frames are NOT size-checked, so
// reading past the declared parameters reads engine locals. On a
// PRE hook those are uninitialised.
//
// The FString members are the part to be careful with. A
// `const FString&` UFUNCTION parameter still occupies a full
// FString in the frame (TArray<TCHAR>: 8-byte data pointer +
// int32 Num + int32 Max = 16 bytes), so the layout below is what
// the engine writes. It is NOT owned by you -- read it, do not
// move from it, do not free it.
//
// RestartGame takes nothing, which is why there is no frame for
// it and why its callback should not call Ctx.GetParams at all.
struct HttpResponseFrame
{
FString ResponseBody; // 0x00
bool bResponseOK; // 0x10
int32 ResponseCode; // 0x14 (after 3 bytes of padding)
};
struct EOSLoginCompleteFrame
{
UObject* UserInfo; // 0x00 const UPocketpairUserInfo*
bool bSuccess; // 0x08
FString ErrorStr; // 0x10 (after 7 bytes of padding)
};
struct CreateSessionFrame { FString Address; };
struct FindPlayerStartWithTagFrame { FName Tag; UObject* ReturnValue; };
protected:
PalGameMode(UObject* Obj, UClass* Expected, const TCHAR* Tag)
: PalGameModeBase(Obj, Expected, Tag) {}
};
// ==================================================================
// APalGameModeLogin -- the title-screen game mode.
//
// Included because it is the OTHER half of the world-lifecycle
// picture and it is easy to forget it exists: going from a save back
// to the title is a map transition into THIS game mode, not a
// RestartGame on the previous one. If teardown only fires on
// APalGameMode, quitting to title leaves every cached pointer alive
// and dangling.
//
// GoToTitle is BlueprintImplementableEvent -- no native body. It
// dispatches through the object's class chain like anything else, so
// it is hookable, but if no Blueprint subclass overrides it there is
// nothing to hook and the registry will defer forever.
// ==================================================================
class PalGameModeLogin : public PalGameModeBase
{
public:
static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalGameModeLogin"); }
static auto DebugTag() -> const TCHAR* { return STR("PalGameModeLogin"); }
PalGameModeLogin() = default;
explicit PalGameModeLogin(UObject* Obj)
: PalGameModeBase(Obj, ClassOf<PalGameModeLogin>(), DebugTag()) {}
static auto OnCompletePath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameModeLogin:OnComplete");
}
static auto GoToTitlePath() -> const TCHAR*
{
return STR("/Script/Pal.PalGameModeLogin:GoToTitle");
}
struct OnCompleteFrame { bool bCanPlayMulti; };
protected:
PalGameModeLogin(UObject* Obj, UClass* Expected, const TCHAR* Tag)
: PalGameModeBase(Obj, Expected, Tag) {}
};
}