Files
PalUIExtension/Reflection/UnrealApiCall.hpp

231 lines
8.4 KiB
C++
Raw Normal View History

2026-09-07 19:33:58 -04:00
#pragma once
#include <optional>
#include <type_traits>
#include <DynamicOutput/DynamicOutput.hpp>
#include <Unreal/UObjectGlobals.hpp>
#include <Unreal/UObject.hpp>
#include <Unreal/UClass.hpp>
#include <Unreal/CoreUObject/UObject/Class.hpp>
#include <Unreal/CoreUObject/UObject/UnrealType.hpp>
#include <Static/ClassCache.hpp>
namespace PalUIExtension::Reflection
{
using namespace RC;
using namespace RC::Unreal;
template <typename TClass, typename TReturn = void> 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<UObject*>(
nullptr, nullptr, m_class_path, false);
if (!Found)
{
Output::send<LogLevel::Warning>(
STR("[{}] ResolveClass: could not find class '{}'\n"),
m_debug_tag, m_class_path);
return nullptr;
}
s_cached_class = static_cast<UClass*>(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<LogLevel::Warning>(
STR("[{}] FindInstance: '{}' not found under outer {}\n"),
m_debug_tag, InstanceName,
Outer ? Outer->GetFullName() : STR("<null>"));
}
return Instance;
}
auto FindOrConstruct(UObject* Outer, const TCHAR* InstanceName, bool& bOutWasNew, bool bExactClass = false) -> UObject*
{
bOutWasNew = false;
if (!InstanceName)
{
Output::send<LogLevel::Warning>(
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 <typename ParamsStruct>
auto Invoke(UObject* Target, const TCHAR* FunctionName, ParamsStruct& Params) -> bool
{
if (!Target)
{
Output::send<LogLevel::Warning>(
STR("[{}] Invoke: null target calling '{}'\n"), m_debug_tag, FunctionName);
return false;
}
UFunction* Fn = Target->GetFunctionByNameInChain(FunctionName);
if (!Fn)
{
Output::send<LogLevel::Warning>(
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<std::is_void_v<TReturn>, char, TReturn>;
template <typename ParamsStruct>
auto InvokeForReturn(UObject* Target, const TCHAR* FunctionName, ParamsStruct& Params,
ReturnMemberType ParamsStruct::* ReturnMember) -> std::optional<ReturnMemberType>
{
static_assert(!std::is_void_v<TReturn>,
"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<LogLevel::Warning>(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<LogLevel::Warning>(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<size_t>(EngineSize) != ActualStructSize)
{
Output::send<LogLevel::Warning>(
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<TReturn>)
{
if (ReturnProp)
{
Output::send<LogLevel::Warning>(
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<LogLevel::Warning>(
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<size_t>(EnginePropSize) != sizeof(TReturn))
{
Output::send<LogLevel::Warning>(
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 };
};
}