initial commit; cv 0.0.3
This commit is contained in:
49
Static/ClassCache.hpp
Normal file
49
Static/ClassCache.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <DynamicOutput/DynamicOutput.hpp>
|
||||
#include <Unreal/UObject.hpp>
|
||||
|
||||
namespace PalUIExtension::Static
|
||||
{
|
||||
using namespace RC;
|
||||
using namespace RC::Unreal;
|
||||
|
||||
class ClassCache
|
||||
{
|
||||
public:
|
||||
|
||||
// Registers a slot to be nulled on Flush. The pointer must outlive the process -- every caller is a static, so this holds.
|
||||
static auto RegisterSlot(UObject** Slot) -> void
|
||||
{
|
||||
if (!Slot) return;
|
||||
s_slots.push_back(Slot);
|
||||
}
|
||||
|
||||
static auto RegisterSlot(UClass** Slot) -> void
|
||||
{
|
||||
if (!Slot) return;
|
||||
s_class_slots.push_back(Slot);
|
||||
}
|
||||
|
||||
// Called on EVERY world teardown, before anything else runs.
|
||||
|
||||
static auto Flush() -> void
|
||||
{
|
||||
for (UObject** Slot : s_slots) { if (Slot) *Slot = nullptr; }
|
||||
for (UClass** Slot : s_class_slots){ if (Slot) *Slot = nullptr; }
|
||||
|
||||
Output::send<LogLevel::Verbose>(
|
||||
STR("[ClassCache] flushed {} object slots and {} class slots\n"),
|
||||
static_cast<int32>(s_slots.size()),
|
||||
static_cast<int32>(s_class_slots.size()));
|
||||
}
|
||||
|
||||
static auto SlotCount() -> size_t { return s_slots.size() + s_class_slots.size(); }
|
||||
|
||||
private:
|
||||
inline static std::vector<UObject**> s_slots{};
|
||||
inline static std::vector<UClass**> s_class_slots{};
|
||||
};
|
||||
}
|
||||
278
Static/EventHookRegistry.hpp
Normal file
278
Static/EventHookRegistry.hpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
#include <DynamicOutput/DynamicOutput.hpp>
|
||||
|
||||
#include <Unreal/Hooks.hpp>
|
||||
#include <Unreal/UObjectGlobals.hpp>
|
||||
#include <Unreal/UObject.hpp>
|
||||
#include <Unreal/UClass.hpp>
|
||||
#include <Unreal/UFunction.hpp>
|
||||
|
||||
namespace PalUIExtension::Static
|
||||
{
|
||||
using namespace RC;
|
||||
using namespace RC::Unreal;
|
||||
|
||||
// Registry + deferred installer.
|
||||
//
|
||||
// Register() resolves the target UFunction immediately if it is already loaded. If it isn't, the request is parked in s_pending and a single
|
||||
// ProcessEvent pre-callback watches for a call on a matching <class name, function name> pair, then installs the real hook.
|
||||
//
|
||||
// NOT THREAD SAFE.
|
||||
|
||||
class EventHookRegistry
|
||||
{
|
||||
public:
|
||||
using StringType = std::basic_string<TCHAR>;
|
||||
using HookId = uint32_t;
|
||||
|
||||
static constexpr HookId InvalidHook = 0;
|
||||
|
||||
private:
|
||||
struct HookRequest
|
||||
{
|
||||
HookId Id{};
|
||||
StringType Name; // owned, not a borrowed const TCHAR*
|
||||
StringType FunctionPath; // owned
|
||||
FName ClassName{}; // e.g. WBP_PalStatus_C
|
||||
FName FunctionName{}; // e.g. Construct
|
||||
|
||||
UnrealScriptFunctionCallable Pre{};
|
||||
UnrealScriptFunctionCallable Post{};
|
||||
void* CustomData{};
|
||||
};
|
||||
|
||||
struct InstalledHook
|
||||
{
|
||||
HookId Id{};
|
||||
StringType Name;
|
||||
UFunction* Function{};
|
||||
int PreId{};
|
||||
int PostId{};
|
||||
};
|
||||
|
||||
inline static std::vector<HookRequest> s_pending{};
|
||||
inline static std::vector<InstalledHook> s_installed{};
|
||||
inline static HookId s_next_id{ 1 };
|
||||
inline static bool s_watcher_installed{ false };
|
||||
inline static bool s_shutdown{ false };
|
||||
|
||||
public:
|
||||
static void Initialize(size_t expected_hooks = 16)
|
||||
{
|
||||
s_shutdown = false;
|
||||
s_pending.reserve(expected_hooks);
|
||||
s_installed.reserve(expected_hooks);
|
||||
}
|
||||
|
||||
static HookId Register(
|
||||
const TCHAR* HookName,
|
||||
const TCHAR* FunctionPath,
|
||||
UnrealScriptFunctionCallable Pre = [](UnrealScriptFunctionCallableContext&, void*) {},
|
||||
UnrealScriptFunctionCallable Post = [](UnrealScriptFunctionCallableContext&, void*) {},
|
||||
void* CustomData = nullptr)
|
||||
{
|
||||
if (!HookName || !FunctionPath)
|
||||
{
|
||||
Output::send<LogLevel::Error>(STR("[EventHookRegistry] null HookName or FunctionPath\n"));
|
||||
return InvalidHook;
|
||||
}
|
||||
|
||||
HookRequest Req{};
|
||||
Req.Id = s_next_id++;
|
||||
Req.Name = HookName;
|
||||
Req.FunctionPath = FunctionPath;
|
||||
Req.Pre = Pre;
|
||||
Req.Post = Post;
|
||||
Req.CustomData = CustomData;
|
||||
|
||||
StringType ClassPart, FuncPart;
|
||||
if (!SplitFunctionPath(Req.FunctionPath, ClassPart, FuncPart))
|
||||
{
|
||||
Output::send<LogLevel::Error>(
|
||||
STR("[EventHookRegistry] '{}': cannot parse '{}' -- expected ")
|
||||
STR("/Path/To/Package.ClassName:FunctionName\n"), HookName, FunctionPath);
|
||||
return InvalidHook;
|
||||
}
|
||||
Req.ClassName = FName(ClassPart.c_str(), FNAME_Add);
|
||||
Req.FunctionName = FName(FuncPart.c_str(), FNAME_Add);
|
||||
|
||||
// Fast path: already loaded, install now, never touch ProcessEvent.
|
||||
if (UFunction* Fn = UObjectGlobals::StaticFindObject<UFunction*>(nullptr, nullptr, Req.FunctionPath.c_str()))
|
||||
{
|
||||
Install(Req, Fn);
|
||||
return Req.Id;
|
||||
}
|
||||
|
||||
Output::send<LogLevel::Verbose>(
|
||||
STR("[EventHookRegistry] '{}': '{}' not loaded yet, deferring\n"), HookName, FunctionPath);
|
||||
|
||||
s_pending.push_back(std::move(Req));
|
||||
EnsureWatcher();
|
||||
return s_next_id - 1;
|
||||
}
|
||||
|
||||
static bool Deregister(const TCHAR* HookName)
|
||||
{
|
||||
if (!HookName) return false;
|
||||
const StringType Name{ HookName };
|
||||
|
||||
for (size_t i = 0; i < s_installed.size(); ++i)
|
||||
{
|
||||
if (s_installed[i].Name != Name) continue;
|
||||
Uninstall(s_installed[i]);
|
||||
s_installed.erase(s_installed.begin() + i);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < s_pending.size(); ++i)
|
||||
{
|
||||
if (s_pending[i].Name != Name) continue;
|
||||
s_pending.erase(s_pending.begin() + i);
|
||||
return true;
|
||||
}
|
||||
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[EventHookRegistry] Deregister: no hook named '{}'\n"), HookName);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool Deregister(HookId Id)
|
||||
{
|
||||
for (size_t i = 0; i < s_installed.size(); ++i)
|
||||
{
|
||||
if (s_installed[i].Id != Id) continue;
|
||||
Uninstall(s_installed[i]);
|
||||
s_installed.erase(s_installed.begin() + i);
|
||||
return true;
|
||||
}
|
||||
for (size_t i = 0; i < s_pending.size(); ++i)
|
||||
{
|
||||
if (s_pending[i].Id != Id) continue;
|
||||
s_pending.erase(s_pending.begin() + i);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsInstalled(HookId Id)
|
||||
{
|
||||
for (const auto& H : s_installed) if (H.Id == Id) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static size_t PendingCount() { return s_pending.size(); }
|
||||
|
||||
// The ProcessEvent callback cannot be unregistered in UE4SS, insteasd it drops all state and sets s_shutdown; the watcher
|
||||
// then costs one bool test per ProcessEvent for the rest of the process.
|
||||
static void Cleanup()
|
||||
{
|
||||
for (auto& H : s_installed) Uninstall(H);
|
||||
s_installed.clear();
|
||||
s_pending.clear();
|
||||
s_shutdown = true;
|
||||
}
|
||||
|
||||
private:
|
||||
static void EnsureWatcher()
|
||||
{
|
||||
if (s_watcher_installed) return;
|
||||
Hook::RegisterProcessEventPreCallback(&EventHookRegistry::OnProcessEvent);
|
||||
s_watcher_installed = true;
|
||||
Output::send<LogLevel::Verbose>(STR("[EventHookRegistry] ProcessEvent watcher installed\n"));
|
||||
}
|
||||
|
||||
// HOT PATH. This runs for effectively every UFunction call in the game.
|
||||
// Every line before the loop is there to make the common case a load,
|
||||
// a branch, and a return.
|
||||
static void OnProcessEvent(UObject* Context, UFunction* Function, void*)
|
||||
{
|
||||
if (s_shutdown || s_pending.empty()) return;
|
||||
if (!Context || !Function) return;
|
||||
|
||||
UClass* Cls = Context->GetClassPrivate();
|
||||
if (!Cls) return;
|
||||
const FName ClsName = Cls->GetNamePrivate();
|
||||
|
||||
// Backwards so erase() doesn't disturb indices we haven't visited.
|
||||
for (size_t i = s_pending.size(); i-- > 0;)
|
||||
{
|
||||
HookRequest& Req = s_pending[i];
|
||||
if (Req.ClassName != ClsName) continue;
|
||||
|
||||
UFunction* Target = UObjectGlobals::StaticFindObject<UFunction*>(nullptr, nullptr, Req.FunctionPath.c_str());
|
||||
|
||||
if (!Target)
|
||||
{
|
||||
if (Req.FunctionName != Function->GetNamePrivate()) continue;
|
||||
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[EventHookRegistry] '{}': '{}' still will not resolve by path; ")
|
||||
STR("falling back to the dispatched UFunction {}. If your hook fires on the ")
|
||||
STR("wrong object, your path string is wrong.\n"),
|
||||
Req.Name, Req.FunctionPath, Function->GetFullName());
|
||||
Target = Function;
|
||||
}
|
||||
|
||||
HookRequest Owned = std::move(Req);
|
||||
s_pending.erase(s_pending.begin() + i);
|
||||
Install(Owned, Target);
|
||||
}
|
||||
}
|
||||
|
||||
static void Install(HookRequest& Req, UFunction* Fn)
|
||||
{
|
||||
if (!Fn) return;
|
||||
|
||||
for (const auto& H : s_installed)
|
||||
{
|
||||
if (H.Function != Fn) continue;
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[EventHookRegistry] '{}': {} is already hooked by '{}'. ")
|
||||
STR("Installing anyway -- both callbacks will fire.\n"),
|
||||
Req.Name, Fn->GetFullName(), H.Name);
|
||||
break;
|
||||
}
|
||||
|
||||
auto _pre = Req.Pre;
|
||||
if (!_pre) _pre = [](UnrealScriptFunctionCallableContext&, void*) {};
|
||||
auto _post = Req.Post;
|
||||
if (!_post) _post = [](UnrealScriptFunctionCallableContext&, void*) {};
|
||||
|
||||
auto [PreId, PostId] = UObjectGlobals::RegisterHook(Fn, _pre, _post, Req.CustomData);
|
||||
|
||||
s_installed.push_back(InstalledHook{ Req.Id, Req.Name, Fn, PreId, PostId });
|
||||
|
||||
Output::send<LogLevel::Verbose>(
|
||||
STR("[EventHookRegistry] '{}': installed on {} (pre={}, post={})\n"),
|
||||
Req.Name, Fn->GetFullName(), PreId, PostId);
|
||||
}
|
||||
|
||||
static void Uninstall(InstalledHook& H)
|
||||
{
|
||||
if (!H.Function) return;
|
||||
UObjectGlobals::UnregisterHook(H.Function, std::make_tuple(H.PreId, H.PostId));
|
||||
H.Function = nullptr;
|
||||
}
|
||||
|
||||
// "/Game/Pal/Blueprint/UI/PalStatus/WBP_PalStatus.WBP_PalStatus_C:Construct"
|
||||
// -> ClassName "WBP_PalStatus_C", FunctionName "Construct"
|
||||
static bool SplitFunctionPath(const StringType& Path, StringType& OutClass, StringType& OutFunction)
|
||||
{
|
||||
const auto Colon = Path.rfind(STR(':'));
|
||||
if (Colon == StringType::npos || Colon + 1 >= Path.size()) return false;
|
||||
|
||||
OutFunction = Path.substr(Colon + 1);
|
||||
|
||||
const StringType ObjectPart = Path.substr(0, Colon);
|
||||
const auto Dot = ObjectPart.rfind(STR('.'));
|
||||
OutClass = (Dot == StringType::npos) ? ObjectPart : ObjectPart.substr(Dot + 1);
|
||||
|
||||
return !OutClass.empty() && !OutFunction.empty();
|
||||
}
|
||||
};
|
||||
}
|
||||
9
Static/LogGate.hpp
Normal file
9
Static/LogGate.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
namespace PalUIExtension::Static
|
||||
{
|
||||
|
||||
inline constexpr bool kVerboseLogging = false;
|
||||
inline constexpr bool kProfileHotPath = true;
|
||||
|
||||
}
|
||||
139
Static/WidgetRegistry.hpp
Normal file
139
Static/WidgetRegistry.hpp
Normal file
@@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <cstdint>
|
||||
|
||||
#include <DynamicOutput/DynamicOutput.hpp>
|
||||
#include <Unreal/UObject.hpp>
|
||||
|
||||
#include <Api/Widget.hpp>
|
||||
|
||||
namespace PalUIExtension::Static
|
||||
{
|
||||
using namespace RC;
|
||||
using namespace RC::Unreal;
|
||||
using Api::Widget;
|
||||
|
||||
class WidgetRegistry
|
||||
{
|
||||
public:
|
||||
using Generation = uint32_t;
|
||||
using KeyType = std::basic_string<TCHAR>;
|
||||
|
||||
// ---- generation control --------------------------------------
|
||||
|
||||
// Advances the generation and drops every existing entry.
|
||||
|
||||
static auto BeginGeneration() -> Generation
|
||||
{
|
||||
++s_generation;
|
||||
s_entries.clear();
|
||||
Output::send<LogLevel::Verbose>(STR("[WidgetRegistry] generation advanced to {}\n"), s_generation);
|
||||
return s_generation;
|
||||
}
|
||||
|
||||
// Calls Widget::RemoveFromParent on every entry still in the
|
||||
// current generation, THEN advances the generation.
|
||||
//
|
||||
// If an entry's pointer is already stale, this will call RemoveFromParent on whatever
|
||||
// object now lives at that address. That is the one real cost of the gap this class cannot close. Keep your generations short.
|
||||
static auto TeardownGeneration() -> void
|
||||
{
|
||||
for (auto& [Key, E] : s_entries)
|
||||
{
|
||||
if (E.Gen != s_generation || !E.Obj) continue;
|
||||
Widget W{ E.Obj };
|
||||
if (W) W.RemoveFromParent();
|
||||
}
|
||||
BeginGeneration();
|
||||
}
|
||||
|
||||
static auto CurrentGeneration() -> Generation { return s_generation; }
|
||||
|
||||
// ---- registration -----------------------------------------------
|
||||
|
||||
static auto Register(const KeyType& Key, UObject* Obj) -> bool
|
||||
{
|
||||
if (!Obj){
|
||||
Output::send<LogLevel::Warning>(STR("[WidgetRegistry] Register('{}'): refusing a null object\n"), Key.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s_entries.find(Key) != s_entries.end())
|
||||
{
|
||||
Output::send<LogLevel::Verbose>(STR("[WidgetRegistry] Register('{}'): overwriting existing entry\n"), Key.c_str());
|
||||
}
|
||||
|
||||
s_entries[Key] = Entry{ Obj, s_generation };
|
||||
return true;
|
||||
}
|
||||
|
||||
static auto Register(const TCHAR* Key, UObject* Obj) -> bool
|
||||
{
|
||||
return Register(KeyType{ Key }, Obj);
|
||||
}
|
||||
|
||||
static auto Unregister(const KeyType& Key) -> bool
|
||||
{
|
||||
return s_entries.erase(Key) > 0;
|
||||
}
|
||||
|
||||
// ---- lookup -----------------------------------------------------
|
||||
|
||||
// Returns nullptr if the key is missing OR if the stored entry predates the current generation.
|
||||
// A stale generation is treated identically to "never registered," and the dead entry is erased
|
||||
// on the way out so it doesn't accumulate.
|
||||
static auto GetObject(const KeyType& Key) -> UObject*
|
||||
{
|
||||
auto It = s_entries.find(Key);
|
||||
if (It == s_entries.end()) return nullptr;
|
||||
|
||||
if (It->second.Gen != s_generation)
|
||||
{
|
||||
Output::send<LogLevel::Verbose>(STR("[WidgetRegistry] GetObject('{}'): stale generation ({} vs current {}), dropping\n"), Key.c_str(), It->second.Gen, s_generation);
|
||||
s_entries.erase(It);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return It->second.Obj;
|
||||
}
|
||||
|
||||
static auto GetObject(const TCHAR* Key) -> UObject*
|
||||
{
|
||||
return GetObject(KeyType{ Key });
|
||||
}
|
||||
|
||||
// Typed lookup. T must be one of the wrapper types in this tree (Widget, TextBlock, CanvasPanel, PanelWidget, ...) exposing
|
||||
// ClassOf<T>() -- the same requirement UnrealWrapper's derived types already have. Constructing T{Obj} runs T's own IsA()
|
||||
// check, so a cross-class stale pointer comes back as an invalid wrapper (IsValid() == false) instead of a wrong-typed one.
|
||||
template <typename T>
|
||||
static auto Get(const KeyType& Key) -> T
|
||||
{
|
||||
UObject* Obj = GetObject(Key);
|
||||
return Obj ? T{ Obj } : T{};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static auto Get(const TCHAR* Key) -> T
|
||||
{
|
||||
return Get<T>(KeyType{ Key });
|
||||
}
|
||||
|
||||
static auto Contains(const KeyType& Key) -> bool
|
||||
{
|
||||
return GetObject(Key) != nullptr;
|
||||
}
|
||||
|
||||
static auto Count() -> size_t { return s_entries.size(); }
|
||||
|
||||
// Drops everything without touching the Slate tree. Same caveat as BeginGeneration(): this does not remove widgets from their parents, it just makes the registry stop tracking them.
|
||||
static auto Clear() -> void { s_entries.clear(); }
|
||||
|
||||
private:
|
||||
struct Entry { UObject* Obj{}; Generation Gen{}; };
|
||||
|
||||
inline static std::unordered_map<KeyType, Entry> s_entries{};
|
||||
inline static Generation s_generation{ 1 };
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user