231 lines
8.4 KiB
C++
231 lines
8.4 KiB
C++
|
|
#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 };
|
||
|
|
};
|
||
|
|
}
|