#include #include #include #include #include #include #include #include #include #include #include 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> 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( STR("[PalUIExtension] installed feature '{}'\n"), Feature->Name()); } InstallWorldLifecycleHooks(); m_installed = true; } private: auto BuildFeatures() -> void { m_features.push_back(std::make_unique()); m_features.push_back(std::make_unique()); m_features.push_back(std::make_unique()); } 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( 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 { if (s_instance) { s_instance->TeardownAllFeatures(STR("LoadMap")); } return { true, true }; }); Output::send( STR("[PalUIExtension] world lifecycle hooks installed (restart={})\n"), RestartId); } static void OnRestartGamePre(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) { auto* Self = static_cast(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( STR("[PalUIExtension] '{}' threw during teardown; its cached pointers " "are now stale.\n"), Feature->Name()); } } Output::send( 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; } }