302 lines
12 KiB
C++
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) {}
|
|
};
|
|
}
|