476 lines
20 KiB
C++
476 lines
20 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 <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);
|
||
|
|
}
|
||
|
|
}
|