#pragma once #include #include #include #include #include #include #include #include #include namespace PalUIExtension::Reflection { using namespace RC; using namespace RC::Unreal; template class UnrealApiCall { public: explicit UnrealApiCall(const TCHAR* ClassPath, const TCHAR* DebugTag) : m_class_path(ClassPath), m_debug_tag(DebugTag) { } auto ResolveClass() -> UClass* { if (s_cached_class) return s_cached_class; UObject* Found = UObjectGlobals::StaticFindObject( nullptr, nullptr, m_class_path, false); if (!Found) { Output::send( STR("[{}] ResolveClass: could not find class '{}'\n"), m_debug_tag, m_class_path); return nullptr; } s_cached_class = static_cast(Found); // Registered on first successful resolve rather than at construction, // because there is no constructor for a static inline member. Guarded // so a re-resolve after a flush does not register the same slot // twice -- the vector would still work, but the duplicate entries // would grow without bound across worlds. if (!s_slot_registered) { Static::ClassCache::RegisterSlot(&s_cached_class); s_slot_registered = true; } return s_cached_class; } auto FindInstance(UObject* Outer, const TCHAR* InstanceName, bool bExactClass = false) -> UObject* { UClass* Cls = ResolveClass(); if (!Cls) { // ResolveClass already logged the reason. return nullptr; } UObject* Instance = UObjectGlobals::FindObject(Cls, Outer, InstanceName, bExactClass); if (!Instance) { Output::send( STR("[{}] FindInstance: '{}' not found under outer {}\n"), m_debug_tag, InstanceName, Outer ? Outer->GetFullName() : STR("")); } return Instance; } auto FindOrConstruct(UObject* Outer, const TCHAR* InstanceName, bool& bOutWasNew, bool bExactClass = false) -> UObject* { bOutWasNew = false; if (!InstanceName) { Output::send( STR("[{}] FindOrConstruct: null InstanceName\n"), m_debug_tag); return nullptr; } if (UObject* Existing = FindInstance(Outer, InstanceName, bExactClass)) { return Existing; } bOutWasNew = true; return ConstructInstance(Outer, InstanceName); } template auto Invoke(UObject* Target, const TCHAR* FunctionName, ParamsStruct& Params) -> bool { if (!Target) { Output::send( STR("[{}] Invoke: null target calling '{}'\n"), m_debug_tag, FunctionName); return false; } UFunction* Fn = Target->GetFunctionByNameInChain(FunctionName); if (!Fn) { Output::send( STR("[{}] Invoke: '{}' not found on {}\n"), m_debug_tag, FunctionName, Target->GetFullName()); return false; } check_struct_size(Fn, FunctionName, sizeof(ParamsStruct)); check_return_type(Fn, FunctionName); Target->ProcessEvent(Fn, &Params); return true; } using ReturnMemberType = std::conditional_t, char, TReturn>; template auto InvokeForReturn(UObject* Target, const TCHAR* FunctionName, ParamsStruct& Params, ReturnMemberType ParamsStruct::* ReturnMember) -> std::optional { static_assert(!std::is_void_v, "InvokeForReturn requires a non-void TReturn; use Invoke() for void calls."); if (!Invoke(Target, FunctionName, Params)) { return std::nullopt; } return Params.*ReturnMember; } auto ConstructInstance(UObject* Outer, const TCHAR* InstanceName = nullptr) -> UObject* { UClass* Cls = ResolveClass(); if (!Cls) return nullptr; if (!Outer) { Output::send(STR("[{}] ConstructInstance: null Outer -- the object will be unreachable by GC and collected out from under you.\n"), m_debug_tag); return nullptr; } FStaticConstructObjectParameters Params{ Cls }; Params.Outer = Outer; if (InstanceName) Params.Name = FName(InstanceName); UObject* Obj = UObjectGlobals::StaticConstructObject(Params); if (!Obj) { Output::send(STR("[{}] ConstructInstance: StaticConstructObject returned null\n"), m_debug_tag); } return Obj; } private: auto check_struct_size(UFunction* Fn, const TCHAR* FunctionName, size_t ActualStructSize) -> void { // UStruct::GetStructureSize() (Class.hpp) reports the engine's own // aligned size for the function's param block. int32 EngineSize = Fn->GetStructureSize(); if (EngineSize > 0 && static_cast(EngineSize) != ActualStructSize) { Output::send( STR("[{}] Invoke: '{}' param struct size mismatch -- ") STR("engine expects {} bytes, ParamsStruct is {} bytes. ") STR("Check field order, padding, and widths (e.g. float vs double) ") STR("before trusting this call's output.\n"), m_debug_tag, FunctionName, EngineSize, ActualStructSize); } } auto check_return_type(UFunction* Fn, const TCHAR* FunctionName) -> void { FProperty* ReturnProp = Fn->GetReturnProperty(); if constexpr (std::is_void_v) { if (ReturnProp) { Output::send( STR("[{}] Invoke: '{}' has a return value on the engine side, ") STR("but this UnrealApiCall was declared with TReturn=void -- ") STR("you're discarding a value the function actually produces.\n"), m_debug_tag, FunctionName); } return; } else { if (!ReturnProp) { Output::send( STR("[{}] Invoke: '{}' has no ReturnValue property on the engine side, ") STR("but this UnrealApiCall expects a return -- the member you read back ") STR("will not have been written by ProcessEvent.\n"), m_debug_tag, FunctionName); return; } int32 EnginePropSize = ReturnProp->GetSize(); if (static_cast(EnginePropSize) != sizeof(TReturn)) { Output::send( STR("[{}] Invoke: '{}' ReturnValue is {} bytes on the engine side, ") STR("TReturn is {} bytes -- this is a real mismatch, not a formality. ") STR("Reading this value as TReturn will read garbage or truncate it.\n"), m_debug_tag, FunctionName, EnginePropSize, sizeof(TReturn)); } } } const TCHAR* m_class_path; const TCHAR* m_debug_tag; static inline UClass* s_cached_class{}; static inline bool s_slot_registered{ false }; }; }