initial commit; cv 0.0.3
This commit is contained in:
230
Reflection/UnrealApiCall.hpp
Normal file
230
Reflection/UnrealApiCall.hpp
Normal file
@@ -0,0 +1,230 @@
|
||||
#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 };
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user