#pragma once #include #include "TextLayoutWidget.hpp" namespace PalUIExtension::Api { class TextBlock : public TextLayoutWidget { public: static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.TextBlock"); } static auto DebugTag() -> const TCHAR* { return STR("TextBlock"); } TextBlock() = default; explicit TextBlock(UObject* Obj) : TextLayoutWidget(Obj, ClassOf(), DebugTag()) {} // ---- text ----------------------------------------------------------- // // Note: // SetText takes FText BY VALUE on the engine side (unlike UWidget::SetToolTipText, which takes const FText&). auto SetText(const FText& InText) -> bool { struct P { FText InText; } p{ InText }; return Call(STR("SetText"), p); } auto GetText() -> std::optional { return CallForReturn(STR("GetText")); } // ---- colour --------------------------------------------------------- auto SetColorAndOpacity(FSlateColor_ InColorAndOpacity) -> bool { struct P { FSlateColor_ InColorAndOpacity; } p{ InColorAndOpacity }; return Call(STR("SetColorAndOpacity"), p); } // Convenience: the common case is a literal colour, styling mode 0. auto SetColor(FLinearColor_ Color) -> bool { return SetColorAndOpacity(FSlateColor_{ Color, static_cast(ESlateColorStylingMode_::UseColor_Specified) }); } auto SetOpacity(float InOpacity) -> bool { struct P { float InOpacity; } p{ InOpacity }; return Call(STR("SetOpacity"), p); } auto SetShadowOffset(FVector2D_ InShadowOffset) -> bool { struct P { FVector2D_ InShadowOffset; } p{ InShadowOffset }; return Call(STR("SetShadowOffset"), p); } auto SetShadowColorAndOpacity(FLinearColor_ InColor) -> bool { struct P { FLinearColor_ InColor; } p{ InColor }; return Call(STR("SetShadowColorAndOpacity"), p); } // ---- layout / policy ------------------------------------------------- auto SetMinDesiredWidth(float InMinDesiredWidth) -> bool { struct P { float InMinDesiredWidth; } p{ InMinDesiredWidth }; return Call(STR("SetMinDesiredWidth"), p); } auto SetAutoWrapText(bool InAutoTextWrap) -> bool { struct P { bool InAutoTextWrap; } p{ InAutoTextWrap }; return Call(STR("SetAutoWrapText"), p); } auto SetTextTransformPolicy(ETextTransformPolicy_ InTransformPolicy) -> bool { struct P { ETextTransformPolicy_ InTransformPolicy; } p{ InTransformPolicy }; return Call(STR("SetTextTransformPolicy"), p); } auto SetTextOverflowPolicy(ETextOverflowPolicy_ InOverflowPolicy) -> bool { struct P { ETextOverflowPolicy_ InOverflowPolicy; } p{ InOverflowPolicy }; return Call(STR("SetTextOverflowPolicy"), p); } // ---- materials ------------------------------------------------------- // Plain object pointers. Nothing subtle here. auto SetFontMaterial(UObject* InMaterial) -> bool { struct P { UObject* InMaterial; } p{ InMaterial }; return Call(STR("SetFontMaterial"), p); } auto SetFontOutlineMaterial(UObject* InMaterial) -> bool { struct P { UObject* InMaterial; } p{ InMaterial }; return Call(STR("SetFontOutlineMaterial"), p); } auto GetDynamicFontMaterial() -> std::optional { return CallForReturn(STR("GetDynamicFontMaterial")); } auto GetDynamicOutlineMaterial() -> std::optional { return CallForReturn(STR("GetDynamicOutlineMaterial")); } // ---- property reads --------------------------------------------------- auto GetMinDesiredWidth() const -> std::optional { return PropertyValue(STR("MinDesiredWidth")); } auto GetShadowOffset() const -> std::optional { return PropertyValue(STR("ShadowOffset")); } auto GetShadowColorAndOpacity()const -> std::optional { return PropertyValue(STR("ShadowColorAndOpacity")); } auto GetColorAndOpacity() const -> std::optional { return PropertyValue(STR("ColorAndOpacity")); } auto GetTextTransformPolicy() const -> std::optional{ return PropertyValue(STR("TextTransformPolicy")); } auto GetTextOverflowPolicy() const -> std::optional { return PropertyValue(STR("TextOverflowPolicy")); } auto GetSimpleTextMode() const -> std::optional { return PropertyValue(STR("bSimpleTextMode")); } auto GetWrapWithInvalidationPanel() const -> std::optional { return PropertyValue(STR("bWrapWithInvalidationPanel")); } // ====================================================================== // FONT // // SetFont(FSlateFontInfo) and SetStrikeBrush(FSlateBrush) are NOT // exposed as value-taking calls, and this is not caution for its // own sake: // // FSlateFontInfo holds TSharedPtr. // FSlateBrush holds FSlateResourceHandle, which holds a shared ref. // // Any POD mirror you build will copy those pointers without touching // their refcounts. Then either the engine's copy-assign increments a // count you never decrement (leak), or a destructor runs on a count // you never incremented (a free of a live font, surfacing as a crash // in the renderer somewhere unrelated). There is no struct layout // that fixes this -- the problem is that these types are not data. // // What works instead: mutate the live struct in place through // reflection. No copy is ever made, so no refcount is ever touched. // ====================================================================== // Reads the current font size. Whether the engine stores it as float // or int32 varies by engine version and both are 4 bytes, so the // size check cannot tell them apart. This logs both readings once -- // exactly one of them will be a sane font size (something like 12 to // 48); that tells you which typed setter to call. Run it once, then // delete the call. auto LogFontSize() const -> void { void* SizePtr = FontMemberPtr(STR("Size"), 4); if (!SizePtr) return; float AsFloat = *reinterpret_cast(SizePtr); int32 AsInt = *reinterpret_cast(SizePtr); Output::send( STR("[TextBlock] Font.Size reads as float={} / int32={} -- ") STR("whichever looks like a font size is the real type\n"), AsFloat, AsInt); } auto SetFontSizeFloat(float NewSize) -> bool { if (void* P = FontMemberPtr(STR("Size"), 4)) { *reinterpret_cast(P) = NewSize; return PushFontToSlate(); } return false; } auto SetFontSizeInt(int32 NewSize) -> bool { if (void* P = FontMemberPtr(STR("Size"), 4)) { *reinterpret_cast(P) = NewSize; return PushFontToSlate(); } return false; } auto SetFontLetterSpacing(int32 Value) -> bool { if (void* P = FontMemberPtr(STR("LetterSpacing"), 4)) { *reinterpret_cast(P) = Value; return PushFontToSlate(); } return false; } auto SetFontSkewAmount(float Value) -> bool { if (void* P = FontMemberPtr(STR("SkewAmount"), 4)) { *reinterpret_cast(P) = Value; return PushFontToSlate(); } return false; } // Sets the named typeface (e.g. "Bold", "Regular", "Italic") on the // live font. FName is a trivially-copyable index pair, so this one // is a safe direct write. auto SetFontTypeface(FName TypefaceName) -> bool { if (void* P = FontMemberPtr(STR("TypefaceFontName"), sizeof(FName))) { *reinterpret_cast(P) = TypefaceName; return PushFontToSlate(); } return false; } // Raw access, for anything not covered above. Returns null and logs // if the member is missing or is not the size you expected. auto FontMemberPtr(const TCHAR* MemberName, size_t ExpectedSize) const -> void* { if (!m_obj) return nullptr; FProperty* FontProp = m_obj->GetPropertyByNameInChain(STR("Font")); if (!FontProp) { Output::send(STR("[TextBlock] no 'Font' property\n")); return nullptr; } // FStructProperty::GetStruct() gives the UScriptStruct whose // property chain describes FSlateFontInfo's members. auto* AsStruct = static_cast(FontProp); UScriptStruct* FontStruct = AsStruct->GetStruct(); if (!FontStruct) { Output::send(STR("[TextBlock] 'Font' is not a struct property\n")); return nullptr; } // The `TFieldRange iter = FontStruct->ForEachProperty();` that // used to sit here was a leftover from before this was a // range-for: it built a second, unused range object every call. FProperty* Member = nullptr; for (FProperty* Property : FontStruct->ForEachProperty()) { if (Property && Property->GetName() == MemberName) { Member = Property; break; } } if (!Member) { Output::send( STR("[TextBlock] FSlateFontInfo has no member '{}' on this build\n"), MemberName); return nullptr; } if (static_cast(Member->GetSize()) != ExpectedSize) { Output::send( STR("[TextBlock] FSlateFontInfo::{} is {} bytes, expected {}\n"), MemberName, Member->GetSize(), ExpectedSize); return nullptr; } uint8* FontBase = reinterpret_cast(m_obj) + FontProp->GetOffset_Internal(); return FontBase + Member->GetOffset_Internal(); } private: // ------------------------------------------------------------------ // The in-place write above changes UTextBlock::Font but does not // reach the SSlateTextBlock -- that only happens inside // SynchronizeProperties, which the setters call and a raw write // does not. There is no UFUNCTION that forces a resync. // // So this calls SetFont with the params block pointing AT THE LIVE // Font ITSELF. The engine does Font = InFontInfo, a real C++ // self-assignment (safe for TSharedPtr and FName), and then runs // the resync. Nothing is copied on our side, so nothing can get the // refcount wrong -- with one open question: // // Does UE4SS's ProcessEvent destruct the params buffer after the // call? If it does, this destructs the widget's live Font. // // Evidence it does not: your existing FTextFromString passes an // FString and reads back an FText through this same path, repeatedly, // without crashing. That is suggestive, not proof -- a refcount // underflow frees memory that stays readable until something else // claims it, so the crash lands far from the cause. // // This is the single highest-value thing to nail down in this // codebase: the answer unlocks FText, FSlateFontInfo, FSlateBrush // and TArray returns all at once. Test it in isolation: pass an // FString whose refcount you can watch, call through ProcessEvent, // and read the count after. // ------------------------------------------------------------------ auto PushFontToSlate() -> bool { if (!m_obj) return false; FProperty* FontProp = m_obj->GetPropertyByNameInChain(STR("Font")); if (!FontProp) return false; void* LiveFont = reinterpret_cast(m_obj) + FontProp->GetOffset_Internal(); UFunction* Fn = m_obj->GetFunctionByNameInChain(STR("SetFont")); if (!Fn) { Output::send(STR("[TextBlock] SetFont not found\n")); return false; } m_obj->ProcessEvent(Fn, LiveFont); return true; } }; }