initial commit; cv 0.0.3
This commit is contained in:
301
Api/Pal/PalGameMode.hpp
Normal file
301
Api/Pal/PalGameMode.hpp
Normal file
@@ -0,0 +1,301 @@
|
||||
#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) {}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user