Files
PalUIExtension/dllmain.cpp
2026-09-07 19:33:58 -04:00

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;
}
}