initial commit; cv 0.0.3
This commit is contained in:
476
Api/UnrealWrapper.hpp
Normal file
476
Api/UnrealWrapper.hpp
Normal file
@@ -0,0 +1,476 @@
|
||||
#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 <Reflection/UnrealApiCall.hpp>
|
||||
#include <Reflection/UnrealObjectRef.hpp>
|
||||
|
||||
namespace PalUIExtension::Api
|
||||
{
|
||||
using namespace RC;
|
||||
using namespace RC::Unreal;
|
||||
|
||||
namespace Detail
|
||||
{
|
||||
struct Tag_Umg {};
|
||||
|
||||
// ONE dispatcher per return type, shared by every wrapper in this
|
||||
// tree -- not one per wrapper class.
|
||||
//
|
||||
// The reason this is correct: UnrealApiCall::Invoke resolves the
|
||||
// function with Target->GetFunctionByNameInChain(), which walks the
|
||||
// *live object's* class chain. It never touches the ClassPath the
|
||||
// UnrealApiCall was constructed with. So the class path is only
|
||||
// used by ResolveClass/FindInstance/ConstructInstance, none of
|
||||
// which we call here. Giving each wrapper class its own set of
|
||||
// per-return-type statics would buy nothing but N redundant
|
||||
// StaticFindObject calls.
|
||||
//
|
||||
// The corollary matters for the inheritance below: a base-class
|
||||
// wrapper calling a function on a derived object works, because
|
||||
// dispatch is by the object's chain, not by the wrapper's type.
|
||||
//
|
||||
// The path below is a placeholder that is never resolved. If you
|
||||
// ever call ResolveClass on one of these, it will resolve UObject
|
||||
// and the resulting IsA check will be meaningless.
|
||||
template <typename R>
|
||||
inline auto Api() -> Reflection::UnrealApiCall<Tag_Umg, R>&
|
||||
{
|
||||
static Reflection::UnrealApiCall<Tag_Umg, R> Instance{
|
||||
STR("/Script/CoreUObject.Object"), STR("UMG") };
|
||||
return Instance;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves and caches the UClass for a wrapper type T.
|
||||
// T must expose static ClassPath() and static DebugTag().
|
||||
template <typename T>
|
||||
inline auto ClassOf() -> UClass*
|
||||
{
|
||||
static Reflection::UnrealObjectRef Ref{ T::ClassPath(), T::DebugTag() };
|
||||
return static_cast<UClass*>(Ref.Get());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Base for every UObject wrapper here.
|
||||
//
|
||||
// Lifetime: holds a raw UObject* and does nothing to keep it alive.
|
||||
// These wrappers are meant to live inside one hook callback. Storing
|
||||
// one across frames means storing a pointer the GC can invalidate,
|
||||
// and a freed UObject slot will eventually be reused by a different
|
||||
// object -- so a stale pointer does not read as null, it reads as
|
||||
// some other widget.
|
||||
// ------------------------------------------------------------------
|
||||
class UnrealWrapper
|
||||
{
|
||||
public:
|
||||
auto IsValid() const -> bool { return m_obj != nullptr; }
|
||||
explicit operator bool() const { return IsValid(); }
|
||||
auto Get() const -> UObject* { return m_obj; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Diagnostics.
|
||||
//
|
||||
// These were copy-pasted identically into PalDetailWidget and
|
||||
// MainMenuPalWidget, differing only in the log prefix -- which
|
||||
// m_tag already carries. Hoisted here so every wrapper has them and
|
||||
// there is one copy to fix when the reflection API shifts.
|
||||
//
|
||||
// LogFunctionSignature is the tool for settling a frame layout
|
||||
// without guessing: it prints the engine's own offsets and sizes
|
||||
// for a UFunction's parameter block, which is exactly what the
|
||||
// hand-written ParamsStructs in this codebase have to match. Run it
|
||||
// before trusting a new struct, not after it misbehaves.
|
||||
// ------------------------------------------------------------------
|
||||
auto LogFunctionSignature(const TCHAR* FunctionName) const -> void
|
||||
{
|
||||
if (!m_obj) return;
|
||||
|
||||
UFunction* Fn = m_obj->GetFunctionByNameInChain(FunctionName);
|
||||
if (!Fn)
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] '{}' is not in the chain of {}\n"),
|
||||
m_tag, FunctionName, m_obj->GetFullName());
|
||||
return;
|
||||
}
|
||||
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] {} -- frame is {} bytes:\n"),
|
||||
m_tag, FunctionName, Fn->GetStructureSize());
|
||||
|
||||
for (FProperty* Prop : Fn->ForEachProperty())
|
||||
{
|
||||
if (!Prop) continue;
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR(" [0x{:X}] {} ({} bytes)\n"),
|
||||
Prop->GetOffset_Internal(), Prop->GetName(), Prop->GetSize());
|
||||
}
|
||||
}
|
||||
|
||||
// Every UPROPERTY on the object's class chain, with offset and
|
||||
// size. The companion to the above for the property side: this is
|
||||
// how you find out what a widget is actually called internally, or
|
||||
// whether the field you want is a promoted variable at all.
|
||||
auto LogProperties() const -> void
|
||||
{
|
||||
if (!m_obj) return;
|
||||
|
||||
UClass* Cls = m_obj->GetClassPrivate();
|
||||
if (!Cls)
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] no class on {}\n"), m_tag, m_obj->GetFullName());
|
||||
return;
|
||||
}
|
||||
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] properties of {}:\n"), m_tag, m_obj->GetFullName());
|
||||
|
||||
for (FProperty* Prop : Cls->ForEachPropertyInChain())
|
||||
{
|
||||
if (!Prop) continue;
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR(" [0x{:X}] {} ({} bytes)\n"),
|
||||
Prop->GetOffset_Internal(), Prop->GetName(), Prop->GetSize());
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
UnrealWrapper() = default;
|
||||
|
||||
UnrealWrapper(UObject* Obj, UClass* Expected, const TCHAR* DebugTag)
|
||||
: m_tag(DebugTag ? DebugTag : STR("Wrapper"))
|
||||
{
|
||||
if (!Obj)
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] constructed from null object\n"), m_tag);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Expected)
|
||||
{
|
||||
// Class path did not resolve. Accept the object rather than
|
||||
// reject it -- Invoke dispatches by the object's own chain,
|
||||
// so calls will still work; you just lost the type check.
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] class unresolved -- accepting {} unchecked\n"),
|
||||
m_tag, Obj->GetFullName());
|
||||
m_obj = Obj;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Obj->IsA(Expected))
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] {} is not of the expected class\n"),
|
||||
m_tag, Obj->GetFullName());
|
||||
return;
|
||||
}
|
||||
|
||||
m_obj = Obj;
|
||||
}
|
||||
|
||||
struct NoParams {};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Deliberate opt-out of the class check.
|
||||
//
|
||||
// The (Obj, Expected, Tag) constructor below treats a null Expected
|
||||
// as "the ClassPath failed to resolve" and logs a Warning about it,
|
||||
// which is right for that case and wrong for a caller that never
|
||||
// had a ClassPath to begin with (LooseObject). The difference
|
||||
// matters more than it looks: that Warning calls
|
||||
// Obj->GetFullName(), which walks the whole outer chain calling
|
||||
// GetName() -- an FName::ToString, i.e. a ProcessEvent -- at every
|
||||
// level, then formats the line and writes it to every open output
|
||||
// device, because Output::send has no level filter (see
|
||||
// Static/LogGate.hpp).
|
||||
//
|
||||
// Paid once at startup for a genuinely unresolved class, that is
|
||||
// fine. Paid on every LooseObject construction on a per-bind path,
|
||||
// it is a dozen-plus ProcessEvent calls and a disk write per pal
|
||||
// selection. This tag ctor says "unchecked on purpose" and stays
|
||||
// silent.
|
||||
//
|
||||
// It does NOT weaken anything: LooseObject was already accepting
|
||||
// the object unchecked. The only thing removed is a diagnostic
|
||||
// that was describing a condition that wasn't happening.
|
||||
// ------------------------------------------------------------------
|
||||
struct UncheckedTag { explicit UncheckedTag() = default; };
|
||||
|
||||
UnrealWrapper(UObject* Obj, UncheckedTag, const TCHAR* DebugTag)
|
||||
: m_obj(Obj), m_tag(DebugTag ? DebugTag : STR("Wrapper"))
|
||||
{
|
||||
}
|
||||
|
||||
// Void-returning UFUNCTIONs only. Detail::Api<void> will log if the
|
||||
// engine function actually has a ReturnValue property, which is the
|
||||
// signal that you meant to use CallForReturn.
|
||||
template <typename ParamsStruct>
|
||||
auto Call(const TCHAR* FunctionName, ParamsStruct& Params) const -> bool
|
||||
{
|
||||
if (!m_obj)
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] '{}' on an invalid wrapper\n"), m_tag, FunctionName);
|
||||
return false;
|
||||
}
|
||||
return Detail::Api<void>().Invoke(m_obj, FunctionName, Params);
|
||||
}
|
||||
|
||||
auto CallVoid(const TCHAR* FunctionName) const -> bool
|
||||
{
|
||||
NoParams p{};
|
||||
return Call(FunctionName, p);
|
||||
}
|
||||
|
||||
// Zero-parameter getter.
|
||||
template <typename R>
|
||||
auto CallForReturn(const TCHAR* FunctionName) const -> std::optional<R>
|
||||
{
|
||||
struct Params { R ReturnValue; };
|
||||
Params p{};
|
||||
return CallForReturn<Params, R>(FunctionName, p, &Params::ReturnValue);
|
||||
}
|
||||
|
||||
// Getter with parameters. ReturnValue must be the LAST member of
|
||||
// ParamsStruct, matching the engine's frame layout.
|
||||
template <typename ParamsStruct, typename R>
|
||||
auto CallForReturn(const TCHAR* FunctionName, ParamsStruct& Params,
|
||||
R ParamsStruct::* ReturnMember) const -> std::optional<R>
|
||||
{
|
||||
if (!m_obj)
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] '{}' on an invalid wrapper\n"), m_tag, FunctionName);
|
||||
return std::nullopt;
|
||||
}
|
||||
return Detail::Api<R>().InvokeForReturn(m_obj, FunctionName, Params, ReturnMember);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Direct UPROPERTY access.
|
||||
//
|
||||
// Cheaper than ProcessEvent and the only option for properties with
|
||||
// no UFUNCTION accessor (UPanelSlot::Parent and ::Content, for
|
||||
// instance -- that class exposes zero UFUNCTIONs).
|
||||
//
|
||||
// Two things this does NOT handle:
|
||||
// 1. Bitfield bools (uint8 bFoo : 1). FProperty::GetSize() returns
|
||||
// 1 for those too, so the size check passes and you read the
|
||||
// whole byte -- every flag packed into it, not just yours.
|
||||
// Use the UFUNCTION getter for those. That is why UWidget's
|
||||
// bIsEnabled/bIsVariable are not exposed here.
|
||||
// 2. Writes. Poking a property directly skips the setter's
|
||||
// SynchronizeProperties call, so the Slate widget will not
|
||||
// update. Read by property, write by UFUNCTION.
|
||||
//
|
||||
// UE4SS API name check: GetPropertyByNameInChain and
|
||||
// GetOffset_Internal are what I expect this version to expose;
|
||||
// verify against your UObject.hpp / UnrealType.hpp before assuming
|
||||
// this compiles as written.
|
||||
// ------------------------------------------------------------------
|
||||
template <typename T>
|
||||
auto PropertyPtr(const TCHAR* PropertyName) const -> T*
|
||||
{
|
||||
if (!m_obj) return nullptr;
|
||||
|
||||
FProperty* Prop = m_obj->GetPropertyByNameInChain(PropertyName);
|
||||
if (!Prop)
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] property '{}' not found on {}\n"),
|
||||
m_tag, PropertyName, m_obj->GetFullName());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (static_cast<size_t>(Prop->GetSize()) != sizeof(T))
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] property '{}' is {} bytes engine-side, read as {} bytes\n"),
|
||||
m_tag, PropertyName, Prop->GetSize(), sizeof(T));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return reinterpret_cast<T*>(
|
||||
reinterpret_cast<uint8*>(m_obj) + Prop->GetOffset_Internal());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// PropertyPtr for the case where ABSENCE IS AN EXPECTED ANSWER, not
|
||||
// a mistake.
|
||||
//
|
||||
// PropertyPtr logs a Warning when the property is missing, and that
|
||||
// log line calls Obj->GetFullName() -- an outer-chain walk with an
|
||||
// FName::ToString at every level (see the UncheckedTag note above).
|
||||
// That is the right behaviour when you are naming a property you
|
||||
// believe exists and want to hear about it if you are wrong.
|
||||
//
|
||||
// It is the wrong behaviour when you are PROBING: "does this widget
|
||||
// happen to be a UUserWidget, i.e. does it have a WidgetTree?" is a
|
||||
// question asked once per node during a tree walk, and the answer is
|
||||
// "no" for almost every node. Logging that is not a diagnostic, it's
|
||||
// a per-node GetFullName() plus a disk write.
|
||||
//
|
||||
// A size mismatch still logs. That one is never an expected answer:
|
||||
// the property exists but is not the shape you think it is, which is
|
||||
// the silent-corruption case, not the absent case.
|
||||
// ------------------------------------------------------------------
|
||||
template <typename T>
|
||||
auto TryPropertyPtr(const TCHAR* PropertyName) const -> T*
|
||||
{
|
||||
if (!m_obj) return nullptr;
|
||||
|
||||
FProperty* Prop = m_obj->GetPropertyByNameInChain(PropertyName);
|
||||
if (!Prop) return nullptr;
|
||||
|
||||
if (static_cast<size_t>(Prop->GetSize()) != sizeof(T))
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] property '{}' is {} bytes engine-side, read as {} bytes\n"),
|
||||
m_tag, PropertyName, Prop->GetSize(), sizeof(T));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return reinterpret_cast<T*>(
|
||||
reinterpret_cast<uint8*>(m_obj) + Prop->GetOffset_Internal());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
auto PropertyValue(const TCHAR* PropertyName) const -> std::optional<T>
|
||||
{
|
||||
if (T* Ptr = PropertyPtr<T>(PropertyName)) return *Ptr;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Convenience for the most common property shape in this tree: an
|
||||
// ObjectProperty read back as a bare pointer. Silent on absence.
|
||||
auto TryObjectProperty(const TCHAR* PropertyName) const -> UObject*
|
||||
{
|
||||
auto Ptr = TryPropertyPtr<UObject*>(PropertyName);
|
||||
return Ptr ? *Ptr : nullptr;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// For `uint8 bFoo : 1;` UPROPERTY bitfields specifically. Do NOT
|
||||
// reach for PropertyValue<uint8>/PropertyPtr<uint8> on one of
|
||||
// these -- it is the wrong tool and it does not fail loudly.
|
||||
//
|
||||
// UHT reflects a bitfield as an FBoolProperty whose GetSize()
|
||||
// reports the storage unit (1 byte), not "1 bit" -- there is no
|
||||
// size check that can tell a lone byte-backed bool apart from one
|
||||
// of several single-bit flags packed into a shared byte. When a
|
||||
// header declares several `uint8 bX : 1;` fields back to back
|
||||
// (exactly the bOverride_* run on USizeBox), the compiler is free
|
||||
// to pack all of them into one physical byte. PropertyPtr<uint8>
|
||||
// would then hand back that WHOLE byte -- every packed flag
|
||||
// OR'd together -- for a call that named only one of them, and
|
||||
// both the found-a-property and the size-matches checks would
|
||||
// pass. That is a silent wrong answer, not a logged failure.
|
||||
//
|
||||
// FBoolProperty carries the two pieces of information that make
|
||||
// this correct: GetByteOffset(), the offset of the specific byte
|
||||
// within the struct (added ON TOP of the base FProperty offset
|
||||
// ContainerPtrToValuePtr already applies -- see
|
||||
// FBoolProperty::GetPropertyValue in UnrealType.hpp, which is
|
||||
// exactly the two-offset addition this function replicates), and
|
||||
// GetFieldMask(), the single bit within that byte belonging to
|
||||
// this specific flag. Test the bit, don't read the byte.
|
||||
auto PropertyBoolValue(const TCHAR* PropertyName) const -> std::optional<bool>
|
||||
{
|
||||
if (!m_obj) return std::nullopt;
|
||||
|
||||
FProperty* Prop = m_obj->GetPropertyByNameInChain(PropertyName);
|
||||
if (!Prop)
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] property '{}' not found on {}\n"),
|
||||
m_tag, PropertyName, m_obj->GetFullName());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop);
|
||||
if (!BoolProp)
|
||||
{
|
||||
Output::send<LogLevel::Warning>(
|
||||
STR("[{}] property '{}' is not a bool/bitfield property on {} -- ")
|
||||
STR("use PropertyValue<T> for a plain field instead\n"),
|
||||
m_tag, PropertyName, m_obj->GetFullName());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint8* Base = reinterpret_cast<const uint8*>(m_obj)
|
||||
+ Prop->GetOffset_Internal() + BoolProp->GetByteOffset();
|
||||
return (*Base & BoolProp->GetFieldMask()) != 0;
|
||||
}
|
||||
|
||||
UObject* m_obj{};
|
||||
const TCHAR* m_tag{ STR("Wrapper") };
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// RawObject -- the wrapper for objects this tree has no class for.
|
||||
//
|
||||
// Every helper that wants to read a property off an object it cannot
|
||||
// name a ClassPath for was previously spelling this inline as a local
|
||||
// struct deriving UnrealWrapper with Expected=nullptr (see the old
|
||||
// WidgetTree::FromUserWidget). That spelling takes the "class path did
|
||||
// not resolve" branch of the checking constructor, which emits a
|
||||
// Warning and calls GetFullName() EVERY TIME -- on a per-bind path,
|
||||
// that is the exact cost the UncheckedTag comment above exists to
|
||||
// avoid.
|
||||
//
|
||||
// This uses UncheckedTag, so it is silent by construction. It is the
|
||||
// same idea as Pal/PalIndividual.hpp's LooseObject, hoisted to where
|
||||
// the UMG side can reach it without including the Pal headers.
|
||||
// ------------------------------------------------------------------
|
||||
class RawObject : public UnrealWrapper
|
||||
{
|
||||
public:
|
||||
RawObject() = default;
|
||||
explicit RawObject(UObject* Obj, const TCHAR* Tag = STR("Raw"))
|
||||
: UnrealWrapper(Obj, UncheckedTag{}, Tag) {}
|
||||
|
||||
using UnrealWrapper::Call;
|
||||
using UnrealWrapper::CallVoid;
|
||||
using UnrealWrapper::CallForReturn;
|
||||
using UnrealWrapper::PropertyPtr;
|
||||
using UnrealWrapper::PropertyValue;
|
||||
using UnrealWrapper::TryPropertyPtr;
|
||||
using UnrealWrapper::TryObjectProperty;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Cheap "is this object one of ours" probe.
|
||||
//
|
||||
// Wrapper constructors log when the IsA check fails, which is right for
|
||||
// "I expected a CanvasPanel and got something else" and wrong for "walk
|
||||
// this tree and tell me which nodes are panels." Use this to ask first,
|
||||
// then construct.
|
||||
//
|
||||
// A false here can also mean the ClassPath never resolved. That is
|
||||
// deliberately NOT treated as "yes" -- unlike the wrapper constructors,
|
||||
// which accept unchecked on an unresolved class because refusing would
|
||||
// break a call that would otherwise have worked. Here the answer feeds
|
||||
// a branch, so guessing "yes" would send a non-panel down the panel
|
||||
// path. ClassOf<T>() logs once on the failed resolve either way.
|
||||
// ------------------------------------------------------------------
|
||||
template <typename T>
|
||||
inline auto ObjectIsA(UObject* Obj) -> bool
|
||||
{
|
||||
if (!Obj) return false;
|
||||
UClass* Cls = ClassOf<T>();
|
||||
return Cls && Obj->IsA(Cls);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user