#pragma once #include #include #include #include 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(), 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 { return PropertyValue(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(), 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: 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(), 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) {} }; }