181 lines
6.1 KiB
C++
181 lines
6.1 KiB
C++
|
|
#include <memory>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#include <Mod/CppUserModBase.hpp>
|
||
|
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||
|
|
|
||
|
|
#include <Unreal/Hooks.hpp>
|
||
|
|
|
||
|
|
#include <Static/EventHookRegistry.hpp>
|
||
|
|
#include <Api/Pal/PalGameMode.hpp>
|
||
|
|
#include <Features/IUIFeature.hpp>
|
||
|
|
#include <Features/PalBoxPalNumber.hpp>
|
||
|
|
#include <Features/PalBoxNightWorkIndicator.hpp>
|
||
|
|
#include <Features/PalStatusPalNumber.hpp>
|
||
|
|
|
||
|
|
using namespace RC;
|
||
|
|
using namespace RC::Unreal;
|
||
|
|
|
||
|
|
// D:\Project\PalworldMods\PalUIExtension\post_build.bat "$(TargetPath)" "C:\Program Files (x86)\Steam\steamapps\common\Palworld\Pal\Binaries\Win64\ue4ss\Mods\PalUIExtension\dlls\main.dll"
|
||
|
|
namespace PalUIExtension
|
||
|
|
{
|
||
|
|
class PalUIExtension : public RC::CppUserModBase
|
||
|
|
{
|
||
|
|
private:
|
||
|
|
std::vector<std::unique_ptr<Features::IUIFeature>> m_features{};
|
||
|
|
bool m_installed = false;
|
||
|
|
|
||
|
|
// The static back-reference exists because UE4SS hook callbacks
|
||
|
|
// for FUNCTIONS take a void* CustomData but the ENGINE-level
|
||
|
|
// callbacks (LoadMap) do not -- they take a std::function with no
|
||
|
|
// user data slot. A capturing lambda would work for the latter,
|
||
|
|
// but then the two teardown paths would reach the mod by two
|
||
|
|
// different mechanisms, and a reader would have to work out
|
||
|
|
// whether that difference meant anything. It does not. One
|
||
|
|
// pointer, one mechanism.
|
||
|
|
//
|
||
|
|
// Single-instance is a safe assumption here: UE4SS constructs
|
||
|
|
// exactly one CppUserModBase per mod DLL via start_mod().
|
||
|
|
inline static PalUIExtension* s_instance{ nullptr };
|
||
|
|
|
||
|
|
public:
|
||
|
|
PalUIExtension() : CppUserModBase()
|
||
|
|
{
|
||
|
|
ModName = STR("PalUIExtension");
|
||
|
|
ModVersion = STR("0.2");
|
||
|
|
ModDescription = STR("PalUIExtension");
|
||
|
|
ModAuthors = STR("FerrenFX");
|
||
|
|
|
||
|
|
s_instance = this;
|
||
|
|
}
|
||
|
|
|
||
|
|
~PalUIExtension() override
|
||
|
|
{
|
||
|
|
// Order matters: hooks first, so nothing can fire a callback
|
||
|
|
// into features that are about to be destroyed.
|
||
|
|
Static::EventHookRegistry::Cleanup();
|
||
|
|
m_features.clear();
|
||
|
|
|
||
|
|
if (s_instance == this) s_instance = nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
auto on_unreal_init() -> void override
|
||
|
|
{
|
||
|
|
if (m_installed) return;
|
||
|
|
|
||
|
|
BuildFeatures();
|
||
|
|
|
||
|
|
// +2 for the game-mode hooks below, which go through the same
|
||
|
|
// registry and would otherwise force a reallocation.
|
||
|
|
Static::EventHookRegistry::Initialize(m_features.size() * 4 + 2);
|
||
|
|
|
||
|
|
for (auto& Feature : m_features)
|
||
|
|
{
|
||
|
|
Feature->InstallHooks();
|
||
|
|
Output::send<LogLevel::Verbose>(
|
||
|
|
STR("[PalUIExtension] installed feature '{}'\n"), Feature->Name());
|
||
|
|
}
|
||
|
|
|
||
|
|
InstallWorldLifecycleHooks();
|
||
|
|
|
||
|
|
m_installed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
auto BuildFeatures() -> void
|
||
|
|
{
|
||
|
|
m_features.push_back(std::make_unique<Features::PalBoxPalNumber>());
|
||
|
|
m_features.push_back(std::make_unique<Features::PalBoxNightWorkIndicator>());
|
||
|
|
m_features.push_back(std::make_unique<Features::PalStatusPalNumber>());
|
||
|
|
}
|
||
|
|
|
||
|
|
auto InstallWorldLifecycleHooks() -> void
|
||
|
|
{
|
||
|
|
const auto RestartId = Static::EventHookRegistry::Register(
|
||
|
|
STR("PalUIExtension::GameModeRestart"),
|
||
|
|
Api::Pal::PalGameMode::RestartGamePath(),
|
||
|
|
&PalUIExtension::OnRestartGamePre, // PRE: the world is still intact
|
||
|
|
nullptr,
|
||
|
|
this);
|
||
|
|
|
||
|
|
if (RestartId == Static::EventHookRegistry::InvalidHook)
|
||
|
|
{
|
||
|
|
Output::send<LogLevel::Warning>(
|
||
|
|
STR("[PalUIExtension] RestartGame hook was rejected. Teardown falls back to "
|
||
|
|
"LoadMap only, which is later but still correct.\n"));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Engine level. No CustomData parameter, hence s_instance.
|
||
|
|
Hook::RegisterLoadMapPreCallback(
|
||
|
|
[](UEngine*, FWorldContext&, FURL, UPendingNetGame*, FString&)
|
||
|
|
-> std::pair<bool, bool>
|
||
|
|
{
|
||
|
|
if (s_instance)
|
||
|
|
{
|
||
|
|
s_instance->TeardownAllFeatures(STR("LoadMap"));
|
||
|
|
}
|
||
|
|
|
||
|
|
return { true, true };
|
||
|
|
});
|
||
|
|
|
||
|
|
Output::send<LogLevel::Verbose>(
|
||
|
|
STR("[PalUIExtension] world lifecycle hooks installed (restart={})\n"), RestartId);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void OnRestartGamePre(UnrealScriptFunctionCallableContext& Ctx, void* CustomData)
|
||
|
|
{
|
||
|
|
auto* Self = static_cast<PalUIExtension*>(CustomData);
|
||
|
|
if (!Self) return;
|
||
|
|
|
||
|
|
Self->TeardownAllFeatures(STR("RestartGame"));
|
||
|
|
}
|
||
|
|
auto TeardownAllFeatures(const TCHAR* Reason) -> void
|
||
|
|
{
|
||
|
|
|
||
|
|
Static::ClassCache::Flush();
|
||
|
|
Static::WidgetRegistry::BeginGeneration();
|
||
|
|
|
||
|
|
int32 TornDown = 0;
|
||
|
|
|
||
|
|
for (auto& Feature : m_features)
|
||
|
|
{
|
||
|
|
if (!Feature) continue;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
Feature->OnWorldTeardown();
|
||
|
|
++TornDown;
|
||
|
|
}
|
||
|
|
catch (...)
|
||
|
|
{
|
||
|
|
Output::send<LogLevel::Error>(
|
||
|
|
STR("[PalUIExtension] '{}' threw during teardown; its cached pointers "
|
||
|
|
"are now stale.\n"), Feature->Name());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Output::send<LogLevel::Verbose>(
|
||
|
|
STR("[PalUIExtension] teardown via {}: {} features\n"), Reason, TornDown);
|
||
|
|
}
|
||
|
|
|
||
|
|
static auto to_wstring(const char* s) -> std::wstring
|
||
|
|
{
|
||
|
|
if (!s) return std::wstring{};
|
||
|
|
return std::wstring(s, s + std::strlen(s)); // ASCII only; what() is never non-ASCII here
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
#define MY_AWESOME_MOD_API __declspec(dllexport)
|
||
|
|
extern "C"
|
||
|
|
{
|
||
|
|
MY_AWESOME_MOD_API RC::CppUserModBase* start_mod()
|
||
|
|
{
|
||
|
|
return new PalUIExtension::PalUIExtension();
|
||
|
|
}
|
||
|
|
|
||
|
|
MY_AWESOME_MOD_API void uninstall_mod(RC::CppUserModBase* mod)
|
||
|
|
{
|
||
|
|
delete mod;
|
||
|
|
}
|
||
|
|
}
|