commit abf065dddea1265165b3a48fc081d784ca53ba7e Author: Hunter Date: Mon Sep 7 19:33:58 2026 -0400 initial commit; cv 0.0.3 diff --git a/Api/BoxPalList.hpp b/Api/BoxPalList.hpp new file mode 100644 index 0000000..d0fdf82 --- /dev/null +++ b/Api/BoxPalList.hpp @@ -0,0 +1,196 @@ +#pragma once + +#include + +#include + +#include "UnrealWrapper.hpp" +#include "UmgTypes.hpp" +#include "Widget.hpp" +#include "UserWidget.hpp" +#include "WidgetTree.hpp" +#include "CanvasPanel.hpp" +#include "TextBlock.hpp" + +namespace PalUIExtension::Api +{ + // ================================================================== + // THE PALBOX GRID CHAIN + // + // PalBoxWidget (the screen) + // -> WBP_BoxPalList_C THIS FILE + // -> WBP_BoxPalListBase_C THIS FILE + // -> WBP_BoxPalScrollList + // == WBP_PalCharacterScrollList_C + // -> the slot tiles + + // ================================================================== + class BoxPalListWidget : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalList.WBP_BoxPalList_C"); + } + static auto DebugTag() -> const TCHAR* { return STR("BoxPalList"); } + + BoxPalListWidget() = default; + + + explicit BoxPalListWidget(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) {} + + auto BaseObject() const -> UObject* { return BoundObject(STR("WBP_BoxPalListBase")); } + + auto CanDragDrop() const -> std::optional + { + return PropertyBoolValue(STR("CanDragDrop")); + } + auto DisplaysBanMarkInExpedition() const -> std::optional + { + return PropertyBoolValue(STR("bDisplayBanMarkInExpedition")); + } + + // ---- hooks --------------------------------------------------- + + static auto UpdateSlotFilteringPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalList.WBP_BoxPalList_C:UpdateSlotFiltering"); + } + static auto UpdateAllSlotFilteringPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalList.WBP_BoxPalList_C:UpdateAllSlotFiltering"); + } + static auto SetupPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalList.WBP_BoxPalList_C:Setup"); + } + static auto DestructPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalList.WBP_BoxPalList_C:Destruct"); + } + + struct SlotButtonFrame { UObject* SlotButton; }; + + }; + + + + class BoxPalListBaseWidget : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalListBase.WBP_BoxPalListBase_C"); + } + static auto DebugTag() -> const TCHAR* { return STR("BoxPalListBase"); } + + BoxPalListBaseWidget() = default; + explicit BoxPalListBaseWidget(UObject* Obj) + : UserWidget(Obj, ClassOf(), DebugTag()) {} + + auto ScrollList() const -> UObject* { return BoundObject(STR("WBP_BoxPalScrollList")); } + + auto LastSelectedPage() const -> std::optional { return PropertyValue(STR("LastSelectedPageNum")); } + auto MaxPageNum() const -> std::optional { return PropertyValue(STR("MaxPageNum")); } + auto SkipPageNum() const -> std::optional { return PropertyValue(STR("SkipPageNum")); } + + auto IsValidPageAction() const -> std::optional { return PropertyBoolValue(STR("bIsValidPageAction")); } + auto IsSkippingPage() const -> std::optional { return PropertyBoolValue(STR("bSkipPage")); } + + // ---- chrome, promoted ----------------------------------------- + auto CanvasSliderValue() const -> CanvasPanel + { + UObject* O = BoundObject(STR("Canvas_SliderValue")); + return O ? CanvasPanel{ O } : CanvasPanel{}; + } + + auto TextBoxName() const -> TextBlock { return WrapText(STR("Text_BoxName")); } + auto TextSliderValue() const -> TextBlock { return WrapText(STR("Text_SliderValue")); } + + auto Slider() const -> UObject* { return BoundObject(STR("Slider")); } + auto SearchButton() const -> UObject* { return BoundObject(STR("WBP_CommonButton_Activation_Search")); } + auto SortButton() const -> UObject* { return BoundObject(STR("WBP_CommonButton_Sort")); } + auto NumArrowLeft() const -> UObject* { return BoundObject(STR("WBP_Common_NumArrow_L")); } + auto NumArrowRight() const -> UObject* { return BoundObject(STR("WBP_Common_NumArrow_R")); } + + static auto OnCreatedNewSlotPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalListBase.WBP_BoxPalListBase_C:OnCreatedNewSlot__DelegateSignature"); + } + static auto OnUpdateSlotPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalListBase.WBP_BoxPalListBase_C:OnUpdateSlot__DelegateSignature"); + } + static auto OnSlotSyncedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalListBase.WBP_BoxPalListBase_C:OnSlotSynced__DelegateSignature"); + } + static auto OnUpdatedPagePath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalListBase.WBP_BoxPalListBase_C:OnUpdatedPage__DelegateSignature"); + } + static auto SetCharacterSlotsPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalListBase.WBP_BoxPalListBase_C:SetCharacterSlots"); + } + static auto SetCurrentPagePath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalListBase.WBP_BoxPalListBase_C:SetCurrentPage"); + } + static auto DestructPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/Common/" + "WBP_BoxPalListBase.WBP_BoxPalListBase_C:Destruct"); + } + + struct SlotWidgetFrame { UObject* Widget; }; + + struct SlotClickFrame + { + UObject* Widget; // 0x00 + uint8 PressType; // 0x08 + }; + + struct PageFrame { int32 NowPage; }; // OnUpdatedPage, OnSelectPalBoxPage + + struct SetCharacterSlotsFrame + { + UObject** Data; + int32 Num; + int32 Max; + }; + static_assert(sizeof(SetCharacterSlotsFrame) == 16, + "SetCharacterSlots frame is 16 bytes on the dumped build"); + + auto GetCurrentPage() -> std::optional + { + struct Frame { int32 CurrentPageNum; } p{}; + if (!Call(STR("GetCurrentPage"), p)) return std::nullopt; + return p.CurrentPageNum; + } + + + private: + auto WrapText(const TCHAR* Name) const -> TextBlock + { + UObject* O = BoundObject(Name); + return O ? TextBlock{ O } : TextBlock{}; + } + + protected: + BoxPalListBaseWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/Canvas.hpp b/Api/Canvas.hpp new file mode 100644 index 0000000..e69de29 diff --git a/Api/CanvasPanel.hpp b/Api/CanvasPanel.hpp new file mode 100644 index 0000000..fc44a0e --- /dev/null +++ b/Api/CanvasPanel.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "PanelWidget.hpp" +#include "CanvasPanelSlot.hpp" + +namespace PalUIExtension::Api +{ + // UCanvasPanel adds exactly one function of its own. Everything else + // it offers comes from UPanelWidget and UWidget, which is now inherited + // rather than re-declared. + class CanvasPanel : public PanelWidget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.CanvasPanel"); } + static auto DebugTag() -> const TCHAR* { return STR("Canvas"); } + + CanvasPanel() = default; + explicit CanvasPanel(UObject* Obj) + : PanelWidget(Obj, ClassOf(), DebugTag()) {} + + // Returns an invalid CanvasPanelSlot if the call failed or the + // engine handed back null. Check it before using it -- the wrapper + // will refuse to dispatch and log, but silently doing nothing is + // not what you want out of a layout call. + auto AddChildToCanvas(UObject* Content) -> CanvasPanelSlot + { + struct P { UObject* Content; UObject* ReturnValue; } p{ Content, nullptr }; + auto Result = CallForReturn(STR("AddChildToCanvas"), p, &P::ReturnValue); + return (Result && *Result) ? CanvasPanelSlot{ *Result } : CanvasPanelSlot{}; + } + }; +} diff --git a/Api/CanvasPanelSlot.hpp b/Api/CanvasPanelSlot.hpp new file mode 100644 index 0000000..61595ed --- /dev/null +++ b/Api/CanvasPanelSlot.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include "PanelSlot.hpp" + +namespace PalUIExtension::Api +{ + class CanvasPanelSlot : public PanelSlot + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.CanvasPanelSlot"); } + static auto DebugTag() -> const TCHAR* { return STR("CanvasSlot"); } + + CanvasPanelSlot() = default; + explicit CanvasPanelSlot(UObject* Obj) + : PanelSlot(Obj, ClassOf(), DebugTag()) {} + + // ---- setters (write through the UFUNCTION so SynchronizeProperties runs) + + auto SetZOrder(int32 InZOrder) -> bool + { + struct P { int32 InZOrder; } p{ InZOrder }; + return Call(STR("SetZOrder"), p); + } + auto SetSize(FVector2D_ InSize) -> bool + { + struct P { FVector2D_ InSize; } p{ InSize }; + return Call(STR("SetSize"), p); + } + auto SetPosition(FVector2D_ InPosition) -> bool + { + struct P { FVector2D_ InPosition; } p{ InPosition }; + return Call(STR("SetPosition"), p); + } + auto SetOffsets(FMargin_ InOffset) -> bool + { + struct P { FMargin_ InOffset; } p{ InOffset }; + return Call(STR("SetOffsets"), p); + } + auto SetMinimum(FVector2D_ InMinimumAnchors) -> bool + { + struct P { FVector2D_ InMinimumAnchors; } p{ InMinimumAnchors }; + return Call(STR("SetMinimum"), p); + } + auto SetMaximum(FVector2D_ InMaximumAnchors) -> bool + { + struct P { FVector2D_ InMaximumAnchors; } p{ InMaximumAnchors }; + return Call(STR("SetMaximum"), p); + } + auto SetAnchors(FAnchors_ InAnchors) -> bool + { + struct P { FAnchors_ InAnchors; } p{ InAnchors }; + return Call(STR("SetAnchors"), p); + } + auto SetAlignment(FVector2D_ InAlignment) -> bool + { + struct P { FVector2D_ InAlignment; } p{ InAlignment }; + return Call(STR("SetAlignment"), p); + } + auto SetAutoSize(bool InbAutoSize) -> bool + { + struct P { bool InbAutoSize; } p{ InbAutoSize }; + return Call(STR("SetAutoSize"), p); + } + // Engine signature is (const FAnchorData&); Blueprint const-ref params + // are still laid out by value in the ProcessEvent frame. + // FAnchorData_ field order is the guess flagged in UmgTypes.hpp -- + // prefer the granular setters until you have dumped the struct. + auto SetLayout(const FAnchorData_& InLayoutData) -> bool + { + struct P { FAnchorData_ InLayoutData; } p{ InLayoutData }; + return Call(STR("SetLayout"), p); + } + + // ---- getters -------------------------------------------------------- + // + // ZOrder, bAutoSize and LayoutData are plain UPROPERTYs, so these + // three read the memory directly instead of building a ProcessEvent + // frame. The rest have no backing property of their own (they are + // views into LayoutData) and go through the UFUNCTION. + + auto GetZOrder() const -> std::optional { return PropertyValue(STR("ZOrder")); } + auto GetAutoSize() const -> std::optional { return PropertyValue(STR("bAutoSize")); } + auto GetLayout() const -> std::optional { return PropertyValue(STR("LayoutData")); } + + auto GetSize() -> std::optional { return CallForReturn(STR("GetSize")); } + auto GetPosition() -> std::optional { return CallForReturn(STR("GetPosition")); } + auto GetAlignment() -> std::optional { return CallForReturn(STR("GetAlignment")); } + auto GetOffsets() -> std::optional { return CallForReturn(STR("GetOffsets")); } + auto GetAnchors() -> std::optional { return CallForReturn(STR("GetAnchors")); } + }; +} diff --git a/Api/HorizontalBox.hpp b/Api/HorizontalBox.hpp new file mode 100644 index 0000000..dba0717 --- /dev/null +++ b/Api/HorizontalBox.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "PanelWidget.hpp" +#include "HorizontalBoxSlot.hpp" + +namespace PalUIExtension::Api +{ + // UHorizontalBox : public UPanelWidget directly -- same shape as + // Overlay. Every generic panel operation is already on PanelWidget; + // AddChildToHorizontalBox exists only to hand back the concrete slot + // type instead of the generic UPanelSlot* AddChild() returns. + class HorizontalBox : public PanelWidget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.HorizontalBox"); } + static auto DebugTag() -> const TCHAR* { return STR("HorizontalBox"); } + + HorizontalBox() = default; + explicit HorizontalBox(UObject* Obj) + : PanelWidget(Obj, ClassOf(), DebugTag()) {} + + auto AddChildToHorizontalBox(UObject* Content) -> HorizontalBoxSlot + { + struct P { UObject* Content; UObject* ReturnValue; } p{ Content, nullptr }; + auto Result = CallForReturn(STR("AddChildToHorizontalBox"), p, &P::ReturnValue); + return (Result && *Result) ? HorizontalBoxSlot{ *Result } : HorizontalBoxSlot{}; + } + }; +} diff --git a/Api/HorizontalBoxSlot.hpp b/Api/HorizontalBoxSlot.hpp new file mode 100644 index 0000000..064521b --- /dev/null +++ b/Api/HorizontalBoxSlot.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "PanelSlot.hpp" + +namespace PalUIExtension::Api +{ + // Same four-member surface as VerticalBoxSlot (Size + Padding + H/V + // alignment). Different engine class, different ClassPath -- kept + // separate rather than shared, matching how CanvasPanelSlot, + // SizeBoxSlot, and OverlaySlot each stand alone in this tree despite + // overlapping surfaces. + class HorizontalBoxSlot : public PanelSlot + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.HorizontalBoxSlot"); } + static auto DebugTag() -> const TCHAR* { return STR("HorizontalBoxSlot"); } + + HorizontalBoxSlot() = default; + explicit HorizontalBoxSlot(UObject* Obj) + : PanelSlot(Obj, ClassOf(), DebugTag()) {} + + // ---- setters (write through the UFUNCTION so SynchronizeProperties runs) + + auto SetSize(FSlateChildSize_ InSize) -> bool + { + struct P { FSlateChildSize_ InSize; } p{ InSize }; + return Call(STR("SetSize"), p); + } + auto SetPadding(FMargin_ InPadding) -> bool + { + struct P { FMargin_ InPadding; } p{ InPadding }; + return Call(STR("SetPadding"), p); + } + auto SetHorizontalAlignment(EHorizontalAlignment_ InHorizontalAlignment) -> bool + { + struct P { EHorizontalAlignment_ InHorizontalAlignment; } p{ InHorizontalAlignment }; + return Call(STR("SetHorizontalAlignment"), p); + } + auto SetVerticalAlignment(EVerticalAlignment_ InVerticalAlignment) -> bool + { + struct P { EVerticalAlignment_ InVerticalAlignment; } p{ InVerticalAlignment }; + return Call(STR("SetVerticalAlignment"), p); + } + + // ---- property reads + + auto GetSize() const -> std::optional { return PropertyValue(STR("Size")); } + auto GetPadding() const -> std::optional { return PropertyValue(STR("Padding")); } + auto GetHorizontalAlignment() const -> std::optional { return PropertyValue(STR("HorizontalAlignment")); } + auto GetVerticalAlignment() const -> std::optional { return PropertyValue(STR("VerticalAlignment")); } + }; +} diff --git a/Api/MainMenuPalWidget.hpp b/Api/MainMenuPalWidget.hpp new file mode 100644 index 0000000..2b1340c --- /dev/null +++ b/Api/MainMenuPalWidget.hpp @@ -0,0 +1,246 @@ +#pragma once + +#include +#include + +#include + +#include "UnrealWrapper.hpp" +#include "UmgTypes.hpp" +#include "Widget.hpp" +#include "UserWidget.hpp" +#include "WidgetTree.hpp" +#include "CanvasPanel.hpp" +#include "Overlay.hpp" +#include "VerticalBox.hpp" +#include "TextBlock.hpp" + +#include + +namespace PalUIExtension::Api +{ + class MainMenuPalWidget : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C"); + } + static auto DebugTag() -> const TCHAR* { return STR("MainMenuPal"); } + + MainMenuPalWidget() = default; + explicit MainMenuPalWidget(UObject* Obj) + : UserWidget(Obj, ClassOf(), DebugTag()) { + } + + auto CanvasPalName() const -> CanvasPanel { return WrapCanvas(STR("Canvas_PalName")); } + auto CanvasPalStatus() const -> CanvasPanel { return WrapCanvas(STR("Canvas_PalStatus")); } + auto CanvasSkillInfo() const -> CanvasPanel { return WrapCanvas(STR("Canvas_SkillInfo")); } + auto CanvasPartnerSkill() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanel_PartnerSkill")); } + auto CanvasPotencial() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanel_Potencial")); } // engine spelling + auto CanvasDebuff() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanelDebuff")); } + auto CanvasAwakening() const -> CanvasPanel { return WrapCanvas(STR("Canvas_Awakening")); } + auto CanvasDropGuide() const -> CanvasPanel { return WrapCanvas(STR("Canvas_DropGuide")); } + + auto CanvasLocked() const -> CanvasPanel { return WrapCanvas(STR("Canvas_Locked")); } + auto CanvasUnlocked() const -> CanvasPanel { return WrapCanvas(STR("Canvas_Unlocked")); } + auto CanvasLock1() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanelLock_1")); } + auto CanvasLockText() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanelLockText")); } + auto CanvasLockText1() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanelLockText_1")); } + + auto BoxPalList() const -> VerticalBox { return WrapVBox(STR("VerticalBox_Pal_List")); } + auto BoxSkill() const -> VerticalBox { return WrapVBox(STR("VerticalBox_Skill")); } + + auto OverlayCircle() const -> Overlay { return WrapOverlay(STR("Overlay_Circle")); } + auto OverlayIconCircle() const -> Overlay { return WrapOverlay(STR("Overlay_Icon_Circle")); } + auto OverlayIconSquare() const -> Overlay { return WrapOverlay(STR("Overlay_Icon_sq")); } + auto OverlaySquare() const -> Overlay { return WrapOverlay(STR("Overlay_sq")); } + + auto GridSuitability() const -> UObject* { return BoundObject(STR("UniformGrid_Suitability")); } + + auto FindCanvas(const TCHAR* Name) -> CanvasPanel { return Child(Name); } + + auto CachedHandle() const -> UObject* { return BoundObject(STR("CachedIndividualHandle")); } + + auto CachedSpecies() const -> std::optional + { + return Pal::PalIndividual::CharacterIdFromHandle(CachedHandle()); + } + auto CachedIdentity() const -> Pal::PalIdentity + { + return Pal::PalIndividual::Identify(CachedHandle()); + } + + // ---- screen state -------------------------------------------- + + auto IsShowingStatusWithList() const -> std::optional { return PropertyBoolValue(STR("isShowStatusWithList")); } + auto IsLocked() const -> std::optional { return PropertyBoolValue(STR("Is Lock")); } + auto IsEditingNickName() const -> std::optional { return PropertyBoolValue(STR("IsEditingNickName")); } + + // ============================================================== + // WIDGET ARRAYS + // ============================================================== + + auto PalPanels() const -> std::vector { return ReadObjectArray(STR("PalPanels")); } + auto ActiveSkillPanels() const -> std::vector { return ReadObjectArray(STR("ActiveSkillPanelArray")); } + auto PassiveSkillPanels() const -> std::vector { return ReadObjectArray(STR("PassiveSkillPanelArray")); } + auto ConditionWidgets() const -> std::vector { return ReadObjectArray(STR("ConditionWidgetArray")); } + auto RarityWidgets() const -> std::vector { return ReadObjectArray(STR("RarityWidgetArray")); } + + // The engine's spelling of "SideInfo" is "SindeInfo". Lol. + auto SideInfoConditionWidgets() const -> std::vector { return ReadObjectArray(STR("SindeInfoConditionWidgetArray")); } + + // ============================================================== + // TEXT + // ============================================================== + auto TextNowHP() const -> TextBlock { return WrapText(STR("Text_NowHP")); } + auto TextMaxHP() const -> TextBlock { return WrapText(STR("Text_MaxHP")); } + auto TextNowHunger() const -> TextBlock { return WrapText(STR("Text_NowHunger")); } + auto TextMaxHunger() const -> TextBlock { return WrapText(STR("Text_MaxHunger")); } + auto TextNowSanity() const -> TextBlock { return WrapText(STR("Text_NowSanity")); } + auto TextLevel() const -> TextBlock { return WrapText(STR("Text_LevelValue")); } + auto TextNextExp() const -> TextBlock { return WrapText(STR("Text_NextExp")); } + auto TextDefense() const -> TextBlock { return WrapText(STR("Text_DefenseValue")); } + auto TextRangeAttack() const -> TextBlock { return WrapText(STR("Text_RangeAttackValue")); } + auto TextWorkSpeed() const -> TextBlock { return WrapText(STR("Text_WorkSpeedValue")); } + auto TextFriendship() const -> TextBlock { return WrapText(STR("Text_FriendshipRank_Num")); } + auto TextNickName() const -> TextBlock { return WrapText(STR("BP_PalTextBlock_NickName")); } + auto TextLvLabel() const -> TextBlock { return WrapText(STR("BP_PalTextBlock_LvText")); } + auto TextSoulRank() const -> TextBlock { return WrapText(STR("BP_PalTextBlock_SoulRank")); } + + auto RichTextPartnerSkillName() const -> UObject* { return BoundObject(STR("RichText_PartnerSkillName")); } + auto RichTextPartnerSkillDesc() const -> UObject* { return BoundObject(STR("RichText_PartnerSkillDesc")); } + + // ---- gauges, images, nested widgets: raw --------------------- + + auto GaugeHP() const -> UObject* { return BoundObject(STR("Gauge_HP")); } + auto GaugeHunger() const -> UObject* { return BoundObject(STR("Gauge_Hunger")); } + auto GaugeExp() const -> UObject* { return BoundObject(STR("Gauge_Exp")); } + auto GaugeFriendship() const -> UObject* { return BoundObject(STR("Gauge_Friendship")); } + + auto ImageRare() const -> UObject* { return BoundObject(STR("Image_Rare")); } + auto ImageStrong() const -> UObject* { return BoundObject(STR("Image_Strong")); } + auto ImageMutant() const -> UObject* { return BoundObject(STR("Image_Mutant")); } + auto ImageLocked() const -> UObject* { return BoundObject(STR("Image_Icon_Locked")); } + auto ImageGlobalImport() const -> UObject* { return BoundObject(STR("Image_Icon_GlobalInport")); } // engine spelling + + auto FoodAmountWidget() const -> UObject* { return BoundObject(STR("WBP_MainMenu_Pal_FoodAmount")); } + auto GenderIcon() const -> UObject* { return BoundObject(STR("WBP_PalGenderIcon")); } + auto SkillInfoWidget() const -> UObject* { return BoundObject(STR("WBP_MainMenu_PalSkillInfo")); } + auto PotencialWidget() const -> UObject* { return BoundObject(STR("WBP_Menu_Pal_Potencial")); } // engine spelling + auto NoDataWidget() const -> UObject* { return BoundObject(STR("WBP_NoData")); } + auto BuffTimers() const -> UObject* { return BoundObject(STR("WBP_StatusBuffTimerContainer")); } + auto KeyGuideIcon() const -> UObject* { return BoundObject(STR("WBP_PalKeyGuideIcon")); } + + // ============================================================== + // HOOK TARGETS + // ============================================================== + + static auto BindFromHandlePath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C:BindFromHandle"); + } + static auto SetPalHandlesPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C:Set Pal Handles"); + } + static auto SetupStatusPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C:Setup Status"); + } + static auto ListToStatusPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C:ListToStatus"); + } + static auto OnClickedPalButtonPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C:OnClickedPalButtonEvent"); + } + static auto OnHoveredPalButtonPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C:OnHoveredPalButtonEvent"); + } + // The honest bookends for "this screen exists" and "your cached + // pointers are now garbage." If you cache anything off this + // class, hook Destruct. + static auto ConstructPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C:Construct"); + } + static auto DestructPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/MainMenu/Pal/" + "WBP_MainMenu_Pal_00.WBP_MainMenu_Pal_00_C:Destruct"); + } + + // ---- frames -------------------------------------------------- + struct BindFromHandleFrame { UObject* targetHandle; }; + struct HandleFrame { UObject* Handle; }; + struct SetPalHandlesFrame + { + UObject** Data; + int32 Num; + int32 Max; + }; + + private: + auto WrapCanvas(const TCHAR* Name) const -> CanvasPanel + { + UObject* O = BoundObject(Name); + return O ? CanvasPanel{ O } : CanvasPanel{}; + } + auto WrapVBox(const TCHAR* Name) const -> VerticalBox + { + UObject* O = BoundObject(Name); + return O ? VerticalBox{ O } : VerticalBox{}; + } + auto WrapOverlay(const TCHAR* Name) const -> Overlay + { + UObject* O = BoundObject(Name); + return O ? Overlay{ O } : Overlay{}; + } + auto WrapText(const TCHAR* Name) const -> TextBlock + { + UObject* O = BoundObject(Name); + return O ? TextBlock{ O } : TextBlock{}; + } + + static constexpr int32 kMaxPlausibleEntries = 1024; + + auto ReadObjectArray(const TCHAR* PropertyName) const -> std::vector + { + std::vector Out; + + auto* Arr = TryPropertyPtr>(PropertyName); + if (!Arr) return Out; + + if (!Arr->IsPlausible(kMaxPlausibleEntries)) + { + Output::send( + STR("[{}] '{}' does not look like a live TArray (Num={}, Max={})\n"), + m_tag, PropertyName, Arr->Num, Arr->Max); + return Out; + } + + Out.reserve(static_cast(Arr->Num)); + for (int32 i = 0; i < Arr->Num; ++i) + { + if (UObject* Entry = Arr->Data[i]) Out.push_back(Entry); + } + return Out; + } + + protected: + MainMenuPalWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) { + } + }; +} \ No newline at end of file diff --git a/Api/Mod/PalDetail_PalIconNightEye.hpp b/Api/Mod/PalDetail_PalIconNightEye.hpp new file mode 100644 index 0000000..ff1c9dd --- /dev/null +++ b/Api/Mod/PalDetail_PalIconNightEye.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace PalUIExtension::Api::Pal; + +namespace PalUIExtension::Api::Mod +{ + + class PalDetail_PalIconNightEye : public UserWidget + { + public: + + constexpr static auto ClassPath() -> const TCHAR* { + return STR("/Game/Mods/PalUIExtension/PalDetail_PalIconNightEye.PalDetail_PalIconNightEye_C"); + } + + constexpr static auto WidgetTreePath() -> const TCHAR* { + return STR("/Game/Mods/PalUIExtension/PalDetail_PalIconNightEye.PalDetail_PalIconNightEye_C:WidgetTree"); + } + + static auto DebugTag() -> const TCHAR* { return STR("PalDetail_PalIconNightEye"); } + + PalDetail_PalIconNightEye() = default; + + explicit PalDetail_PalIconNightEye(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) { + + } + + auto GetIconVisibility() -> std::optional + { + return CallForReturn(STR("GetIconVisibility")); + } + // ---- calls --------------------------------------------------- + + struct SetIconVisibilityFrame { bool visibility; bool localVar1; }; + + auto SetIconVisibility(bool newState) -> bool { + SetIconVisibilityFrame p{ + newState, + newState + }; + if (!Call(STR("SetIconVisibility"), p)){ + Output::send( + STR("[SetIconVisibility] Call to method failed.\n")); + return false; + } + return true; + } + + protected: + PalDetail_PalIconNightEye(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) { + } + }; +} \ No newline at end of file diff --git a/Api/Mod/PalDetail_PalNumber.hpp b/Api/Mod/PalDetail_PalNumber.hpp new file mode 100644 index 0000000..e9bd00c --- /dev/null +++ b/Api/Mod/PalDetail_PalNumber.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace PalUIExtension::Api::Pal; + +namespace PalUIExtension::Api::Mod +{ + + class PalDetail_PalNumberWidget : public UserWidget + { + public: + + static auto ClassPath() -> const TCHAR* { + return STR("/Game/Mods/PalUIExtension/PalDetail_PalNumber.PalDetail_PalNumber_C"); + } + + static auto WidgetTreePath() -> const TCHAR* { + return STR("/Game/Mods/PalUIExtension/PalDetail_PalNumber.PalDetail_PalNumber_C:WidgetTree"); + } + + + + static auto DebugTag() -> const TCHAR* { return STR("PalDetail_PalNumber"); } + + PalDetail_PalNumberWidget() = default; + + explicit PalDetail_PalNumberWidget(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) { + + } + + auto GetBindedHandle() -> UObject* + { + struct P { UObject* returnValue; } p{ nullptr }; + if (!CallForReturn(STR("GetBindedHandle"), p, &P::returnValue)) return nullptr; + return p.returnValue; + } + + auto GetBindedName() -> std::optional + { + struct P { FName returnValue; } p{ FName{} }; + if (!CallForReturn(STR("GetBindedName"), p, &P::returnValue)) return std::nullopt; + return p.returnValue; + } + + auto CurrentHandle() -> PalIndividualCharacterHandle + { + UObject* H = GetBindedHandle(); + return H ? PalIndividualCharacterHandle{ H } : PalIndividualCharacterHandle{}; + } + + + auto HasBinding() -> bool + { + return CallForReturn(STR("GetHasBinding")).value_or(false); + } + // ---- calls --------------------------------------------------- + + struct SetPalNumberTextFrame { FString text; }; + + auto SetPalNumberText(const FString& text) -> bool { + SetPalNumberTextFrame p{text}; + if (!Call(STR("SetPalNumberText"), p)) return false; + return true; + } + + auto SetPalBinding(FPalInstanceID _target) -> std::optional{ + struct P { + FPalInstanceID TargetInstanceId; + FName ResolvedName; + UObject* GetIndivCharacterParamReturnVal; + bool IsValid; + FName GetCharacterIdReturnVal; + } p{ _target, FName{} }; + if (!CallForReturn(STR("SetPalBinding"), p, &P::ResolvedName)) return std::nullopt; + return p.ResolvedName; + } + + + protected: + PalDetail_PalNumberWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) { + } + }; +} \ No newline at end of file diff --git a/Api/Mod/PalStatus_PalNumber.hpp b/Api/Mod/PalStatus_PalNumber.hpp new file mode 100644 index 0000000..072c2dc --- /dev/null +++ b/Api/Mod/PalStatus_PalNumber.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace PalUIExtension::Api::Pal; + +namespace PalUIExtension::Api::Mod +{ + + class PalStatus_PalNumberWidget : public UserWidget + { + public: + + static auto ClassPath() -> const TCHAR* { + return STR("/Game/Mods/PalUIExtension/PalStatus_PalNumber.PalStatus_PalNumber_C"); + } + + static auto WidgetTreePath() -> const TCHAR* { + return STR("/Game/Mods/PalUIExtension/PalStatus_PalNumber.PalStatus_PalNumber_C:WidgetTree"); + } + + + + static auto DebugTag() -> const TCHAR* { return STR("PalStatus_PalNumber"); } + + PalStatus_PalNumberWidget() = default; + + explicit PalStatus_PalNumberWidget(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) { + + } + + auto GetBindedHandle() -> UObject* + { + struct P { UObject* returnValue; } p{ nullptr }; + if (!CallForReturn(STR("GetBindedHandle"), p, &P::returnValue)) return nullptr; + return p.returnValue; + } + + auto GetBindedName() -> std::optional + { + struct P { FName returnValue; } p{ FName{} }; + if (!CallForReturn(STR("GetBindedName"), p, &P::returnValue)) return std::nullopt; + return p.returnValue; + } + + auto CurrentHandle() -> PalIndividualCharacterHandle + { + UObject* H = GetBindedHandle(); + return H ? PalIndividualCharacterHandle{ H } : PalIndividualCharacterHandle{}; + } + + + auto HasBinding() -> bool + { + return CallForReturn(STR("GetHasBinding")).value_or(false); + } + // ---- calls --------------------------------------------------- + + struct SetPalNumberTextFrame { FString text; }; + + auto SetPalNumberText(const FString& text) -> bool { + SetPalNumberTextFrame p{text}; + if (!Call(STR("SetPalNumberText"), p)) return false; + return true; + } + + auto SetPalBinding(FPalInstanceID _target) -> std::optional{ + struct P { + FPalInstanceID TargetInstanceId; + FName ResolvedName; + UObject* GetIndivCharacterParamReturnVal; + bool IsValid; + FName GetCharacterIdReturnVal; + } p{ _target, FName{} }; + if (!CallForReturn(STR("SetPalBinding"), p, &P::ResolvedName)) return std::nullopt; + return p.ResolvedName; + } + + + protected: + PalStatus_PalNumberWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) { + } + }; +} \ No newline at end of file diff --git a/Api/Overlay.hpp b/Api/Overlay.hpp new file mode 100644 index 0000000..c85386c --- /dev/null +++ b/Api/Overlay.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "PanelWidget.hpp" +#include "OverlaySlot.hpp" + +namespace PalUIExtension::Api +{ + // UOverlay : public UPanelWidget directly (confirmed by the header -- + // no ContentWidget rung to skip here, unlike SizeBox). Every generic + // panel operation -- AddChild, GetChildAt, GetAllChildren, + // GetChildObjects -- is already on PanelWidget and works unchanged; + // AddChildToOverlay below is the one thing Overlay adds beyond that, + // and it exists only because the engine returns the concrete + // UOverlaySlot* type instead of the generic UPanelSlot* AddChild() + // returns, so you get typed access to Padding/Alignment without an + // extra cast. + class Overlay : public PanelWidget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.Overlay"); } + static auto DebugTag() -> const TCHAR* { return STR("Overlay"); } + + Overlay() = default; + explicit Overlay(UObject* Obj) + : PanelWidget(Obj, ClassOf(), DebugTag()) {} + + auto AddChildToOverlay(UObject* Content) -> OverlaySlot + { + struct P { UObject* Content; UObject* ReturnValue; } p{ Content, nullptr }; + auto Result = CallForReturn(STR("AddChildToOverlay"), p, &P::ReturnValue); + return (Result && *Result) ? OverlaySlot{ *Result } : OverlaySlot{}; + } + }; +} diff --git a/Api/OverlaySlot.hpp b/Api/OverlaySlot.hpp new file mode 100644 index 0000000..15a95ea --- /dev/null +++ b/Api/OverlaySlot.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "PanelSlot.hpp" + +namespace PalUIExtension::Api +{ + // Same three-member surface as SizeBoxSlot (Padding + H/V alignment). + // Different engine class, different ClassPath -- not merged into one + // wrapper, matching how CanvasPanelSlot stands alone rather than + // sharing with PanelSlot's other concrete subclasses. + class OverlaySlot : public PanelSlot + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.OverlaySlot"); } + static auto DebugTag() -> const TCHAR* { return STR("OverlaySlot"); } + + OverlaySlot() = default; + explicit OverlaySlot(UObject* Obj) + : PanelSlot(Obj, ClassOf(), DebugTag()) {} + + // ---- setters (write through the UFUNCTION so SynchronizeProperties runs) + + auto SetPadding(FMargin_ InPadding) -> bool + { + struct P { FMargin_ InPadding; } p{ InPadding }; + return Call(STR("SetPadding"), p); + } + auto SetHorizontalAlignment(EHorizontalAlignment_ InHorizontalAlignment) -> bool + { + struct P { EHorizontalAlignment_ InHorizontalAlignment; } p{ InHorizontalAlignment }; + return Call(STR("SetHorizontalAlignment"), p); + } + auto SetVerticalAlignment(EVerticalAlignment_ InVerticalAlignment) -> bool + { + struct P { EVerticalAlignment_ InVerticalAlignment; } p{ InVerticalAlignment }; + return Call(STR("SetVerticalAlignment"), p); + } + + // ---- property reads + + auto GetPadding() const -> std::optional { return PropertyValue(STR("Padding")); } + auto GetHorizontalAlignment()const -> std::optional { return PropertyValue(STR("HorizontalAlignment")); } + auto GetVerticalAlignment() const -> std::optional { return PropertyValue(STR("VerticalAlignment")); } + }; +} diff --git a/Api/Pal/PalCharacter.hpp b/Api/Pal/PalCharacter.hpp new file mode 100644 index 0000000..c433e87 --- /dev/null +++ b/Api/Pal/PalCharacter.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include + +#include + +#include +#include "PalIndividual.hpp" +#include "PalTypes.hpp" + +namespace PalUIExtension::Api::Pal +{ + + class PalCharacter : public UnrealWrapper + { + + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalCharacter"); } + static auto DebugTag() -> const TCHAR* { return STR("PalCharacter"); } + + PalCharacter() = default; + explicit PalCharacter(UObject* Obj) : UnrealWrapper(Obj, ClassOf(), DebugTag()) {} + + // ---- components ------------------------------------------------ + + auto GetCharacterParameterComponent() -> UObject* { return ObjectGetter(STR("GetCharacterParameterComponent")); } + auto GetActionComponent() -> UObject* { return ObjectGetter(STR("GetActionComponent")); } + auto GetPalCharacterMovementComponent() -> UObject* { return ObjectGetter(STR("GetPalCharacterMovementComponent")); } + auto GetMainMesh() -> UObject* { return ObjectGetter(STR("GetMainMesh")); } + auto GetCurrentReservedMontage() -> UObject* { return ObjectGetter(STR("GetCurrentReservedMontage")); } + + auto GetOverrideFaceMesh() -> UObject* { return ObjectGetter(STR("GetOverrideFaceMesh")); } + auto GetHandAttachMesh() -> UObject* { return ObjectGetter(STR("GetHandAttachMesh")); } + + // ---- identity -------------------------------------------------- + + auto GetIndividualParameterObject() -> UObject* + { + UObject* Comp = GetCharacterParameterComponent(); + if (!Comp) return nullptr; + LooseObject C{ Comp, STR("PalCharacterParameterComponent") }; + return C.CallForReturn(STR("GetIndividualParameter")).value_or(nullptr); + } + + auto Identify() -> PalIdentity + { + return PalIndividual::IdentifyFromParameter(GetIndividualParameterObject()); + } + + auto CharacterId() -> std::optional + { + return PalIndividual::CharacterId(GetIndividualParameterObject()); + } + + auto BlueprintClassName() const -> std::optional + { + if (!m_obj) return std::nullopt; + UClass* Cls = m_obj->GetClassPrivate(); + if (!Cls) return std::nullopt; + return Cls->GetName(); + } + + // ---- state queries --------------------------------------------- + + auto IsInitialized() -> std::optional { return CallForReturn(STR("IsInitialized")); } + auto IsPart() -> std::optional { return CallForReturn(STR("IsPart")); } + auto IsCooping() -> std::optional { return CallForReturn(STR("IsCooping")); } + auto IsPreCooping() -> std::optional { return CallForReturn(STR("IsPreCooping")); } + auto IsLocalHiddenForCutscene() -> std::optional { return CallForReturn(STR("IsLocalHiddenForCutscene")); } + auto IsUseCustomAutoAimTarget() -> std::optional { return CallForReturn(STR("IsUseCustomAutoAimTarget")); } + auto IsAllActiveSkillCooldownFinished() -> std::optional { return CallForReturn(STR("IsAllActiveSkillCooldownFinished")); } + auto GetBattleMode() -> std::optional { return CallForReturn(STR("GetBattleMode")); } + auto GetTalkMode() -> std::optional { return CallForReturn(STR("GetTalkMode")); } + auto GetActiveActorFlag() -> std::optional { return CallForReturn(STR("GetActiveActorFlag")); } + + auto IsActiveSkillCooldownFinished(EPalWazaID_ WazaID) -> std::optional + { + struct P { EPalWazaID_ WazaID; bool ReturnValue; } p{ WazaID, false }; + return CallForReturn(STR("IsActiveSkillCooldownFinished"), p, &P::ReturnValue); + } + + // ---- local-only mutators --------------------------------------- + + auto ForceResetJumpState() -> bool { return CallVoid(STR("ForceResetJumpState")); } + auto RequestJump() -> bool { return CallVoid(STR("RequestJump")); } + auto ResetTickInterval() -> bool { return CallVoid(STR("ResetTickInterval")); } + auto LocalInitialized() -> bool { return CallVoid(STR("LocalInitialized")); } + auto RefreshDungeonLightingChannels() -> bool { return CallVoid(STR("RefreshDungeonLightingChannels")); } + auto RequestExecuteTickNextFrameForAction() -> bool { return CallVoid(STR("RequestExecuteTickNextFrameForAction")); } + auto BroadcastOnCompleteInitializeParameter() -> bool { return CallVoid(STR("BroadcastOnCompleteInitializeParameter")); } + + auto UpdateGroundRayCast(bool bImmediateApply) -> bool + { + struct P { bool bImmediateApply; } p{ bImmediateApply }; + return Call(STR("UpdateGroundRayCast"), p); + } + auto SetVisibleCharacterMesh(bool Active) -> bool + { + struct P { bool Active; } p{ Active }; + return Call(STR("SetVisibleCharacterMesh"), p); + } + auto SetVisibleHandAttachMesh(bool Active) -> bool + { + struct P { bool Active; } p{ Active }; + return Call(STR("SetVisibleHandAttachMesh"), p); + } + auto SetLocalHiddenForCutscene(bool bHide) -> bool + { + struct P { bool bHide; } p{ bHide }; + return Call(STR("SetLocalHiddenForCutscene"), p); + } + auto SetActiveCollisionMovement(bool Active) -> bool + { + struct P { bool Active; } p{ Active }; + return Call(STR("SetActiveCollisionMovement"), p); + } + auto ChangeBattleModeFlag(bool IsBattle) -> bool + { + struct P { bool IsBattle; } p{ IsBattle }; + return Call(STR("ChangeBattleModeFlag"), p); + } + auto SetDisableChangeIntervalByImportance(FName FlagName, bool bIsDisable) -> bool + { + struct P { FName flagName; bool isDisable; } p{ FlagName, bIsDisable }; + return Call(STR("SetDisableChangeIntervalByImportance"), p); + } + + private: + auto ObjectGetter(const TCHAR* FunctionName) -> UObject* + { + struct P { UObject* ReturnValue; } p{ nullptr }; + auto Result = CallForReturn(FunctionName, p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + }; +} diff --git a/Api/Pal/PalDatabaseCharacterParameter.hpp b/Api/Pal/PalDatabaseCharacterParameter.hpp new file mode 100644 index 0000000..3225c22 --- /dev/null +++ b/Api/Pal/PalDatabaseCharacterParameter.hpp @@ -0,0 +1,369 @@ +#pragma once + +#include + +#include +#include + +#include +#include +#include "PalIndividual.hpp" +#include "PalTypes.hpp" + +namespace PalUIExtension::Api::Pal +{ + + class PalDatabaseCharacterParameter : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalDatabaseCharacterParameter"); } + static auto DebugTag() -> const TCHAR* { return STR("PalDatabase"); } + + PalDatabaseCharacterParameter() = default; + explicit PalDatabaseCharacterParameter(UObject* Obj) : UnrealWrapper(Obj, ClassOf(), DebugTag()) {} + + + static auto StripVariantPrefix(FName CharacterId) -> std::optional + { + const StringType Id = CharacterId.ToString(); + + static const StringType Prefixes[] = { + STR("BOSS_"), STR("PREDATOR_"), STR("RAID_"), STR("GYM_"), + }; + + for (const StringType& Prefix : Prefixes) + { + if (Id.size() <= Prefix.size()) continue; + if (!StartsWithNoCase(Id, Prefix)) continue; + + return FName(StringViewType(Id).substr(Prefix.size()), FNAME_Add); + } + return std::nullopt; + } + + static auto ResolveBaseRowName(FName CharacterId) -> FName + { + return StripVariantPrefix(CharacterId).value_or(CharacterId); + } + + // ---- row lookups by species FName ----------------------------- + + auto GetZukanIndex(FName RowName) -> std::optional + { + struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 }; + return CallForReturn(STR("GetZukanIndex"), p, &P::ReturnValue); + } + + auto GetZukanIndexResolved(FName CharacterId) -> std::optional + { + if (auto Direct = GetZukanIndex(CharacterId); Direct.has_value() && *Direct > 0) + { + return Direct; + } + + auto Base = StripVariantPrefix(CharacterId); + if (!Base.has_value()) return std::nullopt; + + auto Fallback = GetZukanIndex(*Base); + if (!Fallback.has_value() || *Fallback <= 0) return std::nullopt; + + if constexpr (Static::kVerboseLogging) + { + Output::send( + STR("[PalDatabase] ZukanIndex: '{}' had none, resolved via base '{}' -> {}\n"), + CharacterId.ToString(), Base->ToString(), *Fallback); + } + + return Fallback; + } + + + auto GetLocalizedBaseCharacterName(FName CharacterId) -> std::optional + { + return GetLocalizedCharacterName(ResolveBaseRowName(CharacterId)); + } + + auto GetTribe(FName RowName) -> std::optional + { + struct P { FName RowName; EPalTribeID_ ReturnValue; } p{ RowName, 0 }; + return CallForReturn(STR("GetTribe"), p, &P::ReturnValue); + } + auto GetSize(FName RowName) -> std::optional + { + struct P { FName RowName; EPalSizeType_ ReturnValue; } p{ RowName, EPalSizeType_::None }; + return CallForReturn(STR("GetSize"), p, &P::ReturnValue); + } + auto GetGenusCategory(FName RowName) -> std::optional + { + struct P { FName RowName; EPalGenusCategoryType_ ReturnValue; } p{ RowName, EPalGenusCategoryType_::None }; + return CallForReturn(STR("GetGenusCategory"), p, &P::ReturnValue); + } + auto GetOrganizationType(FName RowName) -> std::optional + { + struct P { FName RowName; EPalOrganizationType_ ReturnValue; } p{ RowName, EPalOrganizationType_::None }; + return CallForReturn(STR("GetOrganizationType"), p, &P::ReturnValue); + } + auto GetWeaponType(FName RowName) -> std::optional + { + struct P { FName RowName; EPalWeaponType_ ReturnValue; } p{ RowName, EPalWeaponType_::None }; + return CallForReturn(STR("GetWeaponType"), p, &P::ReturnValue); + } + auto GetBattleBGM(FName RowName) -> std::optional + { + struct P { FName RowName; EPalBattleBGMType_ ReturnValue; } p{ RowName, EPalBattleBGMType_::None }; + return CallForReturn(STR("GetBattleBGM"), p, &P::ReturnValue); + } + auto GetPalAwakeningItemElement(FName ItemId) -> std::optional + { + struct P { FName ItemId; EPalElementType_ ReturnValue; } p{ ItemId, EPalElementType_::None }; + return CallForReturn(STR("GetPalAwakeningItemElement"), p, &P::ReturnValue); + } + + struct ElementTypePair { EPalElementType_ Primary{}; EPalElementType_ Secondary{}; }; + // void return, two out-params -- not CallForReturn's shape. + auto GetElementType(FName RowName) -> std::optional + { + struct P { FName RowName; EPalElementType_ Element1; EPalElementType_ Element2; } + p{ RowName, EPalElementType_::None, EPalElementType_::None }; + if (!Call(STR("GetElementType"), p)) return std::nullopt; + return ElementTypePair{ p.Element1, p.Element2 }; + } + + // void return, one out-param. + auto GetBestWorkSuitability(FName RowName) -> std::optional + { + struct P { FName RowName; EPalWorkSuitability_ BestWorkSuitability; } + p{ RowName, EPalWorkSuitability_::None }; + if (!Call(STR("GetBestWorkSuitability"), p)) return std::nullopt; + return p.BestWorkSuitability; + } + + auto GetWeaponEquip(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetWeaponEquip"), p, &P::ReturnValue); + } + auto GetUseBossHPGauge(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetUseBossHPGauge"), p, &P::ReturnValue); + } + auto GetNocturnal(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetNocturnal"), p, &P::ReturnValue); + } + auto GetIsTowerBoss(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetIsTowerBoss"), p, &P::ReturnValue); + } + auto GetIsRaidBoss(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetIsRaidBoss"), p, &P::ReturnValue); + } + auto GetIsPredatorBoss(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetIsPredatorBoss"), p, &P::ReturnValue); + } + auto GetIsPal(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetIsPal"), p, &P::ReturnValue); + } + auto GetIsLegend(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetIsLegend"), p, &P::ReturnValue); + } + auto GetIsBoss(FName RowName) -> std::optional + { + struct P { FName RowName; bool ReturnValue; } p{ RowName, false }; + return CallForReturn(STR("GetIsBoss"), p, &P::ReturnValue); + } + auto IsArenaUnusableItem(FName ItemId) -> std::optional + { + struct P { FName ItemId; bool ReturnValue; } p{ ItemId, false }; + return CallForReturn(STR("IsArenaUnusableItem"), p, &P::ReturnValue); + } + + auto GetViewingDistance_cm(FName RowName) -> std::optional + { + struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 }; + return CallForReturn(STR("GetViewingDistance_cm"), p, &P::ReturnValue); + } + auto GetViewingAngle_Degree(FName RowName) -> std::optional + { + struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 }; + return CallForReturn(STR("GetViewingAngle_Degree"), p, &P::ReturnValue); + } + auto GetRarity(FName RowName) -> std::optional + { + struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 }; + return CallForReturn(STR("GetRarity"), p, &P::ReturnValue); + } + auto GetFoodAmount(FName RowName) -> std::optional + { + struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 }; + return CallForReturn(STR("GetFoodAmount"), p, &P::ReturnValue); + } + + auto GetPriceRate(FName RowName) -> std::optional + { + struct P { FName RowName; float ReturnValue; } p{ RowName, 0.f }; + return CallForReturn(STR("GetPriceRate"), p, &P::ReturnValue); + } + auto GetHearingRate(FName RowName) -> std::optional + { + struct P { FName RowName; float ReturnValue; } p{ RowName, 0.f }; + return CallForReturn(STR("GetHearingRate"), p, &P::ReturnValue); + } + auto GetExpRatio(FName RowName) -> std::optional + { + struct P { FName RowName; float ReturnValue; } p{ RowName, 0.f }; + return CallForReturn(STR("GetExpRatio"), p, &P::ReturnValue); + } + auto GetCaptureRateCorrect(FName RowName) -> std::optional + { + struct P { FName RowName; float ReturnValue; } p{ RowName, 0.f }; + return CallForReturn(STR("GetCaptureRateCorrect"), p, &P::ReturnValue); + } + + auto GetBPClassName(FName RowName) -> std::optional + { + struct P { FName RowName; FName ReturnValue; } p{ RowName, {} }; + return CallForReturn(STR("GetBPClassName"), p, &P::ReturnValue); + } + auto GetFirstDefeatRewardItemID(FName CharacterID) -> std::optional + { + struct P { FName CharacterID; FName ReturnValue; } p{ CharacterID, {} }; + return CallForReturn(STR("GetFirstDefeatRewardItemID"), p, &P::ReturnValue); + } + + // void return, FName out-param -- not a FString, no ownership question, just an index pair like every other FName here. + auto GetPrefixNameMsgID(FName CharacterID) -> std::optional + { + struct P { FName CharacterID; FName OutMsgID; } p{ CharacterID, {} }; + if (!Call(STR("GetPrefixNameMsgID"), p)) return std::nullopt; + return p.OutMsgID; + } + + auto GetLocalizedCharacterName(FName CharacterID) -> std::optional + { + struct P { FName CharacterID; FText OutText; } p{ CharacterID, {} }; + if (!Call(STR("GetLocalizedCharacterName"), p)) return std::nullopt; + return p.OutText; + } + auto GetLocalizedUniqueNPCName(FName UniqueNPCID) -> std::optional + { + struct P { FName UniqueNPCID; FText OutText; } p{ UniqueNPCID, {} }; + if (!Call(STR("GetLocalizedUniqueNPCName"), p)) return std::nullopt; + return p.OutText; + } + + // ---- friendship ------------------------------------------------ + + auto GetMinFriendshipRank() -> std::optional { return CallForReturn(STR("GetMinFriendshipRank")); } + auto GetMaxFriendshipRank() -> std::optional { return CallForReturn(STR("GetMaxFriendshipRank")); } + auto GetFriendshipRank(int32 FriendshipPoint) -> std::optional + { + struct P { int32 FriendshipPoint; int32 ReturnValue; } p{ FriendshipPoint, 0 }; + return CallForReturn(STR("GetFriendshipRank"), p, &P::ReturnValue); + } + auto CalcFriendshipProgress(int32 FriendshipPoint) -> std::optional + { + struct P { int32 FriendshipPoint; float ReturnValue; } p{ FriendshipPoint, 0.f }; + return CallForReturn(STR("CalcFriendshipProgress"), p, &P::ReturnValue); + } + // bool return + int32& out-param. + auto GetFriendshipRequiredPointByRank(int32 FriendshipRank) -> std::optional + { + struct P { int32 FriendshipRank; int32 OutRequiredPoint; bool ReturnValue; } + p{ FriendshipRank, 0, false }; + if (!Call(STR("GetFriendshipRequiredPointByRank"), p) || !p.ReturnValue) return std::nullopt; + return p.OutRequiredPoint; + } + + // ---- derived from a live UPalIndividualCharacterParameter* --------- + + auto GetHP(UObject* IndividualParameter) -> std::optional + { + struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 }; + return CallForReturn(STR("GetHP"), p, &P::ReturnValue); + } + auto GetMeleeAttack(UObject* IndividualParameter) -> std::optional + { + struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 }; + return CallForReturn(STR("GetMeleeAttack"), p, &P::ReturnValue); + } + auto GetShotAttack(UObject* IndividualParameter) -> std::optional + { + struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 }; + return CallForReturn(STR("GetShotAttack"), p, &P::ReturnValue); + } + auto GetDefense(UObject* IndividualParameter) -> std::optional + { + struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 }; + return CallForReturn(STR("GetDefense"), p, &P::ReturnValue); + } + auto GetCraftSpeed(UObject* IndividualParameter) -> std::optional + { + struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 }; + return CallForReturn(STR("GetCraftSpeed"), p, &P::ReturnValue); + } + auto GetSupport(UObject* IndividualParameter) -> std::optional + { + struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 }; + return CallForReturn(STR("GetSupport"), p, &P::ReturnValue); + } + auto HigherLevelOtomoFromTrainer(UObject* IndividualCharacterParameter) -> std::optional + { + struct P { UObject* IndividualCharacterParameter; int32 ReturnValue; } p{ IndividualCharacterParameter, 0 }; + return CallForReturn(STR("HigherLevelOtomoFromTrainer"), p, &P::ReturnValue); + } + + auto UpdateApplyDatabaseToIndividualParameter(UObject* IndividualParameter) -> bool + { + struct P { UObject* IndividualParameter; } p{ IndividualParameter }; + return Call(STR("UpdateApplyDatabaseToIndividualParameter"), p); + } + + // Raw UObject* return -- UPalPettingPresset isn't modeled anywhere + auto GetPettingPreset() -> UObject* + { + struct P { UObject* ReturnValue; } p{ nullptr }; + auto Result = CallForReturn(STR("GetPettingPreset"), p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + + // WorldContextObject is a plain UObject* at the native signature + // level -- the meta=(WorldContext=...) annotation only affects + // the Blueprint editor node, not the ProcessEvent frame. Pass + // any live UObject with a valid world (a player controller, an + // actor, etc.). + auto CalcCorrectedLevel(int32 TrainerLevel, int32 TargetCharacterLevel, UObject* WorldContextObject) -> std::optional + { + struct P { int32 TrainerLevel; int32 TargetCharacterLevel; UObject* WorldContextObject; int32 ReturnValue; } + p{ TrainerLevel, TargetCharacterLevel, WorldContextObject, 0 }; + return CallForReturn(STR("CalcCorrectedLevel"), p, &P::ReturnValue); + } + + private: + + static auto StartsWithNoCase(const StringType& Str, const StringType& Prefix) -> bool + { + if (Str.size() < Prefix.size()) return false; + for (size_t i = 0; i < Prefix.size(); ++i) + { + CharType A = Str[i]; + CharType B = Prefix[i]; + if (A >= STR('a') && A <= STR('z')) A = static_cast(A - STR('a') + STR('A')); + if (B >= STR('a') && B <= STR('z')) B = static_cast(B - STR('a') + STR('A')); + if (A != B) return false; + } + return true; + } + }; +} diff --git a/Api/Pal/PalGameMode.hpp b/Api/Pal/PalGameMode.hpp new file mode 100644 index 0000000..24950a9 --- /dev/null +++ b/Api/Pal/PalGameMode.hpp @@ -0,0 +1,301 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace PalUIExtension::Api::Pal +{ + using namespace RC; + using namespace RC::Unreal; + + // ================================================================== + // EPalGameModeType -- confirmed from the dump, UENUM(BlueprintType) + // over uint8, two values. Mirrored here for the same reason + // ESlateVisibility_ is: a params struct needs a concrete type of the + // right width, and pulling in the game's generated header is not an + // option. + // ================================================================== + enum class EPalGameModeType_ : uint8 + { + Title = 0, + InGame = 1, + }; + + // ================================================================== + // APalGameModeBase. + // + // Two things exist on this class and one of them matters: + // GameModeType, an FEnumProperty over uint8. Read it to tell the + // title screen apart from a live world without inspecting the map + // name. + // + // ---- THE THING TO INTERNALISE ABOUT GAME MODES ------------------- + // + // A GameMode exists ONLY ON THE AUTHORITY. On a listen host and in + // single-player that is the local process, so this class is + // reachable. On a client connected to a dedicated server, no + // APalGameMode is ever instantiated locally and nothing in this file + // will ever resolve to anything. + // + // That is not a limitation of this wrapper, it is what a game mode + // IS, and it decides what you may and may not build on top of it. A + // client-side UI mod that depends on a game-mode hook for + // correctness is broken for every dedicated-server player. Use these + // as a supplement to an engine-level signal, never as the only one. + // ================================================================== + class PalGameModeBase : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalGameModeBase"); } + static auto DebugTag() -> const TCHAR* { return STR("PalGameModeBase"); } + + PalGameModeBase() = default; + explicit PalGameModeBase(UObject* Obj) + : UnrealWrapper(Obj, ClassOf(), DebugTag()) {} + + // FEnumProperty over uint8 -- GetSize() reports 1, and + // EPalGameModeType_ is 1 byte, so the size check in PropertyPtr is + // meaningful here rather than vacuous. + auto GameModeType() const -> std::optional + { + return PropertyValue(STR("GameModeType")); + } + + auto IsInGame() const -> bool + { + const auto T = GameModeType(); + return T.has_value() && *T == EPalGameModeType_::InGame; + } + + auto IsTitle() const -> bool + { + const auto T = GameModeType(); + return T.has_value() && *T == EPalGameModeType_::Title; + } + + protected: + PalGameModeBase(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UnrealWrapper(Obj, Expected, Tag) {} + }; + + // ================================================================== + // APalGameMode -- the in-world game mode. + // + // ---- WHAT IS WORTH HOOKING HERE, AND WHAT IS NOT ----------------- + // + // RestartGame is the one this tree cares about: it is the game + // deciding the current world is finished, fired BEFORE the travel + // that destroys it. That ordering is the whole value -- a cache of + // widget pointers wants to be dropped while those pointers are still + // merely useless, not after they are dangling. + // + // READ THE CAVEAT BLOCK ON RestartGamePath() BEFORE RELYING ON IT. + // It has two failure modes that are invisible in testing if you only + // test single-player. + // + // The session/auth callbacks (OnUpdateSession, OnCompleteAuth, + // OnCompleteCreateSession, OnEOSLoginDedicatedServerComplete) are + // dedicated-server plumbing. They are wrapped as PATHS ONLY, with + // frames, because they are occasionally useful for diagnosing "why + // did my server mod not initialise" -- but nothing in a client-side + // UI mod should be reading them, and none of them fire in + // single-player at all. + // + // ---- WHY THERE IS NO Current() ----------------------------------- + // + // The obvious convenience -- a static that hands back the live game + // mode -- is deliberately absent. Every way to get one without a + // UWorld in hand goes through UObjectGlobals::FindObject or + // FindObjects, and for a Blueprint-reachable native class that is a + // linear walk of the entire GUObjectArray (see the note in + // PalBoxNightWorkIndicator on what that cost actually is). A helper + // that looks like a property read and is really a million-object scan + // is the kind of convenience that ends up on a per-frame path. + // + // Get the instance from a hook that already has it instead: every + // callback below receives it as Ctx.Context, and + // Hook::RegisterInitGameStatePostCallback hands over an + // AGameModeBase* directly at world start. + // ================================================================== + class PalGameMode : public PalGameModeBase + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalGameMode"); } + static auto DebugTag() -> const TCHAR* { return STR("PalGameMode"); } + + PalGameMode() = default; + explicit PalGameMode(UObject* Obj) + : PalGameModeBase(Obj, ClassOf(), DebugTag()) {} + + // ---- properties ------------------------------------------------ + + // APlayerStart*. No wrapper for it in this tree, so the raw + // pointer is the honest return type. + auto CachePlayerStart() const -> UObject* + { + return TryObjectProperty(STR("CachePlayerStart")); + } + + // ============================================================== + // HOOK TARGETS + // ============================================================== + + // ============================================================== + // RestartGame. READ THIS BEFORE BUILDING ON IT. + // + // Two failure modes, both silent, both invisible if you only + // test single-player: + // + // 1. AUTHORITY ONLY. No APalGameMode is instantiated on a client + // connected to a dedicated server, so this hook never installs + // and never fires there. For a client-side UI mod that is the + // majority multiplayer case. + // + // 2. REFLECTION ONLY. This is a native UFUNCTION(BlueprintCallable). + // UE4SS hooks UFunction execution, which catches invocations + // that go through ProcessEvent -- a Blueprint node, a console + // exec, a reflected call. It does NOT catch native C++ code + // calling APalGameMode::RestartGame() directly, because that + // path never touches the reflection system. Whether Palworld + // restarts via Blueprint or via C++ is not answerable from a + // header dump; it has to be observed. + // + // The practical consequence: treat a RestartGame callback as an + // OPPORTUNISTIC EARLY WARNING, not as the teardown signal. The + // load-bearing one is Hook::RegisterLoadMapPreCallback, which is + // engine-level, fires on every map transition regardless of what + // triggered it, and works identically on clients and hosts. + // + // Wiring both is correct and costs nothing: teardown must be + // idempotent anyway, since a restart that goes through both paths + // will call it twice. + // ============================================================== + static auto RestartGamePath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:RestartGame"); + } + + static auto InitDedicatedServerPath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:InitDedicatedServer"); + } + + static auto OnServerLobbyUpdatePath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:OnServerLobbyUpdate"); + } + + static auto OnUpdateSessionPath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:OnUpdateSession"); + } + + static auto OnCompleteAuthPath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:OnCompleteAuth"); + } + + static auto OnCompleteCreateSessionPath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:OnCompleteCreateSession"); + } + + static auto OnEOSLoginDedicatedServerCompletePath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:OnEOSLoginDedicatedServerComplete"); + } + + static auto CreateSessionPath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:CreateSession"); + } + + static auto FindPlayerStartWithTagPath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameMode:FindPlayerStartWithTag"); + } + + // ---- frames --------------------------------------------------- + // + // Leading parameters only, matching the convention in + // PalCommonCharacterSlot.hpp: hook frames are NOT size-checked, so + // reading past the declared parameters reads engine locals. On a + // PRE hook those are uninitialised. + // + // The FString members are the part to be careful with. A + // `const FString&` UFUNCTION parameter still occupies a full + // FString in the frame (TArray: 8-byte data pointer + + // int32 Num + int32 Max = 16 bytes), so the layout below is what + // the engine writes. It is NOT owned by you -- read it, do not + // move from it, do not free it. + // + // RestartGame takes nothing, which is why there is no frame for + // it and why its callback should not call Ctx.GetParams at all. + + struct HttpResponseFrame + { + FString ResponseBody; // 0x00 + bool bResponseOK; // 0x10 + int32 ResponseCode; // 0x14 (after 3 bytes of padding) + }; + + struct EOSLoginCompleteFrame + { + UObject* UserInfo; // 0x00 const UPocketpairUserInfo* + bool bSuccess; // 0x08 + FString ErrorStr; // 0x10 (after 7 bytes of padding) + }; + + struct CreateSessionFrame { FString Address; }; + struct FindPlayerStartWithTagFrame { FName Tag; UObject* ReturnValue; }; + + protected: + PalGameMode(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : PalGameModeBase(Obj, Expected, Tag) {} + }; + + // ================================================================== + // APalGameModeLogin -- the title-screen game mode. + // + // Included because it is the OTHER half of the world-lifecycle + // picture and it is easy to forget it exists: going from a save back + // to the title is a map transition into THIS game mode, not a + // RestartGame on the previous one. If teardown only fires on + // APalGameMode, quitting to title leaves every cached pointer alive + // and dangling. + // + // GoToTitle is BlueprintImplementableEvent -- no native body. It + // dispatches through the object's class chain like anything else, so + // it is hookable, but if no Blueprint subclass overrides it there is + // nothing to hook and the registry will defer forever. + // ================================================================== + class PalGameModeLogin : public PalGameModeBase + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalGameModeLogin"); } + static auto DebugTag() -> const TCHAR* { return STR("PalGameModeLogin"); } + + PalGameModeLogin() = default; + explicit PalGameModeLogin(UObject* Obj) + : PalGameModeBase(Obj, ClassOf(), DebugTag()) {} + + static auto OnCompletePath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameModeLogin:OnComplete"); + } + static auto GoToTitlePath() -> const TCHAR* + { + return STR("/Script/Pal.PalGameModeLogin:GoToTitle"); + } + + struct OnCompleteFrame { bool bCanPlayMulti; }; + + protected: + PalGameModeLogin(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : PalGameModeBase(Obj, Expected, Tag) {} + }; +} diff --git a/Api/Pal/PalIndividual.hpp b/Api/Pal/PalIndividual.hpp new file mode 100644 index 0000000..9ffd86b --- /dev/null +++ b/Api/Pal/PalIndividual.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include + +#include "PalTypes.hpp" + +namespace PalUIExtension::Api::Pal +{ + + enum class EPalGenderType_ : uint8 { None = 0, Male = 1, Female = 2 }; + + enum class EPalElementType_ : uint8 + { + None = 0, Normal = 1, Fire = 2, Water = 3, Leaf = 4, + Electricity = 5, Ice = 6, Earth = 7, Dark = 8, Dragon = 9, MAX = 10, + }; + + enum class EPalWorkSuitability_ : uint8 + { + None = 0, EmitFlame = 1, Watering = 2, Seeding = 3, + GenerateElectricity = 4, Handcraft = 5, Collection = 6, Deforest = 7, + Mining = 8, OilExtraction = 9, ProductMedicine = 10, Cool = 11, + Transport = 12, MonsterFarm = 13, Anyone = 14, MAX = 15, + }; + + class LooseObject : public UnrealWrapper + { + public: + LooseObject() = default; + explicit LooseObject(UObject* Obj, const TCHAR* Tag = STR("Loose")) + : UnrealWrapper(Obj, UncheckedTag{}, Tag) { + } + + using UnrealWrapper::Call; + using UnrealWrapper::CallVoid; + using UnrealWrapper::CallForReturn; + using UnrealWrapper::PropertyPtr; + using UnrealWrapper::PropertyValue; + }; + + // this struct is now redundant. watch it never get removed. + + struct FPalInstanceID { + FGuid_ PlayerUID; + FGuid_ InstanceID; + FString DebugName; + }; + + struct PalIdentity + { + UObject* Handle{}; + UObject* IndividualParameter{}; + + std::optional CharacterId; // the species. "SheepBall", etc. + std::optional Level; + std::optional Rank; + std::optional Gender; + std::optional bRare; + + explicit operator bool() const { return CharacterId.has_value(); } + }; + + class PalIndividual + { + public: + static auto ParameterFromHandle(UObject* Handle) -> UObject* + { + if (!Handle) return nullptr; + LooseObject H{ Handle, STR("PalIndividualCharacter") }; + return H.CallForReturn(STR("TryGetIndividualParameter")).value_or(nullptr); + } + + static auto CharacterId(UObject* Parameter) -> std::optional + { + if (!Parameter) return std::nullopt; + LooseObject P{ Parameter, STR("PalIndividualCharacterParameter") }; + return P.CallForReturn(STR("GetCharacterID")); + } + + static auto CharacterIdFromHandle(UObject* Handle) -> std::optional + { + return CharacterId(ParameterFromHandle(Handle)); + } + + static auto IdentifyFromParameter(UObject* Parameter) -> PalIdentity + { + PalIdentity Out{}; + Out.IndividualParameter = Parameter; + if (!Parameter) return Out; + + LooseObject P{ Parameter, STR("PalIndividualCharacterParameter") }; + Out.CharacterId = P.CallForReturn(STR("GetCharacterID")); + Out.Level = P.CallForReturn(STR("GetLevel")); + Out.Rank = P.CallForReturn(STR("GetRank")); + Out.Gender = P.CallForReturn(STR("GetGenderType")); + Out.bRare = P.CallForReturn(STR("IsRarePal")); + return Out; + } + + static auto Identify(UObject* Handle) -> PalIdentity + { + PalIdentity Out = IdentifyFromParameter(ParameterFromHandle(Handle)); + Out.Handle = Handle; + return Out; + } + + static auto GetInstanceIds(UObject* Handle) -> std::optional + { + if (!Handle) return std::nullopt; + // FPalInstanceID + LooseObject H{ Handle, STR("PalIndividualCharacterHandle") }; + auto _Out = H.CallForReturn(STR("GetIndividualID")); + + if (!_Out.has_value()) return std::nullopt; + return FPalInstanceID{ _Out.value().PlayerUID, _Out.value().InstanceID, _Out.value().DebugName }; + } + + }; + +} \ No newline at end of file diff --git a/Api/Pal/PalIndividualCharacterHandle.hpp b/Api/Pal/PalIndividualCharacterHandle.hpp new file mode 100644 index 0000000..7b67c02 --- /dev/null +++ b/Api/Pal/PalIndividualCharacterHandle.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include "PalIndividual.hpp" +#include "PalCharacter.hpp" + +namespace PalUIExtension::Api::Pal +{ + class PalIndividualCharacterHandle : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalIndividualCharacterHandle"); } + static auto DebugTag() -> const TCHAR* { return STR("PalIndividualCharacterHandle"); } + + PalIndividualCharacterHandle() = default; + explicit PalIndividualCharacterHandle(UObject* Obj) : UnrealWrapper(Obj, ClassOf(), DebugTag()) {} + + auto TryGetIndividualParameterObject() -> UObject* + { + struct P { UObject* ReturnValue; } p{ nullptr }; + auto Result = CallForReturn(STR("TryGetIndividualParameter"), p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + + auto TryGetIndividualActorObject() -> UObject* + { + struct P { UObject* ReturnValue; } p{ nullptr }; + auto Result = CallForReturn(STR("TryGetIndividualActor"), p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + + auto TryGetIndividualActor() -> PalCharacter + { + UObject* A = TryGetIndividualActorObject(); + return A ? PalCharacter{ A } : PalCharacter{}; + } + + auto TryGetPhantomActor(int32 PhantomId) -> UObject* + { + struct P { int32 PhantomId; UObject* ReturnValue; } p{ PhantomId, nullptr }; + auto Result = CallForReturn(STR("TryGetPhantomActor"), p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + + auto GetPhantomIDByActor(UObject* Character) -> std::optional + { + struct P { UObject* Character; int32 ReturnValue; } p{ Character, 0 }; + return CallForReturn(STR("GetPhantomIDByActor"), p, &P::ReturnValue); + } + + auto GetInstanceIds() -> std::optional + { + return PalIndividual::GetInstanceIds(Get()); + } + }; +} diff --git a/Api/Pal/PalIndividualCharacterParameter.hpp b/Api/Pal/PalIndividualCharacterParameter.hpp new file mode 100644 index 0000000..09f050f --- /dev/null +++ b/Api/Pal/PalIndividualCharacterParameter.hpp @@ -0,0 +1,480 @@ +#pragma once + +#include + +#include +#include + +#include +#include "PalIndividual.hpp" +#include "PalTypes.hpp" + +namespace PalUIExtension::Api::Pal +{ + + class PalIndividualCharacterParameter : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalIndividualCharacterParameter"); } + static auto DebugTag() -> const TCHAR* { return STR("PalIndividualCharacterParameter"); } + + PalIndividualCharacterParameter() = default; + explicit PalIndividualCharacterParameter(UObject* Obj) : UnrealWrapper(Obj, ClassOf(), DebugTag()) {} + + // ---- identity --------------------------------------------------- + + auto GetCharacterID() -> std::optional { return CallForReturn(STR("GetCharacterID")); } + auto GetUniqueNPCID() -> std::optional { return CallForReturn(STR("GetUniqueNPCID")); } + auto GetSkinName() -> std::optional { return CallForReturn(STR("GetSkinName")); } + + auto GetGenderType() -> std::optional + { + return CallForReturn(STR("GetGenderType")); + } + auto GetTribeID() -> std::optional + { + return CallForReturn(STR("GetTribeID")); + } + auto IsRarePal() -> std::optional { return CallForReturn(STR("IsRarePal")); } + auto IsFavoritePal() -> std::optional { return CallForReturn(STR("IsFavoritePal")); } + auto GetFavoriteIndex() -> std::optional { return CallForReturn(STR("GetFavoriteIndex")); } + auto IsSkinApplied() -> std::optional { return CallForReturn(STR("IsSkinApplied")); } + auto GetSkinAppliedCharacterId() -> std::optional + { + return CallForReturn(STR("GetSkinAppliedCharacterId")); + } + + auto GetPalId() -> std::optional + { + struct P { alignas(8) uint8 Frame[48]; } p{}; // FPalInstanceID + if (!Call(STR("GetPalId"), p)) return std::nullopt; + FPalInstanceID Out{}; + std::memcpy(&Out, p.Frame, sizeof(Out)); + return Out; + } + + // Nickname getters + + auto GetNickname() -> std::optional + { + struct P { FString OutName; } p{}; + if (!Call(STR("GetNickname"), p)) return std::nullopt; + return p.OutName; + } + auto GetNickNameWithOnlineID() -> std::optional + { + struct P { FString OutName; } p{}; + if (!Call(STR("GetNickNameWithOnlineID"), p)) return std::nullopt; + return p.OutName; + } + auto GetNickNameByCheckBlockedUser() -> std::optional + { + struct P { FString OutName; } p{}; + if (!Call(STR("GetNickNameByCheckBlockedUser"), p)) return std::nullopt; + return p.OutName; + } + + // ---- level / experience ------------------------------------------ + + auto GetLevel() -> std::optional { return CallForReturn(STR("GetLevel")); } + auto IsLevelMax() -> std::optional { return CallForReturn(STR("IsLevelMax")); } + auto GetExp() -> std::optional { return CallForReturn(STR("GetExp")); } + auto GetRank() -> std::optional { return CallForReturn(STR("GetRank")); } + auto GetRankUpExp() -> std::optional { return CallForReturn(STR("GetRankUpExp")); } + auto GetPalSoulRank() -> std::optional { return CallForReturn(STR("GetPalSoulRank")); } + auto IsOverrideLevel() -> std::optional { return CallForReturn(STR("IsOverrideLevel")); } + auto GetOverrideLevel() -> std::optional { return CallForReturn(STR("GetOverrideLevel")); } + + auto SetOverrideLevel(int32 OverrideLevel) -> bool + { + struct P { int32 OverrideLevel; } p{ OverrideLevel }; + return Call(STR("SetOverrideLevel"), p); + } + + // ---- HP / shield / death ----------------------------------------- + + auto GetHP() -> std::optional { return CallForReturn(STR("GetHP")); } + auto GetMaxHP_withBuff() -> std::optional { return CallForReturn(STR("GetMaxHP_withBuff")); } + auto GetMaxHP() -> std::optional { return CallForReturn(STR("GetMaxHP")); } + auto GetHPRank() -> std::optional { return CallForReturn(STR("GetHPRank")); } + + auto GetShieldHP() -> std::optional { return CallForReturn(STR("GetShieldHP")); } + auto GetShieldMaxHP() -> std::optional { return CallForReturn(STR("GetShieldMaxHP")); } + + auto GetCurrentPhaseHPLimit() -> std::optional + { + struct P { FFixedPoint64_ OutPhaseHPLimit; bool ReturnValue; } p{ {}, false }; + auto Result = CallForReturn(STR("GetCurrentPhaseHPLimit"), p, &P::ReturnValue); + if (!Result || !*Result) return std::nullopt; + return p.OutPhaseHPLimit; + } + auto IsPhaseHPLimitValid() -> std::optional { return CallForReturn(STR("IsPhaseHPLimitValid")); } + + auto AddHP(FFixedPoint64_ PlusHP) -> bool + { + struct P { FFixedPoint64_ PlusHP; } p{ PlusHP }; + return Call(STR("AddHP"), p); + } + auto FullRecoveryHP() -> bool { return CallVoid(STR("FullRecoveryHP")); } + auto IsHPFullRecovered() -> std::optional { return CallForReturn(STR("IsHPFullRecovered")); } + + auto SetShieldHP(FFixedPoint64_ NextShieldHP) -> bool + { + struct P { FFixedPoint64_ NextSheildHP; } p{ NextShieldHP }; // engine spelling + return Call(STR("SetShieldHP"), p); + } + auto SetShieldMaxHP(FFixedPoint64_ NextShieldMaxHP) -> bool + { + struct P { FFixedPoint64_ NextSheildMaxHP; } p{ NextShieldMaxHP }; // engine spelling + return Call(STR("SetShieldMaxHP"), p); + } + auto DecreaseShieldHPByDamage(int32 Damage) -> bool + { + struct P { int32 Damage; } p{ Damage }; + return Call(STR("DecreaseShieldHPByDamage"), p); + } + + auto IsDead() -> std::optional { return CallForReturn(STR("IsDead")); } + auto GetPhysicalHealth() -> std::optional + { + return CallForReturn(STR("GetPhysicalHealth")); + } + auto SetPhysicalHealth(EPalStatusPhysicalHealthType_ PhysicalHealth) -> bool + { + struct P { EPalStatusPhysicalHealthType_ PhysicalHealth; } p{ PhysicalHealth }; + return Call(STR("SetPhysicalHealth"), p); + } + auto IsRespawnReady() -> std::optional { return CallForReturn(STR("IsRespawnReady")); } + auto GetRespawnTime() -> std::optional { return CallForReturn(STR("GetRespawnTime")); } + + // ---- combat stats ------------------------------------------------- + + auto GetMeleeAttack() -> std::optional { return CallForReturn(STR("GetMeleeAttack")); } + auto GetMeleeAttack_withBuff() -> std::optional { return CallForReturn(STR("GetMeleeAttack_withBuff")); } + auto GetShotAttack() -> std::optional { return CallForReturn(STR("GetShotAttack")); } + auto GetShotAttack_withBuff() -> std::optional { return CallForReturn(STR("GetShotAttack_withBuff")); } + auto GetDefense() -> std::optional { return CallForReturn(STR("GetDefense")); } + auto GetDefense_withBuff() -> std::optional { return CallForReturn(STR("GetDefense_withBuff")); } + auto GetAttackRank() -> std::optional { return CallForReturn(STR("GetAttackRank")); } + auto GetDefenceRank() -> std::optional { return CallForReturn(STR("GetDefenceRank")); } + + auto HasMasteredWaza(EPalWazaID_ WazaID) -> std::optional + { + struct P { EPalWazaID_ WazaID; bool ReturnValue; } p{ WazaID, false }; + return CallForReturn(STR("HasMasteredWaza"), p, &P::ReturnValue); + } + auto AddEquipWaza(EPalWazaID_ WazaID) -> bool + { + struct P { EPalWazaID_ WazaID; } p{ WazaID }; + return Call(STR("AddEquipWaza"), p); + } + auto RemoveEquipWaza(EPalWazaID_ WazaID) -> bool + { + struct P { EPalWazaID_ WazaID; } p{ WazaID }; + return Call(STR("RemoveEquipWaza"), p); + } + auto ClearEquipWaza() -> bool { return CallVoid(STR("ClearEquipWaza")); } + + // ---- sanity / hunger / stomach ------------------------------------ + + auto GetSanityValue() -> std::optional { return CallForReturn(STR("GetSanityValue")); } + auto GetMaxSanityValue() -> std::optional { return CallForReturn(STR("GetMaxSanityValue")); } + auto GetSanityRate() -> std::optional { return CallForReturn(STR("GetSanityRate")); } + auto GetAffectSanityValue() -> std::optional { return CallForReturn(STR("GetAffectSanityValue")); } + + auto GetHungerType() -> std::optional + { + return CallForReturn(STR("GetHungerType")); + } + auto GetFullStomach() -> std::optional { return CallForReturn(STR("GetFullStomach")); } + auto GetMaxFullStomach() -> std::optional { return CallForReturn(STR("GetMaxFullStomach")); } + auto GetFullStomachRate() -> std::optional { return CallForReturn(STR("GetFullStomachRate")); } + auto GetFullStomachDecreasingRate() -> std::optional { return CallForReturn(STR("GetFullStomachDecreasingRate")); } + auto IsFullStomachDecreaseStoppedByFood() -> std::optional + { + return CallForReturn(STR("IsFullStomachDecreaseStoppedByFood")); + } + + auto SetFullStomach(float NextValue) -> bool + { + struct P { float NextValue; } p{ NextValue }; + return Call(STR("SetFullStomach"), p); + } + auto SetDecreaseFullStomachRates(FName Name, float Rate) -> bool + { + struct P { FName Name; float Rate; } p{ Name, Rate }; + return Call(STR("SetDecreaseFullStomachRates"), p); + } + auto RemoveDecreaseFullStomachRates(FName Name) -> bool + { + struct P { FName Name; } p{ Name }; + return Call(STR("RemoveDecreaseFullStomachRates"), p); + } + + auto GetEffectFoodName() -> std::optional { return CallForReturn(STR("GetEffectFoodName")); } + auto GetEffectFoodName_FullStomachKeep() -> std::optional { return CallForReturn(STR("GetEffectFoodName_FullStomachKeep")); } + auto GetEffectFoodTimeRate() -> std::optional { return CallForReturn(STR("GetEffectFoodTimeRate")); } + auto GetEffectFoodTimeRate_FullStomachKeep() -> std::optional { return CallForReturn(STR("GetEffectFoodTimeRate_FullStomachKeep")); } + auto GetFoodStatusRate(EPalFoodStatusEffectType_ EffectType) -> std::optional + { + struct P { EPalFoodStatusEffectType_ EffectType; float ReturnValue; } p{ EffectType, 0.f }; + return CallForReturn(STR("GetFoodStatusRate"), p, &P::ReturnValue); + } + + // ---- friendship ----------------------------------------------------- + + auto GetFriendshipRank() -> std::optional { return CallForReturn(STR("GetFriendshipRank")); } + auto GetFriendshipPoint() -> std::optional { return CallForReturn(STR("GetFriendshipPoint")); } + auto AddFriendShip(int32 Value, bool bApplyPassiveSkill) -> bool + { + struct P { int32 Value; bool bApplyPassiveSkill; } p{ Value, bApplyPassiveSkill }; + return Call(STR("AddFriendShip"), p); + } + + // ---- status points -------------------------------------------------- + + auto GetUnusedStatusPoint() -> std::optional { return CallForReturn(STR("GetUnusedStatusPoint")); } + auto DecrementUnusedStatusPoint() -> bool { return CallVoid(STR("DecrementUnusedStatusPoint")); } + auto IsStatusPointAllMax() -> std::optional { return CallForReturn(STR("IsStatusPointAllMax")); } + + auto GetStatusPoint(FName StatusName) -> std::optional + { + struct P { FName StatusName; int32 ReturnValue; } p{ StatusName, 0 }; + return CallForReturn(STR("GetStatusPoint"), p, &P::ReturnValue); + } + auto GetTotalStatusPoint(FName StatusName) -> std::optional + { + struct P { FName StatusName; int32 ReturnValue; } p{ StatusName, 0 }; + return CallForReturn(STR("GetTotalStatusPoint"), p, &P::ReturnValue); + } + auto GetExStatusPoint(FName StatusName) -> std::optional + { + struct P { FName StatusName; int32 ReturnValue; } p{ StatusName, 0 }; + return CallForReturn(STR("GetExStatusPoint"), p, &P::ReturnValue); + } + auto SetStatusPoint(FName StatusName, int32 Point) -> bool + { + struct P { FName StatusName; int32 Point; } p{ StatusName, Point }; + return Call(STR("SetStatusPoint"), p); + } + auto SetExStatusPoint(FName StatusName, int32 Point) -> bool + { + struct P { FName StatusName; int32 Point; } p{ StatusName, Point }; + return Call(STR("SetExStatusPoint"), p); + } + // Two outputs (bool return + int32& out), same non-CallForReturn + // shape as CheckValidWorkSuitability in PalDetailWidget.hpp. + auto IsStatusPointAddable(FName StatusName) -> std::optional + { + struct P { FName StatusName; int32 AddablePoint; bool ReturnValue; } p{ StatusName, 0, false }; + if (!Call(STR("IsStatusPointAddable"), p) || !p.ReturnValue) return std::nullopt; + return p.AddablePoint; + } + + // ---- work suitability ------------------------------------------------- + + auto GetCurrentWorkSuitability() -> std::optional + { + return CallForReturn(STR("GetCurrentWorkSuitability")); + } + auto GetWorkSpeedRank() -> std::optional { return CallForReturn(STR("GetWorkSpeedRank")); } + auto GetCraftSpeed() -> std::optional { return CallForReturn(STR("GetCraftSpeed")); } + auto GetCraftSpeed_withBuff() -> std::optional { return CallForReturn(STR("GetCraftSpeed_withBuff")); } + auto GetCraftSpeedSickRate() -> std::optional { return CallForReturn(STR("GetCraftSpeedSickRate")); } + auto GetBaseCampCraftSpeedBuffRate() -> std::optional { return CallForReturn(STR("GetBaseCampCraftSpeedBuffRate")); } + auto GetWorkerSick() -> std::optional + { + return CallForReturn(STR("GetWorkerSick")); + } + auto GetBaseCampWorkerEventType() -> std::optional + { + return CallForReturn(STR("GetBaseCampWorkerEventType")); + } + + auto HasWorkSuitability(EPalWorkSuitability_ Suitability) -> std::optional + { + struct P { EPalWorkSuitability_ InWorkSuitability; bool ReturnValue; } p{ Suitability, false }; + return CallForReturn(STR("HasWorkSuitability"), p, &P::ReturnValue); + } + auto HasWorkSuitabilityRank(EPalWorkSuitability_ Suitability, int32 Rank) -> std::optional + { + struct P { EPalWorkSuitability_ InWorkSuitability; int32 SuitabilityRank; bool ReturnValue; } + p{ Suitability, Rank, false }; + return CallForReturn(STR("HasWorkSuitabilityRank"), p, &P::ReturnValue); + } + auto GetWorkSuitabilityRank(EPalWorkSuitability_ Suitability) -> std::optional + { + struct P { EPalWorkSuitability_ InWorkSuitability; int32 ReturnValue; } p{ Suitability, 0 }; + return CallForReturn(STR("GetWorkSuitabilityRank"), p, &P::ReturnValue); + } + auto GetWorkSuitabilityRankWithCharacterRank(EPalWorkSuitability_ Suitability) -> std::optional + { + struct P { EPalWorkSuitability_ WorkSuitability; int32 ReturnValue; } p{ Suitability, 0 }; + return CallForReturn(STR("GetWorkSuitabilityRankWithCharacterRank"), p, &P::ReturnValue); + } + auto GetRankBasedWorkSuitabilityBonus(EPalWorkSuitability_ Suitability) -> std::optional + { + struct P { EPalWorkSuitability_ WorkSuitability; int32 ReturnValue; } p{ Suitability, 0 }; + return CallForReturn(STR("GetRankBasedWorkSuitabilityBonus"), p, &P::ReturnValue); + } + auto GetCraftSpeedByWorkSuitability(EPalWorkSuitability_ Suitability) -> std::optional + { + struct P { EPalWorkSuitability_ WorkSuitability; int32 ReturnValue; } p{ Suitability, 0 }; + return CallForReturn(STR("GetCraftSpeedByWorkSuitability"), p, &P::ReturnValue); + } + auto GetCraftSpeedBuffRate(EPalWorkSuitability_ Suitability) -> std::optional + { + struct P { EPalWorkSuitability_ WorkSuitability; float ReturnValue; } p{ Suitability, 0.f }; + return CallForReturn(STR("GetCraftSpeedBuffRate"), p, &P::ReturnValue); + } + auto GetCraftSpeed_withBuff_WorkSuitability(EPalWorkSuitability_ Suitability) -> std::optional + { + struct P { EPalWorkSuitability_ Suitability; int32 ReturnValue; } p{ Suitability, 0 }; + return CallForReturn(STR("GetCraftSpeed_withBuff_WorkSuitability"), p, &P::ReturnValue); + } + auto SetWorkSuitabilityAddRank(EPalWorkSuitability_ Suitability, int32 AddRank) -> bool + { + struct P { EPalWorkSuitability_ WorkSuitability; int32 addRank; } p{ Suitability, AddRank }; + return Call(STR("SetWorkSuitabilityAddRank"), p); + } + + // ---- passive skills --------------------------------------------------- + // + // GetPassiveSkillList (TArray) is NOT wrapped -- see class + // comment. Add/Remove take scalar FName params, no such problem. + auto AddPassiveSkill(FName AddSkill, FName OverrideSkill) -> bool + { + struct P { FName AddSkill; FName OverrideSkill; } p{ AddSkill, OverrideSkill }; + return Call(STR("AddPassiveSkill"), p); + } + auto RemovePassiveSkill(FName SkillId) -> bool + { + struct P { FName SkillId; } p{ SkillId }; + return Call(STR("RemovePassiveSkill"), p); + } + auto GetPassiveRateBySkillEffect(EPalPassiveSkillEffectType_ EffectType) -> std::optional + { + struct P { EPalPassiveSkillEffectType_ EffectType; float ReturnValue; } p{ EffectType, 0.f }; + return CallForReturn(STR("GetPassiveRateBySkillEffect"), p, &P::ReturnValue); + } + auto GetPassiveRateByEquipment(EPalPassiveSkillEffectType_ EffectType) -> std::optional + { + struct P { EPalPassiveSkillEffectType_ EffectType; float ReturnValue; } p{ EffectType, 0.f }; + return CallForReturn(STR("GetPassiveRateByEquipment"), p, &P::ReturnValue); + } + + // ---- classification / misc flags --------------------------------------- + + auto GetSizeType() -> std::optional { return CallForReturn(STR("GetSizeType")); } + auto GetGroupType() -> std::optional { return CallForReturn(STR("GetGroupType")); } + auto GetGroupId() -> std::optional { return CallForReturn(STR("GetGroupId")); } + auto GetBaseCampId() -> std::optional { return CallForReturn(STR("GetBaseCampId")); } + auto GetArenaRank() -> std::optional { return CallForReturn(STR("GetArenaRank")); } + auto GetArenaRankPoint() -> std::optional { return CallForReturn(STR("GetArenaRankPoint")); } + auto IsInArena() -> std::optional { return CallForReturn(STR("IsInArena")); } + auto IsInRaidArea() -> std::optional { return CallForReturn(STR("IsInRaidArea")); } + auto IsUncapturable() -> std::optional { return CallForReturn(STR("IsUncapturable")); } + auto IsForceCapturable() -> std::optional { return CallForReturn(STR("IsForceCapturable")); } + auto IsAwakening() -> std::optional { return CallForReturn(STR("IsAwakening")); } + auto IsPartBroken() -> std::optional { return CallForReturn(STR("IsPartBroken")); } + auto IsParts() -> std::optional { return CallForReturn(STR("IsParts")); } + auto GetParentParameterObject() -> UObject* + { + struct P { UObject* ReturnValue; } p{ nullptr }; + auto Result = CallForReturn(STR("GetParentParameter"), p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + auto IsAssignedToExpedition() -> std::optional { return CallForReturn(STR("IsAssignedToExpedition")); } + auto IsAssignedToExpeditionIn(FGuid_ MapObjectId) -> std::optional + { + struct P { FGuid_ MapObjectConcreteInstanceId; bool ReturnValue; } p{ MapObjectId, false }; + return CallForReturn(STR("IsAssignedToExpeditionIn"), p, &P::ReturnValue); + } + auto IsExcludedFromTeamMission() -> std::optional { return CallForReturn(STR("IsExcludedFromTeamMission")); } + auto CanTargetFromAI() -> std::optional { return CallForReturn(STR("CanTargetFromAI")); } + auto HasGenusCategory(EPalGenusCategoryType_ Category) -> std::optional + { + struct P { EPalGenusCategoryType_ Category; bool ReturnValue; } p{ Category, false }; + return CallForReturn(STR("HasGenusCategory"), p, &P::ReturnValue); + } + auto CanAddTalentByItem(FName ItemName) -> std::optional + { + struct P { FName ItemName; bool ReturnValue; } p{ ItemName, false }; + return CallForReturn(STR("CanAddTalentByItem"), p, &P::ReturnValue); + } + auto GetNPCWeaponType() -> std::optional { return CallForReturn(STR("GetNPCWeaponType")); } + auto GetEquipItemContainerId() -> std::optional + { + return CallForReturn(STR("GetEquipItemContainerId")); + } + + auto SetUncapturable(bool bInUncapturable) -> bool + { + struct P { bool bInUncapturable; } p{ bInUncapturable }; + return Call(STR("SetUncapturable"), p); + } + auto SetForceCapturable(bool bInForceCapturable) -> bool + { + struct P { bool bInForceCapturable; } p{ bInForceCapturable }; + return Call(STR("SetForceCapturable"), p); + } + auto SetInArena(bool InArena) -> bool + { + struct P { bool InArena; } p{ InArena }; + return Call(STR("SetInArena"), p); + } + auto SetInRaidArea(bool InRaidArea) -> bool + { + struct P { bool InRaidArea; } p{ InRaidArea }; + return Call(STR("SetInRaidArea"), p); + } + auto SetCanTargetFromAI(bool bInCanTargetFromAI) -> bool + { + struct P { bool bInCanTargetFromAI; } p{ bInCanTargetFromAI }; + return Call(STR("SetCanTargetFromAI"), p); + } + auto SetSkinName(FName InSkinName) -> bool + { + struct P { FName InSkinName; } p{ InSkinName }; + return Call(STR("SetSkinName"), p); + } + auto SetSkinAppliedCharacterId(FGuid_ InCharacterId) -> bool + { + struct P { FGuid_ InCharacterId; } p{ InCharacterId }; + return Call(STR("SetSkinAppliedCharacterId"), p); + } + auto SetSecurityPoliceTargetPlayerId(FGuid_ PlayerId) -> bool + { + struct P { FGuid_ PlayerId; } p{ PlayerId }; + return Call(STR("SetSecurityPoliceTargetPlayerId"), p); + } + auto SetInvaderData(EPalInvaderType_ InvaderType, FGuid_ InBaseCampId) -> bool + { + struct P { EPalInvaderType_ InvaderType; FGuid_ InBaseCampId; } p{ InvaderType, InBaseCampId }; + return Call(STR("SetInvaderData"), p); + } + auto SetDisableNaturalUpdate(FName Key, bool Disable) -> bool + { + struct P { FName Key; bool Disable; } p{ Key, Disable }; + return Call(STR("SetDisableNaturalUpdate"), p); + } + auto SetDisableNaturalHealing(FName Key, bool Disable) -> bool + { + struct P { FName Key; bool Disable; } p{ Key, Disable }; + return Call(STR("SetDisableNaturalHealing"), p); + } + auto NaturalUpdateSaveParameter(EPalCharacterNaturalUpdateType_ Type) -> bool + { + struct P { EPalCharacterNaturalUpdateType_ Type; } p{ Type }; + return Call(STR("NaturalUpdateSaveParameter"), p); + } + auto SetForcePartBreak() -> bool { return CallVoid(STR("SetForcePartBreak")); } + + // ---- medical bed / misc actions ----------------------------------- + + auto StartRecuperatingInMedicalBed() -> bool { return CallVoid(STR("StartRecuperatingInMedicalBed")); } + auto RecuperateInMedicalBed() -> bool { return CallVoid(STR("RecuperateInMedicalBed")); } + auto EndRecuperatingInMedicalBed() -> bool { return CallVoid(STR("EndRecuperatingInMedicalBed")); } + auto StartRemainderOfLifeTimer() -> bool { return CallVoid(STR("StartRemainderOfLifeTimer")); } + }; +} diff --git a/Api/Pal/PalTypes.hpp b/Api/Pal/PalTypes.hpp new file mode 100644 index 0000000..c8d3614 --- /dev/null +++ b/Api/Pal/PalTypes.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include + +namespace PalUIExtension::Api::Pal +{ + + struct FGuid_ { uint32_t A{}, B{}, C{}, D{}; }; + + struct FFixedPoint64_ { int64_t Value{}; }; + + struct FPalContainerId_ { FGuid_ ID{}; }; + + using EPalTribeID_ = uint16_t; + using EPalWazaID_ = uint16_t; + using EPalPassiveSkillEffectType_ = uint8_t; + + enum class EPalStatusPhysicalHealthType_ : uint8_t + { + Healthful = 0, MinorInjury = 1, Severe = 2, + Dying = 3, DeadBody = 4, CloudCemetery = 5, + }; + + enum class EPalGroupType_ : uint8_t + { + Undefined = 0, Neutral = 1, Organization = 2, IndependentGuild = 3, + Guild = 4, ResidentEnemy = 5, RaidBoss = 6, + }; + + enum class EPalArenaRank_ : uint8_t + { + Bronze = 0, Silver = 1, Gold = 2, Platinum = 3, + Diamond = 4, Master = 5, Legend = 6, + }; + + enum class EPalGenusCategoryType_ : uint8_t + { + None = 0, Humanoid = 1, FourLegged = 2, Dragon = 3, + Fish = 4, Bird = 5, Monster = 6, Other = 7, + }; + + enum class EPalExpCalcType_ : uint8_t { None = 0, Build = 1, Craft = 2 }; + + enum class EPalSizeType_ : uint8_t + { + None = 0, XS = 1, S = 2, M = 3, L = 4, XL = 5, + }; + + enum class EPalStatusHungerType_ : uint8_t + { + Default = 0, Hunger = 1, Starvation = 2, + }; + + enum class EPalWeaponType_ : uint8_t + { + None = 0, ThrowObject = 1, Handgun = 2, AssaultRifle = 3, Shotgun = 4, + SniperRifle = 5, RocketLauncher = 6, MeleeWeapon = 7, Bow = 8, + BowGun = 9, FlameThrower = 10, GatlingGun = 11, Liftup = 12, + LaserRifle = 13, MissileLauncher = 14, GrenadeLauncher = 15, + Katana = 16, MetalDetector = 17, GiantClub = 18, FishingRod = 19, + LaserMiningTool = 20, MAX = 21, + }; + + enum class EPalBaseCampWorkerEventType_ : uint8_t + { + None = 0, Escape = 1, OverworkDeath = 2, Sick = 3, DodgeWork = 4, + DodgeWork_Short = 5, DodgeWork_Sleep = 6, EatTooMuch = 7, + Trantrum = 8, FightWithFriend = 9, TurnFoodBox = 10, DestroyBuilding = 11, + }; + + enum class EPalBaseCampWorkerSickType_ : uint8_t + { + None = 0, Cold = 1, Sprain = 2, Bulimia = 3, GastricUlcer = 4, + Fracture = 5, Weakness = 6, DepressionSprain = 7, DisturbingElement = 8, + }; + + enum class EPalInvaderType_ : uint8_t { None = 0, InvaderEnemy = 1, VisitorNPC = 2 }; + + enum class EPalFoodStatusEffectType_ : uint8_t + { + None = 0, Attack = 1, Defense = 2, HungerResist = 3, SANResist = 4, + WorkSpeed = 5, Regene_Hp = 6, MaxSP = 7, Exp_Increase = 8, + LeanBackInvalid = 9, KnockbackInvalid = 10, ExplosionResist = 11, + LeanBackAndKnockbackInvalid = 12, FullStomachKeep = 13, + }; + + enum class EPalCharacterNaturalUpdateType_ : uint8_t + { + Player = 0, OtomoPal = 1, BaseCampPal = 2, + }; + + enum class EPalOrganizationType_ : uint8_t + { + None = 0, Guild = 1, Police = 2, City = 3, Brigade = 4, + TeamBlackHunter = 5, TeamFireCult = 6, TeamWelfareGroup = 7, + Ally = 8, MAX = 9, + }; + + enum class EPalBattleBGMType_ : uint8_t + { + None = 0, Cute = 1, Cool = 2, Strong = 3, Human_Common = 4, + Human_Villain = 5, FieldBoss_Tier_1 = 6, FieldBoss_Tier_2 = 7, + FieldBoss_Tier_3 = 8, FieldBoss_Tier_4 = 9, FieldBoss_Tier_5 = 10, + Predator = 11, EyeofCthulhu = 12, WildlifeSanctuaryDrone = 13, + Machine = 14, Legend = 15, KingWhale = 16, RaidBoss = 17, + TowerBoss = 18, RaidBoss_KingBahamut = 19, RaidBoss_DarkMechaDragon = 20, + RaidBoss_MoonLord = 21, RaidBoss_LegendDeer = 22, RaidBoss_LegendDeer2 = 23, + WorldTreeDragon = 24, + }; +} diff --git a/Api/Pal/PalUIStatusModel.hpp b/Api/Pal/PalUIStatusModel.hpp new file mode 100644 index 0000000..2461ae4 --- /dev/null +++ b/Api/Pal/PalUIStatusModel.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include + +#include +#include + +#include +#include "PalIndividual.hpp" // EPalElementType_ + +namespace PalUIExtension::Api::Pal +{ + class PalUIStatusModel : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalUIStatusModel"); } + static auto DebugTag() -> const TCHAR* { return STR("PalUIStatusModel"); } + + PalUIStatusModel() = default; + explicit PalUIStatusModel(UObject* Obj) : UnrealWrapper(Obj, ClassOf(), DebugTag()) {} + + + // Count of pals currently loaded into the model -- "pal number". + auto GetDisplayPalNum() -> std::optional + { + return CallForReturn(STR("GetDisplayPalNum")); + } + + auto GetDisplayPalHandle(uint8 Index) -> UObject* + { + struct P { uint8 Index; UObject* ReturnValue; } p{ Index, nullptr }; + auto Result = CallForReturn(STR("GetDisplayPalHandle"), p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + + auto GetNowSelectedIndex() -> std::optional + { + return CallForReturn(STR("GetNowSelectedIndex")); + } + + auto ChangeIndex(uint8 Index) -> bool + { + struct P { uint8 Index; } p{ Index }; + return Call(STR("ChangeIndex"), p); + } + + auto OnClickedPalIcon(int32 Index) -> bool + { + struct P { int32 Index; } p{ Index }; + return Call(STR("OnClickedPalIcon"), p); + } + + // ---- extended: mechanical Index -> stat getters ---------------- + + auto GetDisplayLevel(int32 Index) -> std::optional + { + struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplayLevel"), p, &P::ReturnValue); + } + auto GetDisplaySpeed(int32 Index) -> std::optional + { + struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplaySpeed"), p, &P::ReturnValue); + } + auto GetDisplayMeleeAttack(int32 Index) -> std::optional + { + struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplayMeleeAttack"), p, &P::ReturnValue); + } + auto GetDisplayShotAttack(int32 Index) -> std::optional + { + struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplayShotAttack"), p, &P::ReturnValue); + } + auto GetDisplayDefence(int32 Index) -> std::optional + { + struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplayDefence"), p, &P::ReturnValue); + } + auto GetDisplayEatingHabits(int32 Index) -> std::optional + { + struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplayEatingHabits"), p, &P::ReturnValue); + } + auto GetDisplayNowExp(int32 Index) -> std::optional + { + struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplayNowExp"), p, &P::ReturnValue); + } + auto GetDisplayNextExp(int32 Index) -> std::optional + { + struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplayNextExp"), p, &P::ReturnValue); + } + auto GetDisplayHunger(int32 Index) -> std::optional + { + struct P { int32 Index; float ReturnValue; } p{ Index, 0.f }; + return CallForReturn(STR("GetDisplayHunger"), p, &P::ReturnValue); + } + auto GetDisplayMaxHunger(int32 Index) -> std::optional + { + struct P { int32 Index; float ReturnValue; } p{ Index, 0.f }; + return CallForReturn(STR("GetDisplayMaxHunger"), p, &P::ReturnValue); + } + + auto GetDisplayElementType1(int32 Index) -> std::optional + { + struct P { int32 Index; EPalElementType_ ReturnValue; } p{ Index, EPalElementType_::None }; + return CallForReturn(STR("GetDisplayElementType1"), p, &P::ReturnValue); + } + auto GetDisplayElementType2(int32 Index) -> std::optional + { + struct P { int32 Index; EPalElementType_ ReturnValue; } p{ Index, EPalElementType_::None }; + return CallForReturn(STR("GetDisplayElementType2"), p, &P::ReturnValue); + } + + // Raw byte -- see class comment. Do not assume ordering/meaning. + auto GetDisplayPhysicalHealthTypeRaw(int32 Index) -> std::optional + { + struct P { int32 Index; uint8 ReturnValue; } p{ Index, 0 }; + return CallForReturn(STR("GetDisplayPhysicalHealthType"), p, &P::ReturnValue); + } + + // ---- extended: FText / FString getters ------------------------- + + auto GetDisplayDefaultName(int32 Index) -> std::optional + { + struct P { int32 Index; FText ReturnValue; } p{ Index, {} }; + return CallForReturn(STR("GetDisplayDefaultName"), p, &P::ReturnValue); + } + auto GetDisplayCoopActionName(int32 Index) -> std::optional + { + struct P { int32 Index; FText ReturnValue; } p{ Index, {} }; + return CallForReturn(STR("GetDisplayCoopActionName"), p, &P::ReturnValue); + } + auto GetDisplayActiveSkillName(int32 Index, int32 SkillIndex) -> std::optional + { + struct P { int32 Index; int32 SkillIndex; FText ReturnValue; } p{ Index, SkillIndex, {} }; + return CallForReturn(STR("GetDisplayActiveSkillName"), p, &P::ReturnValue); + } + + auto GetDisplayNickName(int32 Index) -> std::optional + { + struct P { int32 Index; FString OutName; } p{ Index, {} }; + if (!Call(STR("GetDisplayNickName"), p)) return std::nullopt; + return p.OutName; + } + + auto ChangeNickname(int32 Index, const TCHAR* NewNickName) -> bool + { + struct P { int32 Index; FString NewNickName; } p{ Index, FString(NewNickName) }; + return Call(STR("ChangeNickname"), p); + } + }; +} \ No newline at end of file diff --git a/Api/Pal/PalUtility.hpp b/Api/Pal/PalUtility.hpp new file mode 100644 index 0000000..cdd91bb --- /dev/null +++ b/Api/Pal/PalUtility.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include "PalDatabaseCharacterParameter.hpp" + +namespace PalUIExtension::Api::Pal +{ + + class PalUtility : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/Pal.PalUtility"); } + static auto CDOPath() -> const TCHAR* { return STR("/Script/Pal.Default__PalUtility"); } + static auto DebugTag() -> const TCHAR* { return STR("PalUtility"); } + + static auto Instance() -> PalUtility& + { + static PalUtility Singleton{}; + return Singleton; + } + + // ---- static getters ----------------------------------------- + // + // Add more here as you need them -- see the class comment for + // the one-line shape. + + auto GetDatabaseCharacterParameter(UObject* WorldContextObject) -> PalDatabaseCharacterParameter + { + return GetSingleton( + STR("GetDatabaseCharacterParameter"), WorldContextObject); + } + + + auto ZukanIndexFor(FName CharacterId, UObject* WorldContextObject) -> std::optional + { + const uint64_t Key = NameKey(CharacterId); + + if (auto It = s_zukan_cache.find(Key); It != s_zukan_cache.end()) + { + if (It->second == kNoZukanEntry) return std::nullopt; + return It->second; + } + + PalDatabaseCharacterParameter DB = GetDatabaseCharacterParameter(WorldContextObject); + if (!DB) + { + return std::nullopt; + } + + auto Index = DB.GetZukanIndexResolved(CharacterId); + s_zukan_cache.emplace(Key, Index.value_or(kNoZukanEntry)); + return Index; + } + + static auto ClearZukanCache() -> void { s_zukan_cache.clear(); } + + auto NocturnalFor(FName CharacterId, UObject* WorldContextObject) -> std::optional + { + const uint64_t Key = NameKey(CharacterId); + + if (auto It = s_nocturnal_cache.find(Key); It != s_nocturnal_cache.end()) + { + return It->second != 0; + } + + PalDatabaseCharacterParameter DB = GetDatabaseCharacterParameter(WorldContextObject); + if (!DB) return std::nullopt; + + const FName RowName = PalDatabaseCharacterParameter::ResolveBaseRowName(CharacterId); + + auto Nocturnal = DB.GetNocturnal(RowName); + if (!Nocturnal.has_value()) return std::nullopt; + + s_nocturnal_cache.emplace(Key, *Nocturnal ? uint8_t{ 1 } : uint8_t{ 0 }); + return Nocturnal; + } + + static auto ClearNocturnalCache() -> void { s_nocturnal_cache.clear(); } + + private: + PalUtility() : UnrealWrapper(ResolveCDO(), ClassOf(), DebugTag()) {} + + static constexpr int32 kNoZukanEntry = -1; + static inline std::unordered_map s_zukan_cache{}; + + static inline std::unordered_map s_nocturnal_cache{}; + + static auto NameKey(FName N) -> uint64_t + { + return (static_cast(N.GetComparisonIndex().ToUnstableInt()) << 32) + | static_cast(static_cast(N.GetNumber())); + } + + PalUtility(const PalUtility&) = delete; + auto operator=(const PalUtility&) -> PalUtility & = delete; + + static auto ResolveCDO() -> UObject* + { + static Reflection::UnrealObjectRef Ref{ CDOPath(), DebugTag() }; + return Ref.Get(); + } + + auto CallStaticObjectGetter(const TCHAR* FunctionName, UObject* WorldContextObject) -> UObject* + { + struct P { UObject* WorldContextObject; UObject* ReturnValue; } p{ WorldContextObject, nullptr }; + auto Result = CallForReturn(FunctionName, p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + + template + auto GetSingleton(const TCHAR* FunctionName, UObject* WorldContextObject) -> T + { + UObject* Obj = CallStaticObjectGetter(FunctionName, WorldContextObject); + return Obj ? T{ Obj } : T{}; + } + }; +} \ No newline at end of file diff --git a/Api/PalBoxPalListWidget.hpp b/Api/PalBoxPalListWidget.hpp new file mode 100644 index 0000000..1780cbd --- /dev/null +++ b/Api/PalBoxPalListWidget.hpp @@ -0,0 +1,284 @@ +#pragma once + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace PalUIExtension::Api +{ + class PalBoxPalListWidget : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C"); + } + static auto WidgetTreePath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:WidgetTree"); + } + static auto DebugTag() -> const TCHAR* { return STR("PalListEntry"); } + + PalBoxPalListWidget() = default; + + explicit PalBoxPalListWidget(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) {} + + static auto BindFromSlotPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:BindFromSlot"); + } + static auto SetupPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:Setup"); + } + static auto UnbindPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:Unbind"); + } + static auto SetEmptyPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:SetEmpty"); + } + static auto OnUpdateHandleSlotPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:OnUpdateHandleSlot"); + } + static auto OnUpdateSkinPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:OnUpdateSkin"); + } + static auto UpdateHPPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:UpdateHP"); + } + static auto UpdateLevelPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalList.WBP_IngameMenu_PalBox_PalList_C:Update Level"); + } + + // ============================================================== + // FRAME LAYOUTS + // ============================================================== + + // ParmsSize 0x08. Full frame ends 0x3C by my arithmetic. + struct BindFromSlotFrame + { + UObject* TargetSlot; // 0x00 UPalIndividualSlot-ish + uint8 CompilerLocals[0x34]; // 0x08 + }; + + // ParmsSize 0x08. Full frame ends 0x1C. + struct SetupFrame + { + UObject* TargetSlot; // 0x00 + uint8 CompilerLocals[0x14]; // 0x08 + }; + + struct UnbindFrame + { + uint8 CompilerLocals[0x30]; + }; + + struct OnUpdateHandleSlotFrame + { + UObject* Slot; // 0x00 + UObject* LastHandle; // 0x08 + }; + + // ParmsSize 0x08 (FName). Full frame ends 0x62. + struct OnUpdateSkinFrame + { + FName NewSkinName; // 0x00 + uint8 CompilerLocals[0x5A]; // 0x08 + }; + + // ParmsSize 0x10. Full frame ends 0x158 + struct UpdateHPFrame + { + Pal::FFixedPoint64_ nowHP; // 0x00 + Pal::FFixedPoint64_ nowMaxHP; // 0x08 + uint8 CompilerLocals[0x148]; // 0x10 + }; + + // ParmsSize 0x10. Note DOUBLE, not FFixedPoint64 + struct UpdateHungerFrame + { + double nowHunger; // 0x00 + double nowMaxHunger; // 0x08 + uint8 CompilerLocals[0x64]; // 0x10 + }; + + // ParmsSize 0x04. Full frame ends 0x38. + struct UpdateLevelFrame + { + int32 NewLevel; // 0x00 + uint8 CompilerLocals[0x38]; // 0x04 + }; + + // ParmsSize 0x04. Full frame ends 0x39. + struct UpdateFavoriteFrame + { + int32 NewIndex; // 0x00 + uint8 CompilerLocals[0x35]; // 0x04 + }; + + // ParmsSize 0x10 (FString). Full frame ends 0x28. + struct UpdateNicknameFrame + { + FString NewNickName; // 0x00 + uint8 CompilerLocals[0x18]; // 0x10 + }; + + struct SetEnableDragDropFrame { bool bIsEnable; }; + + // EnumProperty at 0x08 with one byte before the frame ends. + struct DraggedButtonEventFrame + { + UObject* Widget; // 0x00 + uint8 PressType; // 0x08 + }; + + // ============================================================== + // CALLS + // ============================================================== + + auto BindFromSlot(UObject* Slot) -> bool + { + BindFromSlotFrame p{ Slot, {} }; + return Call(STR("BindFromSlot"), p); + } + auto Setup(UObject* Slot) -> bool + { + SetupFrame p{ Slot, {} }; + return Call(STR("Setup"), p); + } + auto Unbind() -> bool + { + UnbindFrame p{}; + return Call(STR("Unbind"), p); + } + auto SetEmpty() -> bool { return CallVoid(STR("SetEmpty")); } + + auto SetEnableDragDrop(bool bIsEnable) -> bool + { + SetEnableDragDropFrame p{ bIsEnable }; + return Call(STR("SetEnableDragDrop"), p); + } + auto SetLevel(int32 NewLevel) -> bool + { + UpdateLevelFrame p{ NewLevel, {} }; + return Call(STR("Update Level"), p); // the space is real + } + auto SetFavorite(int32 NewIndex) -> bool + { + UpdateFavoriteFrame p{ NewIndex, {} }; + return Call(STR("UpdateFavorite"), p); + } + auto UpdateHP(Pal::FFixedPoint64_ NowHP, Pal::FFixedPoint64_ NowMaxHP) -> bool + { + UpdateHPFrame p{ NowHP, NowMaxHP, {} }; + return Call(STR("UpdateHP"), p); + } + auto UpdateHunger(double NowHunger, double NowMaxHunger) -> bool + { + UpdateHungerFrame p{ NowHunger, NowMaxHunger, {} }; + return Call(STR("UpdateHunger"), p); + } + auto UpdateSkin(FName NewSkinName) -> bool + { + OnUpdateSkinFrame p{ NewSkinName, {} }; + return Call(STR("OnUpdateSkin"), p); + } + + + static auto SlotHandle(UObject* Slot) -> UObject* + { + if (!Slot) return nullptr; + Pal::LooseObject S{ Slot, STR("PalIndividualSlot") }; + return S.CallForReturn(STR("GetHandle")).value_or(nullptr); + } + + static auto SlotIsEmpty(UObject* Slot) -> std::optional + { + if (!Slot) return std::nullopt; + Pal::LooseObject S{ Slot, STR("PalIndividualSlot") }; + return S.CallForReturn(STR("IsEmpty")); + } + + static auto SpeciesFromSlot(UObject* Slot) -> std::optional + { + return Pal::PalIndividual::CharacterIdFromHandle(SlotHandle(Slot)); + } + + auto BoundSlot() const -> UObject* { return BoundObject(STR("TargetSlot")); } + + auto BoundHandle() const -> UObject* { return SlotHandle(BoundSlot()); } + + // ============================================================== + // BOUND WIDGETS (promoted variables -- property read, no walk) + // ============================================================== + + auto TextHP() const -> TextBlock { return WrapText(STR("Text_HP")); } + auto TextMaxHP() const -> TextBlock { return WrapText(STR("Text_MaxHP")); } + auto TextLevel() const -> TextBlock { return WrapText(STR("Text_LevelValue")); } + auto TextNickName() const -> TextBlock { return WrapText(STR("Text_NickName")); } + auto TextLvLabel() const -> TextBlock { return WrapText(STR("BP_PalTextBlock_LvText")); } + + auto CanvasRoot() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanel_119")); } + auto CanvasSelectFrame() const -> CanvasPanel { return WrapCanvas(STR("PalSelect_BaseTri")); } + + auto GaugeHP() const -> UObject* { return BoundObject(STR("Gauge_HP")); } + auto GaugeHunger() const -> UObject* { return BoundObject(STR("Gauge_Hunger")); } + auto ImageHPBar() const -> UObject* { return BoundObject(STR("Image_HP_Bar")); } + auto ImageHungerBar() const -> UObject* { return BoundObject(STR("Image_Huger_Bar")); } // engine spelling + auto ImageCheck() const -> UObject* { return BoundObject(STR("Image_Check")); } + auto ImageLocked() const -> UObject* { return BoundObject(STR("Image_Icon_Locked")); } + auto ImageListBar() const -> UObject* { return BoundObject(STR("Image_Pal_List_Bar")); } + auto ImageEmpty() const -> UObject* { return BoundObject(STR("Empty")); } + auto PalIcon() const -> UObject* { return BoundObject(STR("PalIcon")); } + auto RetainerBox() const -> UObject* { return BoundObject(STR("RetainerBox_0")); } + auto InvisibleButton() const -> UObject* { return BoundObject(STR("WBP_PalInvisibleButton")); } + auto DynamicMaterial() const -> UObject* { return BoundObject(STR("dynamicMaterial")); } + + v auto AnimFocus() const -> UObject* { return BoundObject(STR("Anm_Focus")); } + auto AnimNormal() const -> UObject* { return BoundObject(STR("Anm_Normal")); } + auto AnimNormalToFocus() const -> UObject* { return BoundObject(STR("Anm_NormalToFocus")); } + auto AnimPalAlive() const -> UObject* { return BoundObject(STR("Anm_Pal_Alive")); } + auto AnimPalDead() const -> UObject* { return BoundObject(STR("Anm_Pal_Dead")); } + + // ---- plain properties ---------------------------------------- + + auto IsDragDropEnabled() const -> std::optional + { + return PropertyBoolValue(STR("bEnableDragDrop")); + } + + private: + auto WrapCanvas(const TCHAR* Name) const -> CanvasPanel + { + UObject* O = BoundObject(Name); + return O ? CanvasPanel{ O } : CanvasPanel{}; + } + auto WrapText(const TCHAR* Name) const -> TextBlock + { + UObject* O = BoundObject(Name); + return O ? TextBlock{ O } : TextBlock{}; + } + + protected: + PalBoxPalListWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/PalBoxWidget.hpp b/Api/PalBoxWidget.hpp new file mode 100644 index 0000000..5e2f1fc --- /dev/null +++ b/Api/PalBoxWidget.hpp @@ -0,0 +1,224 @@ +#pragma once + +#include +#include + +#include + +#include "UnrealWrapper.hpp" +#include "UmgTypes.hpp" +#include "Widget.hpp" +#include "UserWidget.hpp" +#include "WidgetTree.hpp" +#include "CanvasPanel.hpp" +#include "SizeBox.hpp" +#include "TextBlock.hpp" + +#include "PalDetailWidget.hpp" +#include "PalBoxPalListWidget.hpp" + +namespace PalUIExtension::Api +{ + + class PalBoxWidget : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C"); + } + static auto DebugTag() -> const TCHAR* { return STR("PalBox"); } + + PalBoxWidget() = default; + + explicit PalBoxWidget(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) {} + + auto PalDetail() const -> PalDetailWidget + { + UObject* O = BoundObject(STR("WBP_IngameMenu_PalBox_PalDetail")); + return O ? PalDetailWidget{ O } : PalDetailWidget{}; + } + + static constexpr int32 kPartySlotCount = 5; + + auto PartySlot(int32 Index) const -> PalBoxPalListWidget + { + if (Index < 0 || Index >= kPartySlotCount) return PalBoxPalListWidget{}; + + const TCHAR* Names[kPartySlotCount] = { + STR("WBP_IngameMenu_PalBox_PalList_0"), + STR("WBP_IngameMenu_PalBox_PalList_1"), + STR("WBP_IngameMenu_PalBox_PalList_2"), + STR("WBP_IngameMenu_PalBox_PalList_3"), + STR("WBP_IngameMenu_PalBox_PalList_4"), + }; + + UObject* O = BoundObject(Names[Index]); + return O ? PalBoxPalListWidget{ O } : PalBoxPalListWidget{}; + } + + // Convenience for "do something to every party tile." + auto PartySlots() const -> std::vector + { + std::vector Out; + Out.reserve(kPartySlotCount); + for (int32 i = 0; i < kPartySlotCount; ++i) + { + PalBoxPalListWidget W = PartySlot(i); + if (W.IsValid()) Out.push_back(W); + } + return Out; + } + + auto BoxPalList() const -> UObject* { return BoundObject(STR("WBP_BoxPalList")); } + auto BaseCampPalList() const -> UObject* { return BoundObject(STR("WBP_BaseCampPalList")); } + + // ============================================================== + // INJECTION ANCHORS + // ============================================================== + auto CanvasPalBox() const -> CanvasPanel { return WrapCanvas(STR("Canvas_PalBox")); } + auto CanvasPalCamp() const -> CanvasPanel { return WrapCanvas(STR("Canvas_PalCamp")); } + + auto FindCanvas(const TCHAR* Name) -> CanvasPanel { return Child(Name); } + + auto SizeBoxPresetButton() const -> SizeBox + { + UObject* O = BoundObject(STR("SizeBox_PresetButton")); + return O ? SizeBox{ O } : SizeBox{}; + } + + // ============================================================== + // BOUND WIDGETS + // ============================================================== + + auto TextBaseCampName() const -> TextBlock + { + UObject* O = BoundObject(STR("Text_BaseCampName")); + return O ? TextBlock{ O } : TextBlock{}; + } + + auto CloseButton() const -> UObject* { return BoundObject(STR("CloseButton")); } + auto PresetButton() const -> UObject* { return BoundObject(STR("WBP_CommonButton_Preset")); } + auto ReturnButton() const -> UObject* { return BoundObject(STR("WBP_CommonButton_Return")); } + auto MenuButton() const -> UObject* { return BoundObject(STR("WBP_Menu_btn")); } + auto ImageFrame() const -> UObject* { return BoundObject(STR("Image_Frame")); } + + // ============================================================== + // STATE + // ============================================================== + + auto IsDragDropEnabled() const -> std::optional + { + return PropertyBoolValue(STR("bIsEnableDragDropControl")); + } + + auto PartyPalDetails() const -> std::vector + { + std::vector Out; + + auto* Arr = TryPropertyPtr>(STR("PartyPalDetails")); + if (!Arr) return Out; + + if (!Arr->IsPlausible(kMaxPlausibleEntries)) + { + Output::send( + STR("[{}] PartyPalDetails does not look like a live TArray (Num={}, Max={})\n"), + m_tag, Arr->Num, Arr->Max); + return Out; + } + + Out.reserve(static_cast(Arr->Num)); + for (int32 i = 0; i < Arr->Num; ++i) + { + if (UObject* Entry = Arr->Data[i]) Out.push_back(Entry); + } + return Out; + } + + // ============================================================== + // HOOK TARGETS + // ============================================================== + + static auto DisplayPalDetailPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:Display Pal Detail"); + } + static auto SetupPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:Setup"); + } + static auto SetupPartyPalPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:Setup Party Pal"); + } + static auto SetBaseCampPalListPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:SetBaseCampPalList"); + } + static auto SetEnableDragDropPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:SetEnableDragDrop"); + } + static auto OnClickedPartySlotInternalPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:OnClickedPartySlotInternal"); + } + static auto OnHoveredPartySlotInternalPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:OnHoveredPartySlot_Internal"); + } + static auto OnInitializedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:OnInitialized"); + } + static auto PreConstructPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/" + "WBP_IngameMenu_PalBox.WBP_IngameMenu_PalBox_C:PreConstruct"); + } + + // ---- frames -------------------------------------------------- + + struct DisplayPalDetailFrame + { + UObject* Slot; // 0x00 + uint8 slotType; // 0x08 + }; + + struct SetupFrame { UObject* Parameter; }; // 0x00, ParmsSize 0x08 + struct SetEnableDragDropFrame { bool bIsEnableDragDrop; }; + + struct SlotFrame { UObject* Slot; }; + struct SetBaseCampPalListFrame + { + UObject** Data; + int32 Num; + int32 Max; + }; + static_assert(sizeof(SetBaseCampPalListFrame) == 16, + "SetBaseCampPalList frame is 16 bytes on the dumped build"); + + + private: + static constexpr int32 kMaxPlausibleEntries = 1024; + + auto WrapCanvas(const TCHAR* Name) const -> CanvasPanel + { + UObject* O = BoundObject(Name); + return O ? CanvasPanel{ O } : CanvasPanel{}; + } + + protected: + PalBoxWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/PalCharacterScrollList.hpp b/Api/PalCharacterScrollList.hpp new file mode 100644 index 0000000..1ceb819 --- /dev/null +++ b/Api/PalCharacterScrollList.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include +#include + +#include + +#include "UnrealWrapper.hpp" +#include "Widget.hpp" +#include "UserWidget.hpp" +#include "WrapBox.hpp" + +namespace PalUIExtension::Api +{ + // ================================================================== + // WBP_PalCharacterScrollList_C -- the end of the chain. This is the + // widget that creates, owns and destroys the slot tiles. + // + // PalBoxWidget + // -> WBP_BoxPalList -> WBP_BoxPalListBase -> WBP_BoxPalScrollList + // -> WBP_BaseCampPalList ------------------------> (same class) + // == THIS + // -> MyBlock (UWrapBox) + // -> the tiles + // ================================================================== + class PalCharacterScrollList : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C"); + } + static auto DebugTag() -> const TCHAR* { return STR("PalCharacterScrollList"); } + + PalCharacterScrollList() = default; + explicit PalCharacterScrollList(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) {} + + // ============================================================== + // THE TILES + // ============================================================== + + auto TileContainer() const -> WrapBox + { + UObject* O = BoundObject(STR("MyBlock")); + return O ? WrapBox{ O } : WrapBox{}; + } + + auto Tiles() -> std::vector + { + WrapBox Box = TileContainer(); + if (!Box.IsValid()) return {}; + return Box.GetChildObjects(); + } + + auto TileClass() const -> UClass* + { + return PropertyValue(STR("slotWidgetClass")).value_or(nullptr); + } + + auto TileClassName() const -> RC::StringType + { + UClass* C = TileClass(); + return C ? C->GetFullName() : RC::StringType{ STR("") }; + } + + // ============================================================== + // HOOK TARGETS + // ============================================================== + + static auto CreateSlotWidgetPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:CreateSlotWidget"); + } + static auto AddSlotButtonToScrollListPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:AddSlotButtonToScrollList"); + } + static auto BindButtonEventsPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:BindButtonEvents"); + } + + static auto OnUpdateSlotBindedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:OnUpdateSlot_Binded"); + } + static auto OnSlotSyncedInternalPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:OnSlotSynced_Internal"); + } + static auto OnEmptySlotInternalPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:OnEmptySlot_Internal"); + } + static auto AddCharacterSlotPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:AddCharacterSlot"); + } + + static auto ClearAllChildrenPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:ClearAllChildren"); + } + static auto ClearInnerChildrenPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/CommonWidget/CommonScrollList/" + "WBP_PalCharacterScrollList.WBP_PalCharacterScrollList_C:ClearInnnerChildren"); + } + + // ---- frames -------------------------------------------------- + + struct CreateSlotWidgetFrame + { + UObject* TargetSlot; + UObject* createdSlotButton; + }; + + // Covers AddSlotButtonToScrollList (createdSlotButton), + // BindButtonEvents (createdSlotButton), OnUpdateSlot_Binded + // (SlotButton), OnSlotSynced_Internal (SelfButton), + // OnEmptySlot_Internal (SlotButton), OnHovered_Internal / + // OnUnhovered_Internal / OnFocused_Internal / + // OnUnfocused_Internal (Widget), and "On Right Clicked Internal" + // (ButtonBase). Eleven functions, one shape -- a single tile + // pointer at 0x00. Naming it once is the honest version. + struct TileFrame { UObject* Tile; }; + struct AddCharacterSlotFrame { UObject* TargetSlot; }; + + struct TileClickFrame + { + UObject* Widget; // 0x00 + uint8 PressType; // 0x08 + }; + + struct AddCharacterSlotsFrame + { + UObject** Data; + int32 Num; + int32 Max; + }; + + + protected: + PalCharacterScrollList(UObject* Obj, UClass* Expected, const TCHAR* Tag) : UserWidget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/PalCommonCharacterSlot.hpp b/Api/PalCommonCharacterSlot.hpp new file mode 100644 index 0000000..cb38b3b --- /dev/null +++ b/Api/PalCommonCharacterSlot.hpp @@ -0,0 +1,179 @@ +#pragma once + +#include + +#include "UnrealWrapper.hpp" +#include "UmgTypes.hpp" +#include "Widget.hpp" +#include "UserWidget.hpp" +#include "WidgetTree.hpp" +#include "CanvasPanel.hpp" +#include "Overlay.hpp" +#include "TextBlock.hpp" + +namespace PalUIExtension::Api +{ + class PalCommonCharacterSlot : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/WBP_PalCommonCharacterSlot.WBP_PalCommonCharacterSlot_C"); + } + static auto DebugTag() -> const TCHAR* { return STR("PalCharacterSlot"); } + + PalCommonCharacterSlot() = default; + explicit PalCommonCharacterSlot(UObject* Obj) + : UserWidget(Obj, ClassOf(), DebugTag()) {} + + // ============================================================== + // ANCHORS + // ============================================================== + + auto IconOverlay() -> Overlay + { + return Child(STR("Overlay_60")); + } + + auto OverlayAwakening() const -> Overlay { return WrapOverlay(STR("Overlay_Awakening")); } + auto OverlayBan() const -> Overlay { return WrapOverlay(STR("Overlay_Ban")); } + auto OverlayDoping() const -> Overlay { return WrapOverlay(STR("Overlay_Doping")); } + auto OverlayRevive() const -> Overlay { return WrapOverlay(STR("Overlay_Revive")); } + + auto IconWidget() const -> UObject* { return BoundObject(STR("WBP_PalCommonCharacterIcon")); } + + + auto ImageWorkIcon() const -> UObject* { return BoundObject(STR("Image_IconPalWork")); } + auto ImageExpedition() const -> UObject* { return BoundObject(STR("Image_Icon_Expedition")); } + auto ImageLocked() const -> UObject* { return BoundObject(STR("Image_Icon_Locked")); } + auto ImageGlobalImport() const -> UObject* { return BoundObject(STR("Image_Icon_GlobalInport")); } // engine spelling + auto ImageSleep() const -> UObject* { return BoundObject(STR("Image_PalSleep")); } + auto ImageMeal() const -> UObject* { return BoundObject(STR("Image_Meal")); } + auto ImageMutant() const -> UObject* { return BoundObject(STR("Image_Mutant")); } + auto ImageRare() const -> UObject* { return BoundObject(STR("Image_Rare")); } + auto ImageStrong() const -> UObject* { return BoundObject(STR("Image_Strong")); } + auto ImageLowHealth() const -> UObject* { return BoundObject(STR("Image_PalLowHealth")); } + auto ImageDying() const -> UObject* { return BoundObject(STR("Image_PalDying")); } + auto ImageBattle() const -> UObject* { return BoundObject(STR("Image_PalBattleImage")); } + + auto TextHPPercent() const -> TextBlock { return WrapText(STR("Text_HPPercent")); } + auto TextReviveTimer() const -> TextBlock { return WrapText(STR("Text_ReviveTimer")); } + + // ---- state ---------------------------------------------------- + + auto IsBattleModeDisplayed() const -> std::optional { return PropertyBoolValue(STR("isBattleModeDisplay")); } + auto IsLowHealthBlinking() const -> std::optional { return PropertyBoolValue(STR("isLowHealthBlink")); } + auto IsHealthPercentDisplayed()const -> std::optional { return PropertyBoolValue(STR("isDisplayHealthPercent")); } + auto IsRarityDisplayed() const -> std::optional { return PropertyBoolValue(STR("isDisplayRarity")); } + auto LastWorkDetail() const -> std::optional { return PropertyBoolValue(STR("lastWorkDetail")); } + auto IsDopingEffectDisplayed() const -> std::optional { return PropertyBoolValue(STR("bIsDisplayDopingEffect")); } + auto IsFavoriteMarkDisplayed() const -> std::optional { return PropertyBoolValue(STR("bIsDisplayFavoriteMark")); } + auto IsAwakeningMarkDisplayed()const -> std::optional { return PropertyBoolValue(STR("bIsDisplayAwakeningMark")); } + + auto CachedLastHPPercent() const -> std::optional { return PropertyValue(STR("chachedLastHPPercent")); } // engine spelling + + // ============================================================== + // HOOK TARGETS + // ============================================================== + + static auto OnUpdateSlotBindedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/WBP_PalCommonCharacterSlot.WBP_PalCommonCharacterSlot_C:On Update Slot Binded"); + } + + static auto OnInitializedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlot.WBP_PalCommonCharacterSlot_C:OnInitialized"); + } + + static auto OnSetEmptyBindedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/WBP_PalCommonCharacterSlot.WBP_PalCommonCharacterSlot_C:On Set Empty Binded"); + } + + static auto OnNotifyWorkDetailBindedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlot.WBP_PalCommonCharacterSlot_C:OnNotifyWorkDetail_Binded"); + } + static auto DisplayBanMarkPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlot.WBP_PalCommonCharacterSlot_C:DisplayBanMark"); + } + + // ---- frames --------------------------------------------------- + + struct UpdateSlotBindedFrame { UObject* TargetSlot; }; + + struct BoolFrame { bool bValue; }; + + struct RegisterIconWidgetFrame { UObject* IconWidget; }; + struct UpdateFavoriteFrame { int32 NewIndex; }; + + struct UpdateHPFrame + { + int64 nowHP; + int64 nowMaxHP; + }; + + // On Set Rarity Binded: two adjacent bools. + struct SetRarityFrame + { + bool IsBoss; // 0x00 + bool IsRare; // 0x01 + }; + + private: + auto WrapOverlay(const TCHAR* Name) const -> Overlay + { + UObject* O = BoundObject(Name); + return O ? Overlay{ O } : Overlay{}; + } + auto WrapText(const TCHAR* Name) const -> TextBlock + { + UObject* O = BoundObject(Name); + return O ? TextBlock{ O } : TextBlock{}; + } + + protected: + PalCommonCharacterSlot(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) {} + }; + + + class PalCommonCharacterIcon : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/WBP_PalCommonCharacterIcon.WBP_PalCommonCharacterIcon_C"); + } + + static auto DebugTag() -> const TCHAR* { return STR("PalCharacterIcon"); } + + PalCommonCharacterIcon() = default; + explicit PalCommonCharacterIcon(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) {} + + auto IconImage() const -> UObject* { return BoundObject(STR("iconImage")); } + auto Throbber() const -> UObject* { return BoundObject(STR("CircularThrobber_96")); } + auto DynamicMaterial() const -> UObject* { return BoundObject(STR("dynamicMaterial")); } + auto IsEmpty() const -> std::optional { return PropertyBoolValue(STR("IsEmpty")); } + auto SphereMaskRadius() const -> std::optional { return PropertyValue(STR("SphereMaskRadius")); } + + + static auto OnLoadedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/WBP_PalCommonCharacterIcon.WBP_PalCommonCharacterIcon_C:OnLoaded"); + } + static auto OnEmptyPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/WBP_PalCommonCharacterIcon.WBP_PalCommonCharacterIcon_C:OnEmpty"); + } + + protected: + PalCommonCharacterIcon(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/PalCommonCharacterSlotButton.hpp b/Api/PalCommonCharacterSlotButton.hpp new file mode 100644 index 0000000..e7a71d5 --- /dev/null +++ b/Api/PalCommonCharacterSlotButton.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include + +#include "UnrealWrapper.hpp" +#include "UmgTypes.hpp" +#include "Widget.hpp" +#include "UserWidget.hpp" +#include "WidgetTree.hpp" +#include "CanvasPanel.hpp" +#include "Overlay.hpp" + +namespace PalUIExtension::Api +{ + // ================================================================== + // WBP_PalCommonCharacterSlotButton_C + // ================================================================== + class PalCommonCharacterSlotButton : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlotButton.WBP_PalCommonCharacterSlotButton_C"); + } + static auto DebugTag() -> const TCHAR* { return STR("PalSlotButton"); } + + PalCommonCharacterSlotButton() = default; + explicit PalCommonCharacterSlotButton(UObject* Obj) + : UserWidget(Obj, ClassOf(), DebugTag()) {} + + // ============================================================== + // NESTED WIDGETS + // ============================================================== + + auto IconWidget() const -> UObject* { return BoundObject(STR("WBP_PalCommonCharacterSlot")); } + auto InvisibleButton() const -> UObject* { return BoundObject(STR("WBP_PalInvisibleButton")); } + + // ============================================================== + // ANCHORS + // ============================================================== + + auto RootCanvas() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanel_36")); } + auto OverlaySelect() const -> Overlay { return WrapOverlay(STR("Overlay_Select")); } + + // TODO: Review below and remove or update. + auto IconAnchor() -> Overlay + { + return Child(STR("Overlay_60"), + WidgetTree::EScope::IncludeNestedUserWidgets); + } + + auto Check0() const -> UObject* { return BoundObject(STR("Check_0")); } + auto Check1() const -> UObject* { return BoundObject(STR("Check_1")); } + auto Check2() const -> UObject* { return BoundObject(STR("Check_2")); } + auto FocusedFrame() const -> UObject* { return BoundObject(STR("FocusedFrame")); } + auto PushEff() const -> UObject* { return BoundObject(STR("PushEff")); } + + // ---- state ---------------------------------------------------- + auto IsDisplayingDopingEffect() const -> std::optional { return PropertyBoolValue(STR("bIsDisplayDopingEffect")); } + auto HasCheckAnimPlayed() const -> std::optional { return PropertyBoolValue(STR("bCheckAnimPlayed")); } + + // ============================================================== + // HOOK TARGETS + // ============================================================== + + static auto DestructPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlotButton.WBP_PalCommonCharacterSlotButton_C:Destruct"); + } + + static auto RegisterCharacterSlotWidgetPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlotButton.WBP_PalCommonCharacterSlotButton_C:RegisterCharacterSlotWidget"); + } + static auto OnInitializedPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlotButton.WBP_PalCommonCharacterSlotButton_C:OnInitialized"); + } + + static auto SetEnablePath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlotButton.WBP_PalCommonCharacterSlotButton_C:SetEnable"); + } + static auto SetSearchResultActivePath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/Thumbnails/Character/" + "WBP_PalCommonCharacterSlotButton.WBP_PalCommonCharacterSlotButton_C:SetSearchResultActive"); + } + + // ---- frames --------------------------------------------------- + + struct WidgetFrame { UObject* Widget; }; // PlayFocusAnim / PlayUnfocusAnim + struct RegisterCharacterSlotWidgetFrame { UObject* characterSlotWidget; }; + struct RegisterButtonFrame { UObject* Button; }; + struct BoolFrame { bool bValue; }; // SetEnable, SetSearchResultActive, the AnmEvent_* family + + // ============================================================== + // CALLS + // ============================================================== + + auto SetEnable(bool bIsEnable) -> bool + { + BoolFrame p{ bIsEnable }; + return Call(STR("SetEnable"), p); + } + auto SetEnableGreyoutOnly(bool bIsEnable) -> bool + { + BoolFrame p{ bIsEnable }; + return Call(STR("SetEnable_GreyoutOnly"), p); + } + auto SetSearchResultActive(bool bActive) -> bool + { + // The parameter is spelled bAvtive in the engine. Dont fix. Lol. + BoolFrame p{ bActive }; + return Call(STR("SetSearchResultActive"), p); + } + + private: + auto WrapCanvas(const TCHAR* Name) const -> CanvasPanel + { + UObject* O = BoundObject(Name); + return O ? CanvasPanel{ O } : CanvasPanel{}; + } + auto WrapOverlay(const TCHAR* Name) const -> Overlay + { + UObject* O = BoundObject(Name); + return O ? Overlay{ O } : Overlay{}; + } + + protected: + PalCommonCharacterSlotButton(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/PalDetailWidget.hpp b/Api/PalDetailWidget.hpp new file mode 100644 index 0000000..46f68e5 --- /dev/null +++ b/Api/PalDetailWidget.hpp @@ -0,0 +1,270 @@ +#pragma once + +#include +#include + +#include +#include + +#include "UmgTypes.hpp" +#include "UnrealWrapper.hpp" +#include "Widget.hpp" +#include "UserWidget.hpp" +#include "WidgetTree.hpp" +#include "TextBlock.hpp" +#include "CanvasPanel.hpp" + +#include +#include +#include + +using namespace PalUIExtension::Api::Pal; + +namespace PalUIExtension::Api +{ + + + class PalDetailWidget : public UserWidget + { + public: + static auto ClassPath() -> const TCHAR* { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalDetail.WBP_IngameMenu_PalBox_PalDetail_C"); + } + + static auto WidgetTreePath() -> const TCHAR* { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalDetail.WBP_IngameMenu_PalBox_PalDetail_C:WidgetTree"); + } + static auto DebugTag() -> const TCHAR* { return STR("PalDetail"); } + + PalDetailWidget() = default; + explicit PalDetailWidget(UObject* Obj) : UserWidget(Obj, ClassOf(), DebugTag()) { + } + + struct GetBindedHandleFrame + { + UObject* targetHandle; // 0x00 out + bool bValidSoftRef; // 0x08 + uint8 Pad09[7]; // 0x09 + UObject* SoftRefAsObject; // 0x10 + UObject* CastAsHandle; // 0x18 + bool bCastSucceeded; // 0x20 + uint8 Pad21[7]; // 0x21 + }; + static_assert(sizeof(GetBindedHandleFrame) == 40, + "GetBindedHandle frame is 40 bytes on the dumped build"); + + struct GetBindedIndividualParameterFrame + { + UObject* IndividualParameter; // 0x00 out + UObject* Handle; // 0x08 + UObject* TryGetResult; // 0x10 + bool bValid0; // 0x18 + bool bValid1; // 0x19 + uint8 Pad1A[6]; // 0x1A + }; + static_assert(sizeof(GetBindedIndividualParameterFrame) == 32, + "GetBindedIndividualParameter frame is 32 bytes on the dumped build"); + + auto GetBindedHandle() -> UObject* + { + GetBindedHandleFrame p{}; + if (!Call(STR("GetBindedHandle"), p)) return nullptr; + return p.targetHandle; + } + auto GetBindedIndividualParameter() -> UObject* + { + GetBindedIndividualParameterFrame p{}; + if (!Call(STR("GetBindedIndividualParameter"), p)) return nullptr; + return p.IndividualParameter; + } + + auto CurrentHandle() -> PalIndividualCharacterHandle + { + UObject* H = GetBindedHandle(); + return H ? PalIndividualCharacterHandle{ H } : PalIndividualCharacterHandle{}; + } + auto CurrentParameter() -> PalIndividualCharacterParameter + { + UObject* P = GetBindedIndividualParameter(); + return P ? PalIndividualCharacterParameter{ P } : PalIndividualCharacterParameter{}; + } + + auto ResolveIdentity() -> PalIdentity + { + if (UObject* Param = GetBindedIndividualParameter()) + { + return PalIndividual::IdentifyFromParameter(Param); + } + return PalIndividual::Identify(GetBindedHandle()); + } + + auto CurrentCharacterId() -> std::optional + { + if (UObject* Param = GetBindedIndividualParameter()) + { + return PalIndividual::CharacterId(Param); + } + return PalIndividual::CharacterIdFromHandle(GetBindedHandle()); + } + + + auto CanvasBase() const -> CanvasPanel { return WrapCanvas(STR("Canvas_Base")); } + auto CanvasBaseToolTip() const -> CanvasPanel { return WrapCanvas(STR("Canvas_Base_ToolTip")); } + auto CanvasInfo() const -> CanvasPanel { return WrapCanvas(STR("Canvas_Info")); } + auto CanvasFood() const -> CanvasPanel { return WrapCanvas(STR("Canvas_Food")); } + auto CanvasPartnerSkill() const -> CanvasPanel { return WrapCanvas(STR("Canvas_PartnerSkill")); } + auto CanvasPotencial() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanel_Potencial")); } + auto CanvasDebuff() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanelDebuff")); } + auto CanvasStatePalWork() const -> CanvasPanel { return WrapCanvas(STR("CanvasPanelStatePalWork")); } + + auto TextPalName() const -> TextBlock { return WrapText(STR("Text_PalName")); } + auto TextLevel() const -> TextBlock { return WrapText(STR("Text_LevelValue")); } + auto TextNowHP() const -> TextBlock { return WrapText(STR("Text_NowHP")); } + auto TextMaxHP() const -> TextBlock { return WrapText(STR("Text_MaxHP")); } + auto TextNowHunger() const -> TextBlock { return WrapText(STR("Text_NowHunger")); } + auto TextMaxHunger() const -> TextBlock { return WrapText(STR("Text_MaxHunger")); } + auto TextNowSanity() const -> TextBlock { return WrapText(STR("Text_NowSanity")); } + auto TextMaxSanity() const -> TextBlock { return WrapText(STR("Text_MaxSanity")); } + auto TextAttack() const -> TextBlock { return WrapText(STR("Text_AttackValue")); } + auto TextDefense() const -> TextBlock { return WrapText(STR("Text_DefenseValue")); } + auto TextWorkValue() const -> TextBlock { return WrapText(STR("Text_WorkValue")); } + auto TextWorkTypeName() const -> TextBlock { return WrapText(STR("Text_WorkTypeName")); } + auto TextNextExp() const -> TextBlock { return WrapText(STR("Text_NextExp")); } + auto TextFriendshipRank() const -> TextBlock { return WrapText(STR("Text_FriendshipRank_Num")); } + + auto GaugeHP() const -> UObject* { return BoundObject(STR("Gauge_HP")); } + auto GaugeHunger() const -> UObject* { return BoundObject(STR("Gauge_Hunger")); } + auto GaugeExp() const -> UObject* { return BoundObject(STR("Gauge_Exp")); } + auto GaugeFriendship() const -> UObject* { return BoundObject(STR("Gauge_Friendship")); } + auto ImageRare() const -> UObject* { return BoundObject(STR("Image_Rare")); } + auto ImageStrong() const -> UObject* { return BoundObject(STR("Image_Strong")); } + auto ImageMutant() const -> UObject* { return BoundObject(STR("Image_Mutant")); } + auto CharacterIcon() const -> UObject* { return BoundObject(STR("WBP_PalCommonCharacterIcon")); } + auto ElementIcon0() const -> UObject* { return BoundObject(STR("WBP_PalElementIcon")); } + auto ElementIcon1() const -> UObject* { return BoundObject(STR("WBP_PalElementIcon_1")); } + + // ---- plain properties ---------------------------------------- + auto SavedMaxHunger() const -> std::optional { return PropertyValue(STR("savedMaxHunger")); } + auto SavedMaxSanity() const -> std::optional { return PropertyValue(STR("savedMaxSanity")); } + auto HidePassive() const -> std::optional { return PropertyValue(STR("HidePassive")); } + + + // ---- calls --------------------------------------------------- + auto SetEmpty() -> bool { return CallVoid(STR("SetEmpty")); } + auto SetupPartnerSkillInfo() -> bool { return CallVoid(STR("SetupPartnerSkillInfo")); } + auto ToggleSkillInfo() -> bool { return CallVoid(STR("ToggleSKillInfo")); } // engine spelling + auto OverrideHideTalent() -> bool { return CallVoid(STR("OverrideHideTalent")); } + + auto SetVisiblePartnerSkillInfo(bool bVisible) -> bool + { + struct P { bool bVisible; } p{ bVisible }; + return Call(STR("SetVisiblePartnerSkillInfo"), p); + } + auto SetVisibleToggleSkillInfoKeyGuide(bool bVisible) -> bool + { + struct P { bool bVisible; } p{ bVisible }; + return Call(STR("SetVisibleToggleSkillInfoKeyGuide"), p); + } + auto SetAwakening(bool bAwaked) -> bool + { + struct P { bool bAwaked; } p{ bAwaked }; + return Call(STR("SetAwakening"), p); + } + auto SetImportedFlag(bool bImportedFlag) -> bool + { + struct P { bool bImportedFlag; } p{ bImportedFlag }; + return Call(STR("SetImportedFlag"), p); + } + auto SetDetailMode(bool IsTip) -> bool + { + struct P { bool IsTip; } p{ IsTip }; + return Call(STR("Set Detail Mode"), p); // the spaces are real + } + auto SetRarity(bool IsBoss, bool IsRare) -> bool + { + struct P { bool IsBoss; bool IsRare; } p{ IsBoss, IsRare }; + return Call(STR("SetRarity"), p); + } + auto SetRank(int32 Rank) -> bool + { + struct P { int32 Rank; } p{ Rank }; + return Call(STR("SetRank"), p); + } + auto SetSoulRank(int32 Rank) -> bool + { + struct P { int32 Rank; } p{ Rank }; + return Call(STR("SetSoulRank"), p); + } + auto SetFoodAmount(int32 FoodAmount) -> bool + { + struct P { int32 FoodAmount; } p{ FoodAmount }; + return Call(STR("SetFoodAmount"), p); + } + auto SetFriendShip(int32 FriendshipRank, int32 FriendshipPoint) -> bool + { + struct P { int32 FriendshipRank; int32 FriendshipPoint; } p{ FriendshipRank, FriendshipPoint }; + return Call(STR("SetFriendShip"), p); + } + auto SetGender(EPalGenderType_ GenderType) -> bool + { + struct P { EPalGenderType_ GenderType; } p{ GenderType }; + return Call(STR("SetGender"), p); + } + auto SetElementType(EPalElementType_ Type1, EPalElementType_ Type2) -> bool + { + struct P { EPalElementType_ type1; EPalElementType_ type2; } p{ Type1, Type2 }; + return Call(STR("SetElementType"), p); + } + + struct WorkSuitabilityCheck { bool bValid{}; EPalWorkSuitability_ Out{}; }; + auto CheckValidWorkSuitability(EPalWorkSuitability_ In) -> std::optional + { + struct P + { + EPalWorkSuitability_ InWorkSuitability; + bool IsValid; + EPalWorkSuitability_ OutWorkSuitability; + } p{ In, false, EPalWorkSuitability_::None }; + + if (!Call(STR("Check Valid Work Suitability"), p)) return std::nullopt; + return WorkSuitabilityCheck{ p.IsValid, p.OutWorkSuitability }; + } + + // ---- hook targets -------------------------------------------- + static auto BindFromHandlePath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalDetail.WBP_IngameMenu_PalBox_PalDetail_C:BindFromHandle"); + } + + static auto SetEmptyPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalDetail.WBP_IngameMenu_PalBox_PalDetail_C:SetEmpty"); + } + static auto UpdateStatusParameterPath() -> const TCHAR* + { + return STR("/Game/Pal/Blueprint/UI/UserInterface/IngameMenu/PalBox/WBP_IngameMenu_PalBox_PalDetail.WBP_IngameMenu_PalBox_PalDetail_C:Update Status Parameter"); + } + + struct BindFromHandleFrame { UObject* targetHandle; }; // UPalIndividualCharacterHandle* + struct UpdateStatusParameterFrame { UObject* individualParam; }; // UPalIndividualCharacterParameter* + + + + private: + auto WrapCanvas(const TCHAR* Name) const -> CanvasPanel + { + UObject* O = BoundObject(Name); + return O ? CanvasPanel{ O } : CanvasPanel{}; + } + auto WrapText(const TCHAR* Name) const -> TextBlock + { + UObject* O = BoundObject(Name); + return O ? TextBlock{ O } : TextBlock{}; + } + + protected: + PalDetailWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UserWidget(Obj, Expected, Tag) { + } + }; +} \ No newline at end of file diff --git a/Api/PanelSlot.hpp b/Api/PanelSlot.hpp new file mode 100644 index 0000000..289488d --- /dev/null +++ b/Api/PanelSlot.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "UmgTypes.hpp" +#include "UnrealWrapper.hpp" + +namespace PalUIExtension::Api +{ + // ------------------------------------------------------------------ + // UPanelSlot. + // + // Worth saying plainly: this class exposes ZERO UFUNCTIONs. Its entire + // surface is two Instanced UPROPERTYs, Parent and Content. So this + // wrapper cannot use ProcessEvent at all -- it exists only to give + // CanvasPanelSlot a place to inherit property access from, and to give + // you something typed to hold when AddChild() hands back a UPanelSlot* + // whose concrete type you do not know yet. + // + // If you were expecting a base class full of shared calls to inherit, + // that expectation does not survive contact with the engine here. The + // real shared behaviour lives in UnrealWrapper, not in UPanelSlot. + // ------------------------------------------------------------------ + class PanelSlot : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.PanelSlot"); } + static auto DebugTag() -> const TCHAR* { return STR("PanelSlot"); } + + PanelSlot() = default; + explicit PanelSlot(UObject* Obj) : PanelSlot(Obj, ClassOf(), DebugTag()) {} + + // Raw UObject* rather than wrappers: Parent is a UPanelWidget and + // Content is a UWidget, and wrapping either here would mean + // including headers that (transitively) include this one. + auto GetParentObject() const -> UObject* + { + auto Ptr = PropertyPtr(STR("Parent")); + return Ptr ? *Ptr : nullptr; + } + + auto GetContentObject() const -> UObject* + { + auto Ptr = PropertyPtr(STR("Content")); + return Ptr ? *Ptr : nullptr; + } + + protected: + PanelSlot(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UnrealWrapper(Obj, Expected, Tag) {} + }; +} diff --git a/Api/PanelWidget.hpp b/Api/PanelWidget.hpp new file mode 100644 index 0000000..8d19bd9 --- /dev/null +++ b/Api/PanelWidget.hpp @@ -0,0 +1,173 @@ +#pragma once + +#include + +#include "Widget.hpp" +#include "PanelSlot.hpp" + +namespace PalUIExtension::Api +{ + class PanelWidget : public Widget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.PanelWidget"); } + static auto DebugTag() -> const TCHAR* { return STR("PanelWidget"); } + + PanelWidget() = default; + explicit PanelWidget(UObject* Obj) + : Widget(Obj, ClassOf(), DebugTag()) {} + + auto GetChildrenCount() -> std::optional + { + return CallForReturn(STR("GetChildrenCount")); + } + auto HasAnyChildren() -> std::optional + { + return CallForReturn(STR("HasAnyChildren")); + } + + auto GetChildAt(int32 Index) -> Widget + { + struct P { int32 Index; UObject* ReturnValue; } p{ Index, nullptr }; + auto Result = CallForReturn(STR("GetChildAt"), p, &P::ReturnValue); + return (Result && *Result) ? Widget{ *Result } : Widget{}; + } + + auto GetChildIndex(UObject* Content) -> std::optional + { + struct P { UObject* Content; int32 ReturnValue; } p{ Content, -1 }; + return CallForReturn(STR("GetChildIndex"), p, &P::ReturnValue); + } + + auto HasChild(UObject* Content) -> std::optional + { + struct P { UObject* Content; bool ReturnValue; } p{ Content, false }; + return CallForReturn(STR("HasChild"), p, &P::ReturnValue); + } + + auto AddChild(UObject* Content) -> PanelSlot + { + struct P { UObject* Content; UObject* ReturnValue; } p{ Content, nullptr }; + auto Result = CallForReturn(STR("AddChild"), p, &P::ReturnValue); + return (Result && *Result) ? PanelSlot{ *Result } : PanelSlot{}; + } + + auto RemoveChild(UObject* Content) -> std::optional + { + struct P { UObject* Content; bool ReturnValue; } p{ Content, false }; + return CallForReturn(STR("RemoveChild"), p, &P::ReturnValue); + } + + auto RemoveChildAt(int32 Index) -> std::optional + { + struct P { int32 Index; bool ReturnValue; } p{ Index, false }; + return CallForReturn(STR("RemoveChildAt"), p, &P::ReturnValue); + } + + auto ClearChildren() -> bool { return CallVoid(STR("ClearChildren")); } + + // ------------------------------------------------------------------ + // Deliberately NOT a wrapper around the engine's GetAllChildren. + // + // That UFUNCTION returns TArray by value. ProcessEvent + // constructs that array into the frame with the engine's allocator + // and then hands you ownership of the allocation. Getting that + // wrong is either a leak on every call or a free across a module + // boundary, and neither shows up as a crash near the cause. + // + // The count + index loop is two extra ProcessEvent calls per child + // and zero ownership questions. If you are calling this per-frame + // over a large panel, cache the result; if you are calling it once + // in a Construct hook, the cost is irrelevant. + // ------------------------------------------------------------------ + auto GetAllChildren() -> std::vector + { + std::vector Children; + + auto Count = GetChildrenCount(); + if (!Count || *Count <= 0) return Children; + + Children.reserve(static_cast(*Count)); + for (int32 i = 0; i < *Count; ++i) + { + Widget Child = GetChildAt(i); + if (Child) Children.push_back(Child); + } + return Children; + } + + // ------------------------------------------------------------------ + // Children WITHOUT ProcessEvent. + // + // GetAllChildren() above costs 1 + N ProcessEvent calls. That is + // fine once in a Construct hook and not fine as the inner loop of a + // whole-tree walk, where it becomes 1 + N per PANEL. + // + // UPanelWidget::Slots is a TArray UPROPERTY (confirmed + // in the dump: UHTHeaderDump/UMG/Public/PanelWidget.h line 14), and + // UPanelSlot::Content is a UPROPERTY too. So the full child list is + // reachable as pure memory reads. Slot order is child order -- the + // engine's own GetChildAt(i) is Slots[i]->Content. + // + // Nothing here is copied out of engine memory except the pointers + // themselves, which is what makes reading the TArray safe; see the + // TArrayRead_ comment in UmgTypes.hpp for where that line is. + // + // Returns false if Slots could not be read or looked implausible, + // WITHOUT touching Out -- so a caller can fall back to + // GetAllChildren() and know it isn't double-appending. + // ------------------------------------------------------------------ + static constexpr int32 kMaxPlausibleChildren = 4096; + + auto TryGetChildObjects(std::vector& Out) const -> bool + { + auto* Slots = TryPropertyPtr>(STR("Slots")); + if (!Slots) return false; + + if (!Slots->IsPlausible(kMaxPlausibleChildren)) + { + Output::send( + STR("[{}] 'Slots' does not look like a live TArray ") + STR("(Num={}, Max={}, Data={}) -- falling back to ProcessEvent\n"), + m_tag, Slots->Num, Slots->Max, + Slots->Data ? STR("set") : STR("null")); + return false; + } + + Out.reserve(Out.size() + static_cast(Slots->Num)); + for (int32 i = 0; i < Slots->Num; ++i) + { + UObject* Slot = Slots->Data[i]; + if (!Slot) continue; + + // Read Content off the slot directly rather than through the + // PanelSlot wrapper: the wrapper's IsA check would log on + // anything unexpected, and this runs once per node of a + // whole-tree walk. + if (UObject* Content = RawObject{ Slot, STR("PanelSlot") }.TryObjectProperty(STR("Content"))) + { + Out.push_back(Content); + } + } + return true; + } + + // Property read first, ProcessEvent second. Use this anywhere you + // just want the children and don't care which route produced them. + auto GetChildObjects() -> std::vector + { + std::vector Out; + if (TryGetChildObjects(Out)) return Out; + + for (Widget& Child : GetAllChildren()) + { + if (UObject* O = Child.Get()) Out.push_back(O); + } + return Out; + } + + protected: + PanelWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : Widget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/SizeBox.hpp b/Api/SizeBox.hpp new file mode 100644 index 0000000..0addc21 --- /dev/null +++ b/Api/SizeBox.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include "PanelWidget.hpp" + +namespace PalUIExtension::Api +{ + // ------------------------------------------------------------------ + // USizeBox : public UContentWidget : public UPanelWidget. + // + // UContentWidget is not in this tree's header dump, so there is no + // Api::ContentWidget rung to inherit from. This derives from + // PanelWidget directly, skipping it. That is safe, not a shortcut: + // Invoke() dispatches through Target->GetFunctionByNameInChain() on + // the LIVE object's actual engine class chain (USizeBox -> + // UContentWidget -> UPanelWidget -> UWidget -> ...), never through + // this wrapper's C++ inheritance. Our wrapper hierarchy only + // determines which typed accessors are in scope at the call site; it + // has no effect on what ProcessEvent can reach. UContentWidget itself + // adds no BlueprintCallable surface beyond what UPanelWidget already + // gives you (its one member, GetContentSlot(), is a plain C++ helper + // on the engine side, not a UFUNCTION), so nothing is actually lost + // by skipping the rung -- there would be nothing to put in it. + // + // Every AddChild / GetChildAt / GetAllChildren / GetChildObjects call + // from PanelWidget works unchanged. SizeBox only adds the + // override/clear surface below. + // ------------------------------------------------------------------ + class SizeBox : public PanelWidget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.SizeBox"); } + static auto DebugTag() -> const TCHAR* { return STR("SizeBox"); } + + SizeBox() = default; + explicit SizeBox(UObject* Obj) + : PanelWidget(Obj, ClassOf(), DebugTag()) {} + + // ---- setters (write through the UFUNCTION so SynchronizeProperties runs) + + auto SetWidthOverride(float InWidthOverride) -> bool + { + struct P { float InWidthOverride; } p{ InWidthOverride }; + return Call(STR("SetWidthOverride"), p); + } + auto SetHeightOverride(float InHeightOverride) -> bool + { + struct P { float InHeightOverride; } p{ InHeightOverride }; + return Call(STR("SetHeightOverride"), p); + } + auto SetMinDesiredWidth(float InMinDesiredWidth) -> bool + { + struct P { float InMinDesiredWidth; } p{ InMinDesiredWidth }; + return Call(STR("SetMinDesiredWidth"), p); + } + auto SetMinDesiredHeight(float InMinDesiredHeight) -> bool + { + struct P { float InMinDesiredHeight; } p{ InMinDesiredHeight }; + return Call(STR("SetMinDesiredHeight"), p); + } + auto SetMaxDesiredWidth(float InMaxDesiredWidth) -> bool + { + struct P { float InMaxDesiredWidth; } p{ InMaxDesiredWidth }; + return Call(STR("SetMaxDesiredWidth"), p); + } + auto SetMaxDesiredHeight(float InMaxDesiredHeight) -> bool + { + struct P { float InMaxDesiredHeight; } p{ InMaxDesiredHeight }; + return Call(STR("SetMaxDesiredHeight"), p); + } + auto SetMinAspectRatio(float InMinAspectRatio) -> bool + { + struct P { float InMinAspectRatio; } p{ InMinAspectRatio }; + return Call(STR("SetMinAspectRatio"), p); + } + auto SetMaxAspectRatio(float InMaxAspectRatio) -> bool + { + struct P { float InMaxAspectRatio; } p{ InMaxAspectRatio }; + return Call(STR("SetMaxAspectRatio"), p); + } + + // ---- clear (each Set* above also flips its bOverride_ flag on; + // these are the only way to flip it back off -- there is no + // "SetWidthOverride with override=false" overload on the engine + // side, Clear* is a distinct UFUNCTION) + + auto ClearWidthOverride() -> bool { return CallVoid(STR("ClearWidthOverride")); } + auto ClearHeightOverride() -> bool { return CallVoid(STR("ClearHeightOverride")); } + auto ClearMinDesiredWidth() -> bool { return CallVoid(STR("ClearMinDesiredWidth")); } + auto ClearMinDesiredHeight() -> bool { return CallVoid(STR("ClearMinDesiredHeight")); } + auto ClearMaxDesiredWidth() -> bool { return CallVoid(STR("ClearMaxDesiredWidth")); } + auto ClearMaxDesiredHeight() -> bool { return CallVoid(STR("ClearMaxDesiredHeight")); } + auto ClearMinAspectRatio() -> bool { return CallVoid(STR("ClearMinAspectRatio")); } + auto ClearMaxAspectRatio() -> bool { return CallVoid(STR("ClearMaxAspectRatio")); } + + // ---- property reads: the float values ------------------------ + + auto GetWidthOverride() const -> std::optional { return PropertyValue(STR("WidthOverride")); } + auto GetHeightOverride() const -> std::optional { return PropertyValue(STR("HeightOverride")); } + auto GetMinDesiredWidth() const -> std::optional { return PropertyValue(STR("MinDesiredWidth")); } + auto GetMinDesiredHeight() const -> std::optional { return PropertyValue(STR("MinDesiredHeight")); } + auto GetMaxDesiredWidth() const -> std::optional { return PropertyValue(STR("MaxDesiredWidth")); } + auto GetMaxDesiredHeight() const -> std::optional { return PropertyValue(STR("MaxDesiredHeight")); } + auto GetMinAspectRatio() const -> std::optional { return PropertyValue(STR("MinAspectRatio")); } + auto GetMaxAspectRatio() const -> std::optional { return PropertyValue(STR("MaxAspectRatio")); } + + // ---- property reads: the override flags ----------------------- + // + // These are `uint8 bOverride_X : 1;` bitfields, EIGHT of them + // declared back to back on the header. That is exactly the shape + // where PropertyValue/PropertyPtr goes quietly + // wrong: the compiler is free to pack all eight into one shared + // byte, and a plain offset+size read would hand back that whole + // byte -- every flag OR'd together -- for a call asking about + // just one of them, with no warning, because the property exists + // and 1 byte does equal sizeof(uint8). + // + // PropertyBoolValue (UnrealWrapper.hpp) exists specifically for + // this: it reads FBoolProperty's own ByteOffset/FieldMask and + // tests the correct single bit rather than the storage byte. Use + // it for every bOverride_* flag; do not reach for PropertyValue + // here even though it would compile. + auto IsWidthOverridden() const -> std::optional { return PropertyBoolValue(STR("bOverride_WidthOverride")); } + auto IsHeightOverridden() const -> std::optional { return PropertyBoolValue(STR("bOverride_HeightOverride")); } + auto IsMinDesiredWidthOverridden() const -> std::optional { return PropertyBoolValue(STR("bOverride_MinDesiredWidth")); } + auto IsMinDesiredHeightOverridden() const -> std::optional { return PropertyBoolValue(STR("bOverride_MinDesiredHeight")); } + auto IsMaxDesiredWidthOverridden() const -> std::optional { return PropertyBoolValue(STR("bOverride_MaxDesiredWidth")); } + auto IsMaxDesiredHeightOverridden() const -> std::optional { return PropertyBoolValue(STR("bOverride_MaxDesiredHeight")); } + auto IsMinAspectRatioOverridden() const -> std::optional { return PropertyBoolValue(STR("bOverride_MinAspectRatio")); } + auto IsMaxAspectRatioOverridden() const -> std::optional { return PropertyBoolValue(STR("bOverride_MaxAspectRatio")); } + }; +} diff --git a/Api/SizeBoxSlot.hpp b/Api/SizeBoxSlot.hpp new file mode 100644 index 0000000..50bcf75 --- /dev/null +++ b/Api/SizeBoxSlot.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "PanelSlot.hpp" + +namespace PalUIExtension::Api +{ + class SizeBoxSlot : public PanelSlot + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.SizeBoxSlot"); } + static auto DebugTag() -> const TCHAR* { return STR("SizeBoxSlot"); } + + SizeBoxSlot() = default; + explicit SizeBoxSlot(UObject* Obj) + : PanelSlot(Obj, ClassOf(), DebugTag()) {} + + // ---- setters (write through the UFUNCTION so SynchronizeProperties runs) + + auto SetPadding(FMargin_ InPadding) -> bool + { + struct P { FMargin_ InPadding; } p{ InPadding }; + return Call(STR("SetPadding"), p); + } + auto SetHorizontalAlignment(EHorizontalAlignment_ InHorizontalAlignment) -> bool + { + struct P { EHorizontalAlignment_ InHorizontalAlignment; } p{ InHorizontalAlignment }; + return Call(STR("SetHorizontalAlignment"), p); + } + auto SetVerticalAlignment(EVerticalAlignment_ InVerticalAlignment) -> bool + { + struct P { EVerticalAlignment_ InVerticalAlignment; } p{ InVerticalAlignment }; + return Call(STR("SetVerticalAlignment"), p); + } + + // ---- property reads + + auto GetPadding() const -> std::optional { return PropertyValue(STR("Padding")); } + auto GetHorizontalAlignment()const -> std::optional { return PropertyValue(STR("HorizontalAlignment")); } + auto GetVerticalAlignment() const -> std::optional { return PropertyValue(STR("VerticalAlignment")); } + }; +} diff --git a/Api/TextBlock.hpp b/Api/TextBlock.hpp new file mode 100644 index 0000000..122f874 --- /dev/null +++ b/Api/TextBlock.hpp @@ -0,0 +1,289 @@ +#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; + } + }; +} diff --git a/Api/TextLayoutWidget.hpp b/Api/TextLayoutWidget.hpp new file mode 100644 index 0000000..b472753 --- /dev/null +++ b/Api/TextLayoutWidget.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "Widget.hpp" + +namespace PalUIExtension::Api +{ + // ------------------------------------------------------------------ + // UTextLayoutWidget. One UFUNCTION (SetJustification) and a handful of + // protected UPROPERTYs. + // + // "protected" is a C++ access specifier. It is invisible to reflection: + // the properties are in the class's FProperty chain either way, and + // PropertyPtr reaches them fine. Do not read protected as unreachable. + // + // AutoWrapText is uint8 : 1 and is therefore NOT exposed as a property + // read -- see the bitfield note in UnrealWrapper.hpp. UTextBlock has a + // SetAutoWrapText UFUNCTION; use that. + // ------------------------------------------------------------------ + class TextLayoutWidget : public Widget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.TextLayoutWidget"); } + static auto DebugTag() -> const TCHAR* { return STR("TextLayoutWidget"); } + + TextLayoutWidget() = default; + explicit TextLayoutWidget(UObject* Obj) + : Widget(Obj, ClassOf(), DebugTag()) {} + + auto SetJustification(ETextJustify_ InJustification) -> bool + { + struct P { ETextJustify_ InJustification; } p{ InJustification }; + return Call(STR("SetJustification"), p); + } + + // ---- property reads ------------------------------------------------- + auto GetJustification() const -> std::optional { return PropertyValue(STR("Justification")); } + auto GetWrappingPolicy() const -> std::optional{ return PropertyValue(STR("WrappingPolicy")); } + auto GetWrapTextAt() const -> std::optional { return PropertyValue(STR("WrapTextAt")); } + auto GetLineHeightPercentage()const -> std::optional { return PropertyValue(STR("LineHeightPercentage")); } + auto GetMargin() const -> std::optional { return PropertyValue(STR("Margin")); } + + // ---- property writes ------------------------------------------------ + // + // These have no UFUNCTION setter, so a direct write is the only + // option -- and it will NOT reach the Slate widget until something + // else triggers SynchronizeProperties. Set them before the widget + // is added to a panel, or accept that they take effect late. + auto SetWrapTextAt(float Value) -> bool + { + if (float* P = PropertyPtr(STR("WrapTextAt"))) { *P = Value; return true; } + return false; + } + auto SetLineHeightPercentage(float Value) -> bool + { + if (float* P = PropertyPtr(STR("LineHeightPercentage"))) { *P = Value; return true; } + return false; + } + auto SetMargin(FMargin_ Value) -> bool + { + if (FMargin_* P = PropertyPtr(STR("Margin"))) { *P = Value; return true; } + return false; + } + + protected: + TextLayoutWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : Widget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/UmgTypes.hpp b/Api/UmgTypes.hpp new file mode 100644 index 0000000..023c7cc --- /dev/null +++ b/Api/UmgTypes.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include + +namespace PalUIExtension::Api +{ + using namespace RC; + using namespace RC::Unreal; + + template + struct TArrayRead_ + { + T* Data{}; + int32 Num{}; + int32 Max{}; + + auto IsPlausible(int32 SanityLimit) const -> bool + { + if (Num < 0 || Max < 0 || Num > Max) return false; + if (Num > SanityLimit) return false; + return Num == 0 || Data != nullptr; + } + }; + struct FVector2D_ { double X{}; double Y{}; }; + struct FVector_ { double X{}; double Y{}; double Z{}; }; + struct FMargin_ { float Left{}; float Top{}; float Right{}; float Bottom{}; }; + struct FAnchors_ { FVector2D_ Minimum{}; FVector2D_ Maximum{}; }; + + // GUESS (field order). + struct FAnchorData_ { FMargin_ Offsets{}; FAnchors_ Anchors{}; FVector2D_ Alignment{}; }; + + // GUESS (field order). Engine source order is Translation, Scale, Shear, Angle. + struct FWidgetTransform_ + { + FVector2D_ Translation{}; + FVector2D_ Scale{ 1.0, 1.0 }; + FVector2D_ Shear{}; + float Angle{}; + }; + + + + enum class ESlateVisibility_ : uint8 + { + Visible = 0, + Collapsed = 1, + Hidden = 2, + HitTestInvisible = 3, + SelfHitTestInvisible = 4, + }; + + enum class EWidgetClipping_ : uint8 + { + Inherit = 0, + ClipToBounds = 1, + ClipToBoundsWithoutIntersecting = 2, + ClipToBoundsAlways = 3, + OnDemand = 4, + }; + + enum class EUINavigation_ : uint8 + { + Left = 0, + Right = 1, + Up = 2, + Down = 3, + Next = 4, + Previous = 5, + Num = 6, + Invalid = 7, + }; + + enum class EUINavigationRule_ : uint8 + { + Escape = 0, + Explicit = 1, + Wrap = 2, + Stop = 3, + Custom = 4, + CustomBoundary = 5, + Invalid = 6, + }; + + using EMouseCursorByte_ = uint8; + + enum class EHorizontalAlignment_ : uint8 + { + Fill = 0, + Left = 1, + Center = 2, + Right = 3, + }; + + enum class EVerticalAlignment_ : uint8 + { + Fill = 0, + Top = 1, + Center = 2, + Bottom = 3, + }; + + struct FLinearColor_ { float R{}; float G{}; float B{}; float A{ 1.0f }; }; + + struct FSlateColor_ + { + FLinearColor_ SpecifiedColor{}; + uint8 ColorUseRule{ 0 }; // UseColor_Specified + }; + + enum class ESlateColorStylingMode_ : uint8 + { + UseColor_Specified = 0, + UseColor_Specified_Link = 1, + UseColor_Foreground = 2, + UseColor_Foreground_Subdued = 3, + }; + + enum class ETextTransformPolicy_ : uint8 + { + None = 0, + ToLower = 1, + ToUpper = 2, + }; + + enum class ETextOverflowPolicy_ : uint8 + { + Clip = 0, + Ellipsis = 1, + }; + + enum class ETextWrappingPolicy_ : uint8 + { + DefaultWrapping = 0, + AllowPerCharacterWrapping = 1, + }; + + // TEnumAsByte + enum class ETextJustify_ : uint8 + { + Left = 0, + Center = 1, + Right = 2, + }; + + enum class ESlateSizeRule_ : uint8 + { + Automatic = 0, + Fill = 1, + }; + + struct FSlateChildSize_ + { + float Value{}; + ESlateSizeRule_ SizeRule{ ESlateSizeRule_::Automatic }; + }; +} diff --git a/Api/UnrealWrapper.hpp b/Api/UnrealWrapper.hpp new file mode 100644 index 0000000..eba84c1 --- /dev/null +++ b/Api/UnrealWrapper.hpp @@ -0,0 +1,476 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +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 + inline auto Api() -> Reflection::UnrealApiCall& + { + static Reflection::UnrealApiCall 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 + inline auto ClassOf() -> UClass* + { + static Reflection::UnrealObjectRef Ref{ T::ClassPath(), T::DebugTag() }; + return static_cast(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( + STR("[{}] '{}' is not in the chain of {}\n"), + m_tag, FunctionName, m_obj->GetFullName()); + return; + } + + Output::send( + STR("[{}] {} -- frame is {} bytes:\n"), + m_tag, FunctionName, Fn->GetStructureSize()); + + for (FProperty* Prop : Fn->ForEachProperty()) + { + if (!Prop) continue; + Output::send( + 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( + STR("[{}] no class on {}\n"), m_tag, m_obj->GetFullName()); + return; + } + + Output::send( + STR("[{}] properties of {}:\n"), m_tag, m_obj->GetFullName()); + + for (FProperty* Prop : Cls->ForEachPropertyInChain()) + { + if (!Prop) continue; + Output::send( + 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( + 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( + STR("[{}] class unresolved -- accepting {} unchecked\n"), + m_tag, Obj->GetFullName()); + m_obj = Obj; + return; + } + + if (!Obj->IsA(Expected)) + { + Output::send( + 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 will log if the + // engine function actually has a ReturnValue property, which is the + // signal that you meant to use CallForReturn. + template + auto Call(const TCHAR* FunctionName, ParamsStruct& Params) const -> bool + { + if (!m_obj) + { + Output::send( + STR("[{}] '{}' on an invalid wrapper\n"), m_tag, FunctionName); + return false; + } + return Detail::Api().Invoke(m_obj, FunctionName, Params); + } + + auto CallVoid(const TCHAR* FunctionName) const -> bool + { + NoParams p{}; + return Call(FunctionName, p); + } + + // Zero-parameter getter. + template + auto CallForReturn(const TCHAR* FunctionName) const -> std::optional + { + struct Params { R ReturnValue; }; + Params p{}; + return CallForReturn(FunctionName, p, &Params::ReturnValue); + } + + // Getter with parameters. ReturnValue must be the LAST member of + // ParamsStruct, matching the engine's frame layout. + template + auto CallForReturn(const TCHAR* FunctionName, ParamsStruct& Params, + R ParamsStruct::* ReturnMember) const -> std::optional + { + if (!m_obj) + { + Output::send( + STR("[{}] '{}' on an invalid wrapper\n"), m_tag, FunctionName); + return std::nullopt; + } + return Detail::Api().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 + auto PropertyPtr(const TCHAR* PropertyName) const -> T* + { + if (!m_obj) return nullptr; + + FProperty* Prop = m_obj->GetPropertyByNameInChain(PropertyName); + if (!Prop) + { + Output::send( + STR("[{}] property '{}' not found on {}\n"), + m_tag, PropertyName, m_obj->GetFullName()); + return nullptr; + } + + if (static_cast(Prop->GetSize()) != sizeof(T)) + { + Output::send( + STR("[{}] property '{}' is {} bytes engine-side, read as {} bytes\n"), + m_tag, PropertyName, Prop->GetSize(), sizeof(T)); + return nullptr; + } + + return reinterpret_cast( + reinterpret_cast(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 + 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(Prop->GetSize()) != sizeof(T)) + { + Output::send( + STR("[{}] property '{}' is {} bytes engine-side, read as {} bytes\n"), + m_tag, PropertyName, Prop->GetSize(), sizeof(T)); + return nullptr; + } + + return reinterpret_cast( + reinterpret_cast(m_obj) + Prop->GetOffset_Internal()); + } + + template + auto PropertyValue(const TCHAR* PropertyName) const -> std::optional + { + if (T* Ptr = PropertyPtr(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(PropertyName); + return Ptr ? *Ptr : nullptr; + } + + // ------------------------------------------------------------------ + // For `uint8 bFoo : 1;` UPROPERTY bitfields specifically. Do NOT + // reach for PropertyValue/PropertyPtr 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 + // 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 + { + if (!m_obj) return std::nullopt; + + FProperty* Prop = m_obj->GetPropertyByNameInChain(PropertyName); + if (!Prop) + { + Output::send( + STR("[{}] property '{}' not found on {}\n"), + m_tag, PropertyName, m_obj->GetFullName()); + return std::nullopt; + } + + FBoolProperty* BoolProp = CastField(Prop); + if (!BoolProp) + { + Output::send( + STR("[{}] property '{}' is not a bool/bitfield property on {} -- ") + STR("use PropertyValue for a plain field instead\n"), + m_tag, PropertyName, m_obj->GetFullName()); + return std::nullopt; + } + + const uint8* Base = reinterpret_cast(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() logs once on the failed resolve either way. + // ------------------------------------------------------------------ + template + inline auto ObjectIsA(UObject* Obj) -> bool + { + if (!Obj) return false; + UClass* Cls = ClassOf(); + return Cls && Obj->IsA(Cls); + } +} \ No newline at end of file diff --git a/Api/UserWidget.hpp b/Api/UserWidget.hpp new file mode 100644 index 0000000..5dd8179 --- /dev/null +++ b/Api/UserWidget.hpp @@ -0,0 +1,175 @@ +#pragma once + +#include + +#include "Widget.hpp" +#include "WidgetTree.hpp" + +namespace PalUIExtension::Api +{ + // ------------------------------------------------------------------ + // UUserWidget. Base for every WBP_..._C generated class. + // + // NAMESPACE: this file previously declared itself in + // PalStatusOverlay::Api while every other header in the tree is in + // PalUIExtension::Api. Nothing included it, so nothing caught it -- the + // first #include from dllmain.cpp would have failed to find any of + // these types. Fixed; if you have local code referring to + // PalStatusOverlay::Api::UserWidget, that is why it no longer resolves. + // Static/WidgetRegistry.hpp had the same split and is fixed to match. + // + // GetWidgetFromName is NOT available on this build. It does not appear + // in the UserWidget.h you pulled from UMG.zip, which is UHT's own + // generated listing of every UFUNCTION this class has. Stock UE ships + // it BlueprintPure; this build either stripped it or the dump is + // incomplete. + // + // That is no longer an obstacle rather than a caveat: FindWidget below + // walks the widget tree instead of calling it. See WidgetTree.hpp for + // why walking beats both the missing UFUNCTION and the global-object + // scan that UnrealApiCall::FindInstance does. + // ------------------------------------------------------------------ + class UserWidget : public Widget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.UserWidget"); } + static auto DebugTag() -> const TCHAR* { return STR("UserWidget"); } + + UserWidget() = default; + explicit UserWidget(UObject* Obj) + : Widget(Obj, ClassOf(), DebugTag()) {} + + // ---- the widget tree ------------------------------------------ + // + // WidgetTree is a plain UPROPERTY (Transient/DuplicateTransient, + // not a bitfield) -- direct property read, no ProcessEvent frame. + auto GetWidgetTree() const -> WidgetTree + { + return WidgetTree::FromUserWidget(m_obj); + } + + // Find a widget by its designer name anywhere in THIS widget's + // tree. Replaces the GetWidgetFromName call this class used to make + // opportunistically. + // + // Scope defaults to ThisTreeOnly -- names are unique per tree, not + // globally, and a nested WBP has its own "Text_PalName" just like + // its parent does. See WidgetTree::EScope. + auto FindWidget(const TCHAR* Name, + WidgetTree::EScope Scope = WidgetTree::EScope::ThisTreeOnly) -> Widget + { + return GetWidgetTree().Find(Name, Scope); + } + + auto FindWidgetObject(const TCHAR* Name, + WidgetTree::EScope Scope = WidgetTree::EScope::ThisTreeOnly) -> UObject* + { + return GetWidgetTree().FindObjectByName(Name, Scope); + } + + // Typed variant: UserWidget{W}.FindWidgetAs(STR("Canvas_Base")) + template + auto FindWidgetAs(const TCHAR* Name, + WidgetTree::EScope Scope = WidgetTree::EScope::ThisTreeOnly) -> T + { + return GetWidgetTree().FindAs(Name, Scope); + } + + // ---- bound widget properties ------------------------------------ + // + // The OTHER way to reach a child, and the cheaper one when it + // applies. UMG promotes any widget marked "Is Variable" in the + // designer to a UPROPERTY on the generated class, so the pointer + // sits at a fixed offset -- no walk at all. PalDetailWidget's + // CanvasBase()/TextPalName()/etc. are exactly this. + // + // The catch, and the reason FindWidget still earns its place: this + // only works for widgets that were marked as variables. Anything + // left unpromoted (most auto-named CanvasPanel_N containers) has no + // property and can only be found by walking. + auto BoundObject(const TCHAR* Name) const -> UObject* + { + return TryObjectProperty(Name); + } + + // Property first, tree walk second. Use this when you do not know + // whether a given child was promoted to a variable, and do not want + // to care. + auto ChildObject(const TCHAR* Name, + WidgetTree::EScope Scope = WidgetTree::EScope::ThisTreeOnly) -> UObject* + { + if (UObject* Bound = BoundObject(Name)) return Bound; + return FindWidgetObject(Name, Scope); + } + + template + auto Child(const TCHAR* Name, + WidgetTree::EScope Scope = WidgetTree::EScope::ThisTreeOnly) -> T + { + UObject* O = ChildObject(Name, Scope); + if (!O) + { + Output::send( + STR("[{}] no child named '{}' (neither a bound variable nor in the tree)\n"), + m_tag, Name); + return T{}; + } + if (!ObjectIsA(O)) + { + Output::send( + STR("[{}] child '{}' resolved to {}, which is not a {}\n"), + m_tag, Name, O->GetFullName(), T::DebugTag()); + return T{}; + } + return T{ O }; + } + + // ---- viewport lifecycle ------------------------------------------- + auto AddToViewport(int32 ZOrder) -> bool + { + struct P { int32 ZOrder; } p{ ZOrder }; + return Call(STR("AddToViewport"), p); + } + auto RemoveFromViewport() -> bool { return CallVoid(STR("RemoveFromViewport")); } + + auto GetIsVisible() -> std::optional { return CallForReturn(STR("GetIsVisible")); } + + // BlueprintImplementableEvent. Dispatch is by the object's own + // class chain regardless of native-vs-event, so this call works + // the same either way -- but if no Blueprint graph overrides it, + // the engine's default thunk hands back bool's zero value (false), + // not an error. A false here does not prove non-interactable. + auto IsInteractable() -> std::optional { return CallForReturn(STR("IsInteractable")); } + + // ---- cosmetic setters ---------------------------------------------- + auto SetPadding(FMargin_ InPadding) -> bool + { + struct P { FMargin_ InPadding; } p{ InPadding }; + return Call(STR("SetPadding"), p); + } + auto SetColorAndOpacity(FLinearColor_ InColorAndOpacity) -> bool + { + struct P { FLinearColor_ InColorAndOpacity; } p{ InColorAndOpacity }; + return Call(STR("SetColorAndOpacity"), p); + } + auto SetForegroundColor(FSlateColor_ InForegroundColor) -> bool + { + struct P { FSlateColor_ InForegroundColor; } p{ InForegroundColor }; + return Call(STR("SetForegroundColor"), p); + } + + // ---- plain properties ------------------------------------------ + auto GetPriority() const -> std::optional { return PropertyValue(STR("Priority")); } + auto GetColorAndOpacity()const -> std::optional{ return PropertyValue(STR("ColorAndOpacity")); } + auto GetPaddingValue() const -> std::optional { return PropertyValue(STR("Padding")); } + // bIsFocusable / bStopAction / bHasScriptImplementedTick / + // bHasScriptImplementedPaint are packed uint8:1 bitfields -- not + // exposed, same reasoning as UWidget::bIsEnabled in Widget.hpp. + // No UFUNCTION getter exists for any of them either; there is no + // clean substitute, only a byte read and a manual mask. + + protected: + UserWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : Widget(Obj, Expected, Tag) {} + }; +} diff --git a/Api/VerticalBox.hpp b/Api/VerticalBox.hpp new file mode 100644 index 0000000..d710f72 --- /dev/null +++ b/Api/VerticalBox.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "PanelWidget.hpp" +#include "VerticalBoxSlot.hpp" + +namespace PalUIExtension::Api +{ + // UVerticalBox : public UPanelWidget directly -- same shape as + // Overlay / HorizontalBox. + class VerticalBox : public PanelWidget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.VerticalBox"); } + static auto DebugTag() -> const TCHAR* { return STR("VerticalBox"); } + + VerticalBox() = default; + explicit VerticalBox(UObject* Obj) + : PanelWidget(Obj, ClassOf(), DebugTag()) {} + + auto AddChildToVerticalBox(UObject* Content) -> VerticalBoxSlot + { + struct P { UObject* Content; UObject* ReturnValue; } p{ Content, nullptr }; + auto Result = CallForReturn(STR("AddChildToVerticalBox"), p, &P::ReturnValue); + return (Result && *Result) ? VerticalBoxSlot{ *Result } : VerticalBoxSlot{}; + } + }; +} diff --git a/Api/VerticalBoxSlot.hpp b/Api/VerticalBoxSlot.hpp new file mode 100644 index 0000000..bc2e24e --- /dev/null +++ b/Api/VerticalBoxSlot.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "PanelSlot.hpp" + +namespace PalUIExtension::Api +{ + // Same four-member surface as HorizontalBoxSlot. Kept separate for + // the same reason -- see HorizontalBoxSlot.hpp. + class VerticalBoxSlot : public PanelSlot + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.VerticalBoxSlot"); } + static auto DebugTag() -> const TCHAR* { return STR("VerticalBoxSlot"); } + + VerticalBoxSlot() = default; + explicit VerticalBoxSlot(UObject* Obj) + : PanelSlot(Obj, ClassOf(), DebugTag()) {} + + // ---- setters (write through the UFUNCTION so SynchronizeProperties runs) + + auto SetSize(FSlateChildSize_ InSize) -> bool + { + struct P { FSlateChildSize_ InSize; } p{ InSize }; + return Call(STR("SetSize"), p); + } + auto SetPadding(FMargin_ InPadding) -> bool + { + struct P { FMargin_ InPadding; } p{ InPadding }; + return Call(STR("SetPadding"), p); + } + auto SetHorizontalAlignment(EHorizontalAlignment_ InHorizontalAlignment) -> bool + { + struct P { EHorizontalAlignment_ InHorizontalAlignment; } p{ InHorizontalAlignment }; + return Call(STR("SetHorizontalAlignment"), p); + } + auto SetVerticalAlignment(EVerticalAlignment_ InVerticalAlignment) -> bool + { + struct P { EVerticalAlignment_ InVerticalAlignment; } p{ InVerticalAlignment }; + return Call(STR("SetVerticalAlignment"), p); + } + + // ---- property reads + + auto GetSize() const -> std::optional { return PropertyValue(STR("Size")); } + auto GetPadding() const -> std::optional { return PropertyValue(STR("Padding")); } + auto GetHorizontalAlignment() const -> std::optional { return PropertyValue(STR("HorizontalAlignment")); } + auto GetVerticalAlignment() const -> std::optional { return PropertyValue(STR("VerticalAlignment")); } + }; +} diff --git a/Api/Widget.hpp b/Api/Widget.hpp new file mode 100644 index 0000000..d9f5dac --- /dev/null +++ b/Api/Widget.hpp @@ -0,0 +1,227 @@ +#pragma once + +#include +#include + +#include "UmgTypes.hpp" +#include "UnrealWrapper.hpp" + +namespace PalUIExtension::Api +{ + + class Widget : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.Widget"); } + static auto DebugTag() -> const TCHAR* { return STR("Widget"); } + + Widget() = default; + explicit Widget(UObject* Obj) : Widget(Obj, ClassOf(), DebugTag()) {} + + // ---- visibility / enable ------------------------------------------ + + auto SetVisibility(ESlateVisibility_ InVisibility) -> bool + { + struct P { ESlateVisibility_ InVisibility; } p{ InVisibility }; + return Call(STR("SetVisibility"), p); + } + auto GetVisibility() -> std::optional + { + return CallForReturn(STR("GetVisibility")); + } + + auto SetIsEnabled(bool bInIsEnabled) -> bool + { + struct P { bool bInIsEnabled; } p{ bInIsEnabled }; + return Call(STR("SetIsEnabled"), p); + } + auto GetIsEnabled() -> std::optional + { + return CallForReturn(STR("GetIsEnabled")); + } + + auto IsVisible() -> std::optional { return CallForReturn(STR("IsVisible")); } + auto IsRendered() -> std::optional { return CallForReturn(STR("IsRendered")); } + auto IsInViewport() -> std::optional { return CallForReturn(STR("IsInViewport")); } + auto IsHovered() -> std::optional { return CallForReturn(STR("IsHovered")); } + + // ---- render transform --------------------------------------------- + + auto SetRenderTransform(FWidgetTransform_ InTransform) -> bool + { + struct P { FWidgetTransform_ InTransform; } p{ InTransform }; + return Call(STR("SetRenderTransform"), p); + } + auto SetRenderTranslation(FVector2D_ Translation) -> bool + { + struct P { FVector2D_ Translation; } p{ Translation }; + return Call(STR("SetRenderTranslation"), p); + } + auto SetRenderScale(FVector2D_ Scale) -> bool + { + struct P { FVector2D_ Scale; } p{ Scale }; + return Call(STR("SetRenderScale"), p); + } + auto SetRenderShear(FVector2D_ Shear) -> bool + { + struct P { FVector2D_ Shear; } p{ Shear }; + return Call(STR("SetRenderShear"), p); + } + auto SetRenderTransformPivot(FVector2D_ Pivot) -> bool + { + struct P { FVector2D_ Pivot; } p{ Pivot }; + return Call(STR("SetRenderTransformPivot"), p); + } + auto SetRenderTransformAngle(float Angle) -> bool + { + struct P { float Angle; } p{ Angle }; + return Call(STR("SetRenderTransformAngle"), p); + } + auto GetRenderTransformAngle() -> std::optional + { + return CallForReturn(STR("GetRenderTransformAngle")); + } + + auto SetRenderOpacity(float InOpacity) -> bool + { + struct P { float InOpacity; } p{ InOpacity }; + return Call(STR("SetRenderOpacity"), p); + } + auto GetRenderOpacity() -> std::optional + { + return CallForReturn(STR("GetRenderOpacity")); + } + + // ---- clipping / cursor --------------------------------------------- + + auto SetClipping(EWidgetClipping_ InClipping) -> bool + { + struct P { EWidgetClipping_ InClipping; } p{ InClipping }; + return Call(STR("SetClipping"), p); + } + auto GetClipping() -> std::optional + { + return CallForReturn(STR("GetClipping")); + } + + auto SetCursor(EMouseCursorByte_ InCursor) -> bool + { + struct P { EMouseCursorByte_ InCursor; } p{ InCursor }; + return Call(STR("SetCursor"), p); + } + auto ResetCursor() -> bool { return CallVoid(STR("ResetCursor")); } + + // ---- tooltips ------------------------------------------------------- + // + // FText here is the same ownership question flagged in + // FTextFromString: you are handing a refcounted object into a frame + // the engine will destruct. Confirm who owns the allocation before + // shipping this on a hot path. + auto SetToolTipText(const FText& InToolTipText) -> bool + { + struct P { FText InToolTipText; } p{ InToolTipText }; + return Call(STR("SetToolTipText"), p); + } + auto SetToolTip(UObject* InWidget) -> bool + { + struct P { UObject* Widget; } p{ InWidget }; + return Call(STR("SetToolTip"), p); + } + + // ---- focus ---------------------------------------------------------- + + auto SetKeyboardFocus() -> bool { return CallVoid(STR("SetKeyboardFocus")); } + auto SetFocus() -> bool { return CallVoid(STR("SetFocus")); } + auto SetUserFocus(UObject* PlayerController) -> bool + { + struct P { UObject* PlayerController; } p{ PlayerController }; + return Call(STR("SetUserFocus"), p); + } + + auto HasKeyboardFocus() -> std::optional { return CallForReturn(STR("HasKeyboardFocus")); } + auto HasMouseCapture() -> std::optional { return CallForReturn(STR("HasMouseCapture")); } + auto HasAnyUserFocus() -> std::optional { return CallForReturn(STR("HasAnyUserFocus")); } + auto HasFocusedDescendants()-> std::optional { return CallForReturn(STR("HasFocusedDescendants")); } + + auto HasUserFocus(UObject* PlayerController) -> std::optional + { + struct P { UObject* PlayerController; bool ReturnValue; } p{ PlayerController, false }; + return CallForReturn(STR("HasUserFocus"), p, &P::ReturnValue); + } + auto HasUserFocusedDescendants(UObject* PlayerController) -> std::optional + { + struct P { UObject* PlayerController; bool ReturnValue; } p{ PlayerController, false }; + return CallForReturn(STR("HasUserFocusedDescendants"), p, &P::ReturnValue); + } + auto HasMouseCaptureByUser(int32 UserIndex, int32 PointerIndex) -> std::optional + { + struct P { int32 UserIndex; int32 PointerIndex; bool ReturnValue; } + p{ UserIndex, PointerIndex, false }; + return CallForReturn(STR("HasMouseCaptureByUser"), p, &P::ReturnValue); + } + + // ---- navigation ----------------------------------------------------- + + auto SetNavigationRuleBase(EUINavigation_ Direction, EUINavigationRule_ Rule) -> bool + { + struct P { EUINavigation_ Direction; EUINavigationRule_ Rule; } p{ Direction, Rule }; + return Call(STR("SetNavigationRuleBase"), p); + } + auto SetNavigationRuleExplicit(EUINavigation_ Direction, UObject* InWidget) -> bool + { + struct P { EUINavigation_ Direction; UObject* InWidget; } p{ Direction, InWidget }; + return Call(STR("SetNavigationRuleExplicit"), p); + } + auto SetNavigationRule(EUINavigation_ Direction, EUINavigationRule_ Rule, FName WidgetToFocus) -> bool + { + struct P { EUINavigation_ Direction; EUINavigationRule_ Rule; FName WidgetToFocus; } + p{ Direction, Rule, WidgetToFocus }; + return Call(STR("SetNavigationRule"), p); + } + auto SetAllNavigationRules(EUINavigationRule_ Rule, FName WidgetToFocus) -> bool + { + struct P { EUINavigationRule_ Rule; FName WidgetToFocus; } p{ Rule, WidgetToFocus }; + return Call(STR("SetAllNavigationRules"), p); + } + + // ---- layout / lifetime ---------------------------------------------- + + auto GetDesiredSize() -> std::optional + { + return CallForReturn(STR("GetDesiredSize")); + } + auto ForceLayoutPrepass() -> bool { return CallVoid(STR("ForceLayoutPrepass")); } + auto InvalidateLayoutAndVolatility() -> bool { return CallVoid(STR("InvalidateLayoutAndVolatility")); } + auto ForceVolatile(bool bForce) -> bool + { + struct P { bool bForce; } p{ bForce }; + return Call(STR("ForceVolatile"), p); + } + auto RemoveFromParent() -> bool { return CallVoid(STR("RemoveFromParent")); } + + // ---- relationships --------------------------------------------------- + // + // Returns the raw UPanelWidget*. Not a PanelWidget wrapper, because + // PanelWidget derives from this class and cannot be named here. + // Wrap it at the call site: PanelWidget{ *w.GetParentObject() }. + auto GetParentObject() -> std::optional + { + return CallForReturn(STR("GetParent")); + } + + // UWidget::Slot is a UPROPERTY with no getter. + auto GetSlotObject() const -> UObject* + { + auto Ptr = PropertyPtr(STR("Slot")); + return Ptr ? *Ptr : nullptr; + } + + auto GetOwningPlayer() -> std::optional { return CallForReturn(STR("GetOwningPlayer")); } + auto GetOwningLocalPlayer() -> std::optional { return CallForReturn(STR("GetOwningLocalPlayer")); } + auto GetGameInstance() -> std::optional { return CallForReturn(STR("GetGameInstance")); } + + protected: + Widget(UObject* Obj, UClass* Expected, const TCHAR* Tag) + : UnrealWrapper(Obj, Expected, Tag) {} + }; +} diff --git a/Api/WidgetBlueprintGeneratedClass.hpp b/Api/WidgetBlueprintGeneratedClass.hpp new file mode 100644 index 0000000..541e7ed --- /dev/null +++ b/Api/WidgetBlueprintGeneratedClass.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +namespace PalUIExtension::Api +{ + // ================================================================== + // UWidgetBlueprintGeneratedClass -- the CLASS object behind every WBP_... + // ================================================================== + class WidgetBlueprintGeneratedClass : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.WidgetBlueprintGeneratedClass"); } + static auto DebugTag() -> const TCHAR* { return STR("WBPGeneratedClass"); } + + WidgetBlueprintGeneratedClass() = default; + + explicit WidgetBlueprintGeneratedClass(UObject* Obj) : UnrealWrapper(Obj, ClassOf(), DebugTag()) {} + + static auto Of(UObject* Instance) -> WidgetBlueprintGeneratedClass + { + if (!Instance) return WidgetBlueprintGeneratedClass{}; + + UClass* Cls = Instance->GetClassPrivate(); + if (!Cls) return WidgetBlueprintGeneratedClass{}; + + UClass* Expected = ClassOf(); + if (!Expected || !Cls->IsA(Expected)) return WidgetBlueprintGeneratedClass{}; + + return WidgetBlueprintGeneratedClass{ static_cast(Cls) }; + } + + auto TemplateTree() const -> WidgetTree + { + UObject* Tree = TryObjectProperty(STR("WidgetTree")); + return Tree ? WidgetTree{ Tree } : WidgetTree{}; + } + + auto TemplateTreeObject() const -> UObject* + { + return TryObjectProperty(STR("WidgetTree")); + } + }; +} \ No newline at end of file diff --git a/Api/WidgetLayoutLibrary.hpp b/Api/WidgetLayoutLibrary.hpp new file mode 100644 index 0000000..9e5bcb0 --- /dev/null +++ b/Api/WidgetLayoutLibrary.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include + +#include + +#include "UmgTypes.hpp" +#include "UnrealWrapper.hpp" +#include "PanelSlot.hpp" +#include "CanvasPanelSlot.hpp" + +namespace PalUIExtension::Api +{ + + class WidgetLayoutLibrary : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.WidgetLayoutLibrary"); } + static auto CDOPath() -> const TCHAR* { return STR("/Script/UMG.Default__WidgetLayoutLibrary"); } + static auto DebugTag() -> const TCHAR* { return STR("WidgetLayoutLibrary"); } + + static auto Instance() -> WidgetLayoutLibrary& + { + static WidgetLayoutLibrary Singleton{}; + return Singleton; + } + + // ---- slot casts --------------------------------------------------- + + auto SlotAsCanvasSlot(UObject* Widget) -> CanvasPanelSlot + { + return GetSingleton(STR("SlotAsCanvasSlot"), Widget); + } + auto SlotAsWrapBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsWrapBoxSlot"), Widget); } + auto SlotAsWidgetSwitcherSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsWidgetSwitcherSlot"), Widget); } + auto SlotAsVerticalBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsVerticalBoxSlot"), Widget); } + auto SlotAsUniformGridSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsUniformGridSlot"), Widget); } + auto SlotAsSizeBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsSizeBoxSlot"), Widget); } + auto SlotAsScrollBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsScrollBoxSlot"), Widget); } + auto SlotAsScaleBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsScaleBoxSlot"), Widget); } + auto SlotAsSafeBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsSafeBoxSlot"), Widget); } + auto SlotAsOverlaySlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsOverlaySlot"), Widget); } + auto SlotAsHorizontalBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsHorizontalBoxSlot"), Widget); } + auto SlotAsGridSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsGridSlot"), Widget); } + auto SlotAsBorderSlot(UObject* Widget) -> PanelSlot { return GetSingleton(STR("SlotAsBorderSlot"), Widget); } + + // ---- viewport / mouse ---------------------------------------------- + + auto RemoveAllWidgets(UObject* WorldContextObject) -> bool + { + struct P { UObject* WorldContextObject; } p{ WorldContextObject }; + return Call(STR("RemoveAllWidgets"), p); + } + + auto GetViewportSize(UObject* WorldContextObject) -> std::optional + { + struct P { UObject* WorldContextObject; FVector2D_ ReturnValue; } p{ WorldContextObject, {} }; + return CallForReturn(STR("GetViewportSize"), p, &P::ReturnValue); + } + auto GetViewportScale(UObject* WorldContextObject) -> std::optional + { + struct P { UObject* WorldContextObject; float ReturnValue; } p{ WorldContextObject, 0.f }; + return CallForReturn(STR("GetViewportScale"), p, &P::ReturnValue); + } + auto GetMousePositionOnViewport(UObject* WorldContextObject) -> std::optional + { + struct P { UObject* WorldContextObject; FVector2D_ ReturnValue; } p{ WorldContextObject, {} }; + return CallForReturn(STR("GetMousePositionOnViewport"), p, &P::ReturnValue); + } + auto GetMousePositionOnPlatform() -> std::optional + { + return CallForReturn(STR("GetMousePositionOnPlatform")); + } + + struct MousePositionDPI { float X{}; float Y{}; }; + auto GetMousePositionScaledByDPI(UObject* Player) -> std::optional + { + struct P { UObject* Player; float LocationX; float LocationY; bool ReturnValue; } + p{ Player, 0.f, 0.f, false }; + if (!Call(STR("GetMousePositionScaledByDPI"), p) || !p.ReturnValue) return std::nullopt; + return MousePositionDPI{ p.LocationX, p.LocationY }; + } + + auto ProjectWorldLocationToWidgetPosition(UObject* PlayerController, FVector_ WorldLocation, bool bPlayerViewportRelative) -> std::optional + { + struct P + { + UObject* PlayerController; + FVector_ WorldLocation; + FVector2D_ ScreenPosition; + bool bPlayerViewportRelative; + bool ReturnValue; + } p{ PlayerController, WorldLocation, {}, bPlayerViewportRelative, false }; + + if (!Call(STR("ProjectWorldLocationToWidgetPosition"), p) || !p.ReturnValue) return std::nullopt; + return p.ScreenPosition; + } + + private: + WidgetLayoutLibrary() : UnrealWrapper(ResolveCDO(), ClassOf(), DebugTag()) {} + + WidgetLayoutLibrary(const WidgetLayoutLibrary&) = delete; + auto operator=(const WidgetLayoutLibrary&) -> WidgetLayoutLibrary& = delete; + + static auto ResolveCDO() -> UObject* + { + static Reflection::UnrealObjectRef Ref{ CDOPath(), DebugTag() }; + return Ref.Get(); + } + + auto CallStaticObjectGetter(const TCHAR* FunctionName, UObject* InObject) -> UObject* + { + struct P { UObject* InObject; UObject* ReturnValue; } p{ InObject, nullptr }; + auto Result = CallForReturn(FunctionName, p, &P::ReturnValue); + return (Result && *Result) ? *Result : nullptr; + } + + template + auto GetSingleton(const TCHAR* FunctionName, UObject* InObject) -> T + { + UObject* Obj = CallStaticObjectGetter(FunctionName, InObject); + return Obj ? T{ Obj } : T{}; + } + }; +} diff --git a/Api/WidgetTree.hpp b/Api/WidgetTree.hpp new file mode 100644 index 0000000..f0fb6ed --- /dev/null +++ b/Api/WidgetTree.hpp @@ -0,0 +1,349 @@ +#pragma once + +#include +#include + +#include + +#include "UnrealWrapper.hpp" +#include "Widget.hpp" +#include "PanelWidget.hpp" + +namespace PalUIExtension::Api +{ + + class WidgetTree : public UnrealWrapper + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.WidgetTree"); } + static auto DebugTag() -> const TCHAR* { return STR("WidgetTree"); } + + WidgetTree() = default; + explicit WidgetTree(UObject* Obj) : UnrealWrapper(Obj, ClassOf(), DebugTag()) {} + + static auto FromUserWidget(UObject* UserWidget) -> WidgetTree + { + if (!UserWidget) return WidgetTree{}; + + UObject* Tree = RawObject{ UserWidget, STR("UserWidget") }.TryObjectProperty(STR("WidgetTree")); + return Tree ? WidgetTree{ Tree } : WidgetTree{}; + } + + // The tree a widget LIVES IN. + static auto ContainingTree(UObject* AnyWidget) -> WidgetTree + { + if (!AnyWidget) return WidgetTree{}; + + UClass* TreeClass = ClassOf(); + if (!TreeClass) return WidgetTree{}; // ClassOf already logged + + if (UObject* Tree = AnyWidget->GetTypedOuter(TreeClass)) + { + return WidgetTree{ Tree }; + } + + UObject* Outer = AnyWidget->GetOuterPrivate(); + for (int32 Hops = 0; Outer && Hops < kMaxOuterHops; ++Hops) + { + if (Outer->IsA(TreeClass)) return WidgetTree{ Outer }; + Outer = Outer->GetOuterPrivate(); + } + return WidgetTree{}; + } + + static auto FromWidget(UObject* AnyWidget) -> WidgetTree + { + if (WidgetTree Owned = FromUserWidget(AnyWidget)) return Owned; + return ContainingTree(AnyWidget); + } + + // ---- root --------------------------------------------------------- + + auto GetRootWidgetObject() const -> UObject* + { + auto Ptr = PropertyPtr(STR("RootWidget")); + return Ptr ? *Ptr : nullptr; + } + + auto Root() const -> Widget + { + UObject* R = GetRootWidgetObject(); + return R ? Widget{ R } : Widget{}; + } + + // ================================================================== + // TRAVERSAL + // ================================================================== + + enum class EScope : uint8 + { + ThisTreeOnly, + IncludeNestedUserWidgets, + }; + + // What a visitor wants to happen next. + enum class EVisit : uint8 + { + Continue, // descend into this node's children + SkipChildren, // this node is fine, its subtree is not interesting + Stop, // end the walk entirely + }; + + static constexpr int32 kDefaultNodeBudget = 8192; + static constexpr int32 kMaxDepth = 64; + static constexpr int32 kMaxOuterHops = 32; + + template + auto ForEach(Fn&& Visit, EScope Scope = EScope::ThisTreeOnly, int32 NodeBudget = kDefaultNodeBudget) -> bool + { + WalkState State{ Scope, NodeBudget, false, false }; + WalkNode(GetRootWidgetObject(), 0, State, Visit); + + if (State.bExhausted) + { + Output::send( + STR("[WidgetTree] walk aborted -- exceeded {} nodes or depth {}. ") + STR("Results are incomplete.\n"), NodeBudget, kMaxDepth); + } + return !State.bExhausted; + } + + // ================================================================== + // SEARCH + // ================================================================== + + auto FindObjectByName(FName Name, EScope Scope = EScope::ThisTreeOnly) -> UObject* + { + if (Name.IsNone()) return nullptr; + + UObject* Found = nullptr; + ForEach([&](UObject* Node, int32) -> EVisit { + if (Node->GetNamePrivate() == Name) + { + Found = Node; + return EVisit::Stop; + } + return EVisit::Continue; + }, Scope); + + return Found; + } + + auto FindObjectByName(const TCHAR* Name, EScope Scope = EScope::ThisTreeOnly) -> UObject* + { + if (!Name) return nullptr; + return FindObjectByName(FName(Name, FNAME_Find), Scope); + } + + // Same search, wrapped. Invalid Widget on a miss. + auto Find(const TCHAR* Name, EScope Scope = EScope::ThisTreeOnly) -> Widget + { + UObject* O = FindObjectByName(Name, Scope); + return O ? Widget{ O } : Widget{}; + } + + template + auto FindAs(const TCHAR* Name, EScope Scope = EScope::ThisTreeOnly) -> T + { + UObject* O = FindObjectByName(Name, Scope); + if (!O) + { + Output::send( + STR("[WidgetTree] no widget named '{}' in this tree\n"), Name); + return T{}; + } + + if (!ObjectIsA(O)) + { + Output::send( + STR("[WidgetTree] '{}' resolved to {}, which is not a {}\n"), + Name, O->GetFullName(), T::DebugTag()); + return T{}; + } + return T{ O }; + } + + template + auto FindFirstOfType(EScope Scope = EScope::ThisTreeOnly) -> T + { + UClass* Cls = ClassOf(); + if (!Cls) return T{}; + + UObject* Found = nullptr; + ForEach([&](UObject* Node, int32) -> EVisit + { + if (Node->IsA(Cls)) { Found = Node; return EVisit::Stop; } + return EVisit::Continue; + }, Scope); + + return Found ? T{ Found } : T{}; + } + + // Every widget of a given wrapper type, in traversal order. + template + auto FindAllOfType(EScope Scope = EScope::ThisTreeOnly) -> std::vector + { + std::vector Out; + + UClass* Cls = ClassOf(); + if (!Cls) return Out; + + ForEach([&](UObject* Node, int32) -> EVisit + { + if (Node->IsA(Cls)) Out.emplace_back(Node); + return EVisit::Continue; + }, Scope); + + return Out; + } + + // Flat list of every widget object the walk reaches. + auto AllWidgetObjects(EScope Scope = EScope::ThisTreeOnly) -> std::vector + { + std::vector Out; + ForEach([&](UObject* Node, int32) -> EVisit + { + Out.push_back(Node); + return EVisit::Continue; + }, Scope); + return Out; + } + + // ================================================================== + // DIAGNOSTICS + // ================================================================== + + auto Dump(EScope Scope = EScope::IncludeNestedUserWidgets) -> void + { + if (!m_obj) + { + Output::send(STR("[WidgetTree] Dump on an invalid tree\n")); + return; + } + + Output::send( + STR("[WidgetTree] dump of {}\n"), m_obj->GetFullName()); + + int32 Count = 0; + ForEach([&](UObject* Node, int32 Depth) -> EVisit + { + ++Count; + + std::basic_string Indent; + Indent.reserve(static_cast(Depth) * 2); + for (int32 i = 0; i < Depth; ++i) Indent += STR(" "); + + UClass* Cls = Node->GetClassPrivate(); + std::basic_string ClassName = + Cls ? Cls->GetName() : std::basic_string(STR("")); + + if (ObjectIsA(Node)) + { + Output::send( + STR(" {}{} [{}] ({} children)\n"), + Indent.c_str(), Node->GetName().c_str(), ClassName.c_str(), + static_cast(PanelWidget{ Node }.GetChildObjects().size())); + } + else + { + Output::send( + STR(" {}{} [{}]\n"), + Indent.c_str(), Node->GetName().c_str(), ClassName.c_str()); + } + return EVisit::Continue; + }, Scope); + + Output::send(STR("[WidgetTree] {} widgets\n"), Count); + } + + // ================================================================== + // CONSTRUCTION + // ================================================================== + + auto ConstructWidget(UClass* WidgetClass, const TCHAR* InstanceName) -> UObject* + { + if (!m_obj) + { + Output::send( + STR("[WidgetTree] ConstructWidget on an invalid tree\n")); + return nullptr; + } + if (!WidgetClass) + { + Output::send( + STR("[WidgetTree] ConstructWidget: null class\n")); + return nullptr; + } + + FStaticConstructObjectParameters Params{ WidgetClass }; + Params.Outer = m_obj; + if (InstanceName) Params.Name = FName(InstanceName); + + UObject* Obj = UObjectGlobals::StaticConstructObject(Params); + if (!Obj) + { + Output::send( + STR("[WidgetTree] ConstructWidget: StaticConstructObject returned null for '{}'\n"), + InstanceName ? InstanceName : STR("")); + } + return Obj; + } + + template + auto ConstructWidget(const TCHAR* InstanceName) -> T + { + UObject* O = ConstructWidget(ClassOf(), InstanceName); + return O ? T{ O } : T{}; + } + + private: + struct WalkState + { + EScope Scope; + int32 Budget; + bool bStopped; + bool bExhausted; + }; + + static auto OwnedTreeObject(UObject* Node) -> UObject* + { + return RawObject{ Node, STR("Widget") }.TryObjectProperty(STR("WidgetTree")); + } + + template + static auto WalkNode(UObject* Node, int32 Depth, WalkState& State, Fn& Visit) -> void + { + if (!Node || State.bStopped) return; + + if (State.Budget-- <= 0 || Depth >= kMaxDepth) + { + State.bExhausted = true; + State.bStopped = true; + return; + } + + const EVisit Result = Visit(Node, Depth); + if (Result == EVisit::Stop) { State.bStopped = true; return; } + if (Result == EVisit::SkipChildren) return; + + if (UObject* NestedTree = OwnedTreeObject(Node)) + { + if (State.Scope == EScope::IncludeNestedUserWidgets) + { + UObject* NestedRoot = + RawObject{ NestedTree, STR("WidgetTree") }.TryObjectProperty(STR("RootWidget")); + WalkNode(NestedRoot, Depth + 1, State, Visit); + } + return; + } + + if (!ObjectIsA(Node)) return; + + for (UObject* Child : PanelWidget{ Node }.GetChildObjects()) + { + if (State.bStopped) return; + WalkNode(Child, Depth + 1, State, Visit); + } + } + }; +} diff --git a/Api/WrapBox.hpp b/Api/WrapBox.hpp new file mode 100644 index 0000000..1526865 --- /dev/null +++ b/Api/WrapBox.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "PanelWidget.hpp" +#include "PanelSlot.hpp" + +namespace PalUIExtension::Api +{ + class WrapBox : public PanelWidget + { + public: + static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.WrapBox"); } + static auto DebugTag() -> const TCHAR* { return STR("WrapBox"); } + + WrapBox() = default; + explicit WrapBox(UObject* Obj) + : PanelWidget(Obj, ClassOf(), DebugTag()) {} + + auto AddChildToWrapBox(UObject* Content) -> PanelSlot + { + struct P { UObject* Content; UObject* ReturnValue; } p{ Content, nullptr }; + auto Result = CallForReturn(STR("AddChildToWrapBox"), p, &P::ReturnValue); + return (Result && *Result) ? PanelSlot{ *Result } : PanelSlot{}; + } + }; +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..20189d8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.18) + +set(TARGET PalUIExtension) +project(${TARGET}) + +add_library(${TARGET} SHARED "dllmain.cpp") +target_include_directories(${TARGET} PRIVATE .) +target_link_libraries(${TARGET} PUBLIC UE4SS) \ No newline at end of file diff --git a/Features/IUIFeature.hpp b/Features/IUIFeature.hpp new file mode 100644 index 0000000..0febc1f --- /dev/null +++ b/Features/IUIFeature.hpp @@ -0,0 +1,194 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace PalUIExtension::Features +{ + using namespace RC; + using namespace RC::Unreal; + + class IUIFeature + { + public: + using StringType = std::basic_string; + + virtual ~IUIFeature() = default; + + virtual auto Name() const -> const TCHAR* = 0; + + virtual auto InstallHooks() -> void = 0; + + virtual auto OnWorldTeardown() -> void {} + + virtual auto SetEnabled(bool enabled) -> void {} + + virtual auto GetEnabled() -> bool { return false; } + + + + + + + + // ---- logging --------------------------------------------------- + + template + auto SendLog(LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void + { + Emit(Level, Fmt, std::forward(Fargs)...); + } + + template + auto SendLogOnce(const TCHAR* Key, LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void + { + if (!Key) return; + if (!m_log_once_keys.insert(StringType{ Key }).second) return; + + Emit(Level, Fmt, std::forward(Fargs)...); + } + + template + auto SendLogThrottled(const TCHAR* Key, int64_t IntervalMs, LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void + { + if (!Key) return; + + using Clock = std::chrono::steady_clock; + const auto Now = Clock::now(); + + auto& Last = m_log_throttle[StringType{ Key }]; + if (Last.time_since_epoch().count() != 0 && std::chrono::duration_cast(Now - Last).count() < IntervalMs) + { + return; + } + Last = Now; + + Emit(Level, Fmt, std::forward(Fargs)...); + } + + template + auto SendVerbose(const TCHAR* Fmt, Args&&... Fargs) const -> void + { + if constexpr (Static::kVerboseLogging) + { + Emit(LogLevel::Verbose, Fmt, std::forward(Fargs)...); + } + } + + auto ResetLogOnce() const -> void + { + m_log_once_keys.clear(); + m_log_throttle.clear(); + } + + // ---- argument helpers ------------------------------------------- + static auto NameOf(UObject* Obj) -> StringType { return Obj ? Obj->GetName() : StringType{ STR("") }; } + + static auto FullNameOf(UObject* Obj) -> StringType { return Obj ? Obj->GetFullName() : StringType{ STR("") }; } + + static auto ClassNameOf(UObject* Obj) -> StringType + { + if (!Obj) return StringType{ STR("") }; + UClass* Cls = Obj->GetClassPrivate(); + return Cls ? Cls->GetName() : StringType{ STR("") }; + } + + static auto AddressOf(const void* P) -> uint64_t + { + return static_cast(reinterpret_cast(P)); + } + + private: + template + auto Emit(LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void + { + if (!Fmt) return; + + try + { + switch (Level) + { + case LogLevel::Error: + Output::send(STR("PalUIExt [{}] "), Name()); + Output::send(Fmt, std::forward(Fargs)...); + Output::send(STR("\n")); + break; + case LogLevel::Warning: + Output::send(STR("PalUIExt [{}] "), Name()); + Output::send(Fmt, std::forward(Fargs)...); + Output::send(STR("\n")); + break; + case LogLevel::Verbose: + Output::send(STR("PalUIExt [{}] "), Name()); + Output::send(Fmt, std::forward(Fargs)...); + Output::send(STR("\n")); + break; + default: + Output::send(STR("PalUIExt [{}] "), Name()); + Output::send(Fmt, std::forward(Fargs)...); + Output::send(STR("\n")); + break; + } + } + catch (const std::exception&) + { + try { Output::send(STR("PalUIExt: malformed log format string\n")); } + catch (...) {} + } + catch (...) + { + } + } + + mutable std::unordered_set m_log_once_keys; + mutable std::unordered_map m_log_throttle; + }; + + + class ScopedHotPathTimer + { + public: + ScopedHotPathTimer(const IUIFeature& Feature, const TCHAR* Label, + int64_t ReportAboveMicros = 250) + : m_feature(Feature), m_label(Label), m_threshold(ReportAboveMicros) + { + if constexpr (Static::kProfileHotPath) + { + m_start = std::chrono::steady_clock::now(); + } + } + + ~ScopedHotPathTimer() + { + if constexpr (Static::kProfileHotPath) + { + const auto Micros = std::chrono::duration_cast( + std::chrono::steady_clock::now() - m_start).count(); + + if (Micros >= m_threshold) + { + m_feature.SendLogThrottled(m_label, 1000, LogLevel::Warning, + STR("slow path: {} took {}us"), m_label, Micros); + } + } + } + + ScopedHotPathTimer(const ScopedHotPathTimer&) = delete; + ScopedHotPathTimer& operator=(const ScopedHotPathTimer&) = delete; + + private: + const IUIFeature& m_feature; + const TCHAR* m_label; + int64_t m_threshold; + std::chrono::steady_clock::time_point m_start{}; + }; +} \ No newline at end of file diff --git a/Features/PalBoxNightWorkIndicator.hpp b/Features/PalBoxNightWorkIndicator.hpp new file mode 100644 index 0000000..8585c4c --- /dev/null +++ b/Features/PalBoxNightWorkIndicator.hpp @@ -0,0 +1,449 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "IUIFeature.hpp" + +namespace PalUIExtension::Features +{ + using namespace RC; + using namespace RC::Unreal; + + class PalBoxNightWorkIndicator final : public IUIFeature + { + public: + auto Name() const -> const TCHAR* override { return STR("PalBoxNightWorkIndicator"); } + + auto InstallHooks() -> void override + { + const auto BindId = Static::EventHookRegistry::Register( + STR("PalBoxNightWorkIndicator::UpdateSlotBinded"), + Api::PalCommonCharacterSlot::OnUpdateSlotBindedPath(), + nullptr, + &PalBoxNightWorkIndicator::OnUpdateSlotBinded, + this); + + if (BindId == Static::EventHookRegistry::InvalidHook) { + SendLog(LogLevel::Error, STR("InstallHooks: 'On Update Slot Binded' failed to bind")); + } + + const auto EmptyId = Static::EventHookRegistry::Register( + STR("PalBoxNightWorkIndicator::SetEmpty"), + Api::PalCommonCharacterSlot::OnSetEmptyBindedPath(), + nullptr, + &PalBoxNightWorkIndicator::OnSetEmpty, + this); + + if (EmptyId == Static::EventHookRegistry::InvalidHook) { + SendLog(LogLevel::Warning, STR("InstallHooks: 'On Set Empty Binded' failed to bind.")); + } + + SendLog(LogLevel::Normal, STR("InstallHooks: bind={} empty={}"), BindId, EmptyId); + } + + auto OnWorldTeardown() -> void override + { + // Every pointer in here belongs to a world that is going away. + SendLog(LogLevel::Normal, + STR("teardown: dropping {} cached slots (injected={}, tier2 walks={}, tier3 constructs={})"), + static_cast(m_slots.size()), + m_injection_done ? 1 : 0, + m_tier2_walks, + m_fallback_constructs); + + m_slots.clear(); + m_injection_done = false; + m_injection_failed = false; + m_tier2_walks = 0; + m_fallback_constructs = 0; + ResetLogOnce(); + } + + auto SetEnabled(bool bEnabled) -> void override { m_enabled = bEnabled; } + auto GetEnabled() -> bool override { return m_enabled; } + + private: + static constexpr const TCHAR* kBadgeInstanceName = STR("PalUI_NightWorkBadge"); + static constexpr size_t kMaxCachedSlots = 2048; + + struct SlotEntry + { + UObject* Badge{ nullptr }; + UObject* Tree{ nullptr }; // the slot widget's WidgetTree, for validation + bool bVisible{ false }; + bool bStateKnown{ false }; + }; + + bool m_enabled{ true }; + bool m_injection_done{ false }; + bool m_injection_failed{ false }; + + int32 m_tier2_walks{ 0 }; + int32 m_fallback_constructs{ 0 }; + + std::unordered_map m_slots; + + // ---- hooks --------------------------------------------------- + + static void OnUpdateSlotBinded(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) + { + auto* Self = static_cast(CustomData); + if (!Self || !Self->m_enabled || !Ctx.Context) return; + + auto& Frame = Ctx.GetParams(); + Self->Apply(Ctx.Context, Frame.TargetSlot); + } + + static void OnSetEmpty(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) + { + auto* Self = static_cast(CustomData); + if (!Self || !Self->m_enabled || !Ctx.Context) return; + + Self->SetBadgeVisible(Ctx.Context, false); + } + + // ---- the work ------------------------------------------------ + + auto Apply(UObject* SlotWidget, UObject* TargetSlot) -> void + { + ScopedHotPathTimer Timer{ *this, STR("NightWorkIndicator::Apply") }; + + + EnsureTemplateInjection(SlotWidget); + + const auto Species = SpeciesFromSlot(TargetSlot); + if (!Species.has_value()) + { + SendLogOnce(STR("Apply::no-species"), LogLevel::Verbose, + STR("Apply: no species from slot {} (class {}). Hiding badge. " + "If this repeats for FULL tiles, GetHandle/GetCharacterID is the broken link."), + NameOf(TargetSlot), ClassNameOf(TargetSlot)); + + SetBadgeVisible(SlotWidget, false); + return; + } + + const auto Nocturnal = Api::Pal::PalUtility::Instance().NocturnalFor(*Species, SlotWidget); + if (!Nocturnal.has_value()) + { + SendLogThrottled(STR("Apply::no-nocturnal"), 5000, LogLevel::Warning, + STR("Apply: nocturnal lookup returned nothing for species '{}'. " + "Character database not ready, or the species ID needs variant stripping."), + Species->ToString()); + + SetBadgeVisible(SlotWidget, false, /*bRecordState=*/false); + return; + } + + SetBadgeVisible(SlotWidget, *Nocturnal); + } + + auto SpeciesFromSlot(UObject* Slot) const -> std::optional + { + if (!Slot) return std::nullopt; + + Api::Pal::LooseObject S{ Slot, STR("PalIndividualSlot") }; + UObject* Handle = S.CallForReturn(STR("GetHandle")).value_or(nullptr); + + return Api::Pal::PalIndividual::CharacterIdFromHandle(Handle); + } + + // ============================================================== + // TIER 1 -- class template injection + // ============================================================== + auto EnsureTemplateInjection(UObject* AnySlotWidget) -> void + { + if (m_injection_done || m_injection_failed || !AnySlotWidget) return; + + // One attempt. On failure the flag latches, because retrying a + // class-level operation on every bind is exactly the per-tile + // cost this rewrite exists to remove. + m_injection_failed = true; + + Api::WidgetBlueprintGeneratedClass GenClass = + Api::WidgetBlueprintGeneratedClass::Of(AnySlotWidget); + + if (!GenClass.IsValid()) + { + SendLog(LogLevel::Warning, + STR("injection: {} is not a WidgetBlueprintGeneratedClass instance. " + "Falling back to per-instance attach for the whole session."), + ClassNameOf(AnySlotWidget)); + return; + } + + Api::WidgetTree Template = GenClass.TemplateTree(); + if (!Template.IsValid()) + { + SendLog(LogLevel::Warning, + STR("injection: class {} has no template WidgetTree (cooked-tree build?). " + "Falling back to per-instance attach for the whole session."), + ClassNameOf(AnySlotWidget)); + return; + } + + // Already there from a previous world? Then nothing to do the class survived the transition. + if (Template.FindObjectByName(kBadgeInstanceName)) + { + m_injection_done = true; + m_injection_failed = false; + SendLog(LogLevel::Normal, STR("injection: template already carries the badge; reusing.")); + return; + } + + UObject* AnchorObj = Template.FindObjectByName(STR("CanvasPanel_249")); + if (!AnchorObj) + { + SendLog(LogLevel::Warning, + STR("injection: 'CanvasPanel_249' is not in the template tree of {}. " + "The designer name changed, or the anchor moved into a nested widget. " + "Dump the tree to find its new name."), + ClassNameOf(AnySlotWidget)); + return; + } + + Api::CanvasPanel Anchor{ AnchorObj }; + if (!Anchor.IsValid()) + { + SendLog(LogLevel::Warning, + STR("injection: 'CanvasPanel_249' resolved to a {}, not a CanvasPanel."), + ClassNameOf(AnchorObj)); + return; + } + + UClass* BadgeClass = Api::ClassOf(); + if (!BadgeClass) + { + SendLog(LogLevel::Error, + STR("injection: badge class '{}' did not resolve. Is the mod pak mounted?"), + Api::Mod::PalDetail_PalIconNightEye::ClassPath()); + return; + } + + UObject* TemplateBadge = Template.ConstructWidget(BadgeClass, kBadgeInstanceName); + if (!TemplateBadge) + { + SendLog(LogLevel::Error, + STR("injection: failed to construct '{}' under the template tree."), + kBadgeInstanceName); + return; + } + + if (!Anchor.AddChild(TemplateBadge).IsValid()) + { + SendLog(LogLevel::Error, + STR("injection: AddChild returned no slot. The badge exists but is not parented; " + "it will not duplicate into instances.")); + return; + } + + m_injection_done = true; + m_injection_failed = false; + + SendLog(LogLevel::Normal, + STR("injection: badge added to the {} template tree. Instances built from here on " + "carry it for free."), + ClassNameOf(AnySlotWidget)); + + VerifyBadgeDefaultsCollapsed(TemplateBadge); + } + + auto VerifyBadgeDefaultsCollapsed(UObject* TemplateBadge) -> void + { + Api::Mod::PalDetail_PalIconNightEye W{ TemplateBadge }; + if (!W.IsValid()) return; + + const auto Vis = W.GetIconVisibility(); + if (!Vis.has_value()) + { + SendLog(LogLevel::Warning, + STR("injection: could not read the template badge's default visibility. " + "Confirm by eye that it is hidden on non-palbox screens.")); + return; + } + } + + // ============================================================== + // Applying the state + // ============================================================== + auto SetBadgeVisible(UObject* SlotWidget, bool bVisible, bool bRecordState = true) -> void + { + if (!SlotWidget) return; + + SlotEntry* Entry = ResolveEntry(SlotWidget); + if (!Entry || !Entry->Badge) return; // ResolveEntry logged + + // The real gate. Per widget, not a memo of one -- this is what + // the old comment described and the old code could not do. + if (bRecordState && Entry->bStateKnown && Entry->bVisible == bVisible) return; + + Api::Mod::PalDetail_PalIconNightEye W{ Entry->Badge }; + if (!W.IsValid()) + { + SendLogOnce(STR("SetBadgeVisible::bad-badge"), LogLevel::Warning, + STR("SetBadgeVisible: cached badge on {} is not a PalDetail_PalIconNightEye " + "(it is a {}). Dropping the cache entry."), + NameOf(SlotWidget), ClassNameOf(Entry->Badge)); + + m_slots.erase(SlotWidget); + return; + } + + const bool bApplied = W.SetIconVisibility(bVisible); + if (!bApplied) + { + SendLogThrottled(STR("SetBadgeVisible::call-failed"), 5000, LogLevel::Warning, + STR("SetBadgeVisible: SetIconVisibility failed on the badge under {}. " + "Not recording state, so the next bind retries."), + NameOf(SlotWidget)); + + Entry->bStateKnown = false; + return; + } + + if (!bRecordState) + { + Entry->bStateKnown = false; + return; + } + + Entry->bVisible = bVisible; + Entry->bStateKnown = true; + } + + // ============================================================== + // The lookup. Cache -> tree walk -> construct. + // ============================================================== + auto ResolveEntry(UObject* SlotWidget) -> SlotEntry* + { + // The validation token: one pointer read, no ProcessEvent. + UObject* Tree = Api::WidgetTree::FromUserWidget(SlotWidget).Get(); + if (!Tree) + { + SendLogOnce(STR("ResolveEntry::no-tree"), LogLevel::Warning, + STR("ResolveEntry: {} has no WidgetTree. Not a UUserWidget, or the hook is " + "firing on the wrong object."), + NameOf(SlotWidget)); + return nullptr; + } + + if (auto It = m_slots.find(SlotWidget); It != m_slots.end()) + { + // A recycled address holding a DIFFERENT widget has a + // different tree. This turns a collision into a miss. + if (It->second.Tree == Tree && It->second.Badge && + !It->second.Badge->IsUnreachable()) + { + return &It->second; + } + + SendVerbose(STR("ResolveEntry: stale entry for {} dropped (address recycled)."), + NameOf(SlotWidget)); + m_slots.erase(It); + } + + UObject* Badge = FindOrAttachBadge(SlotWidget, Tree); + if (!Badge) return nullptr; + + if (m_slots.size() >= kMaxCachedSlots) + { + // Not an LRU on purpose: this bound should never be + // reached, so reaching it is a signal, and a silent + // eviction policy would hide it. + SendLogThrottled(STR("ResolveEntry::cache-full"), 30000, LogLevel::Warning, + STR("ResolveEntry: slot cache hit {} entries and was cleared. Slot widgets are " + "not being reused the way this assumes; entries are accumulating."), + static_cast(m_slots.size())); + m_slots.clear(); + } + + auto [It, bInserted] = m_slots.emplace(SlotWidget, SlotEntry{ Badge, Tree, false, false }); + return &It->second; + } + + auto FindOrAttachBadge(UObject* SlotWidget, UObject* TreeObj) -> UObject* + { + Api::WidgetTree Tree{ TreeObj }; + + ++m_tier2_walks; + if (UObject* Existing = Tree.FindObjectByName(kBadgeInstanceName)) + { + return Existing; + } + + ++m_fallback_constructs; + + SendLogThrottled(STR("FindOrAttachBadge::fallback"), 10000, LogLevel::Warning, + STR("per-instance construct #{} (walks={}, injection_done={}). Expected to settle " + "shortly after startup. If this keeps climbing, template duplication is not " + "carrying the badge into new instances."), + m_fallback_constructs, m_tier2_walks, m_injection_done ? 1 : 0); + + UClass* BadgeClass = Api::ClassOf(); + if (!BadgeClass) + { + SendLogOnce(STR("FindOrAttachBadge::no-class"), LogLevel::Error, + STR("badge class '{}' did not resolve. Is the mod pak mounted?"), + Api::Mod::PalDetail_PalIconNightEye::ClassPath()); + return nullptr; + } + + UObject* Badge = Tree.ConstructWidget(BadgeClass, kBadgeInstanceName); + if (!Badge) + { + SendLogOnce(STR("FindOrAttachBadge::construct-failed"), LogLevel::Error, + STR("could not construct '{}' under the tree of {}."), + kBadgeInstanceName, NameOf(SlotWidget)); + return nullptr; + } + + Api::PalCommonCharacterSlot Slot{ SlotWidget }; + if (!Slot.IsValid()) + { + SendLogOnce(STR("FindOrAttachBadge::bad-slot"), LogLevel::Warning, + STR("{} is a {}, not a PalCommonCharacterSlot. The hook is on the wrong class."), + NameOf(SlotWidget), ClassNameOf(SlotWidget)); + return nullptr; + } + + Api::CanvasPanel Anchor = Api::CanvasPanel{ Slot.Child< Api::CanvasPanel>(STR("CanvasPanel_249")) }; + if (!Anchor.IsValid()) + { + SendLogOnce(STR("FindOrAttachBadge::no-anchor"), LogLevel::Warning, + STR("no 'CanvasPanel_249' under {}. The badge was constructed but has nowhere to go; " + "it will stay invisible."), + NameOf(SlotWidget)); + return nullptr; + } + + if (!Anchor.AddChild(Badge).IsValid()) + { + SendLogOnce(STR("FindOrAttachBadge::attach-failed"), LogLevel::Warning, + STR("AddChild returned no slot attaching the badge under {}."), + NameOf(SlotWidget)); + return nullptr; + } + + return Badge; + } + }; +} \ No newline at end of file diff --git a/Features/PalBoxPalNumber.cpp b/Features/PalBoxPalNumber.cpp new file mode 100644 index 0000000..5b5eae9 --- /dev/null +++ b/Features/PalBoxPalNumber.cpp @@ -0,0 +1,10 @@ +#include "PalBoxPalNumber.hpp" + +inline auto PalUIExtension::Features::PalBoxPalNumber::SameIndividual(const PalKey& A, const PalKey& B) -> bool +{ + + return A.InstanceID.A == B.InstanceID.A && A.InstanceID.B == B.InstanceID.B && + A.InstanceID.C == B.InstanceID.C && A.InstanceID.D == B.InstanceID.D && + A.PlayerUID.A == B.PlayerUID.A && A.PlayerUID.B == B.PlayerUID.B && + A.PlayerUID.C == B.PlayerUID.C && A.PlayerUID.D == B.PlayerUID.D; +} diff --git a/Features/PalBoxPalNumber.hpp b/Features/PalBoxPalNumber.hpp new file mode 100644 index 0000000..632cf95 --- /dev/null +++ b/Features/PalBoxPalNumber.hpp @@ -0,0 +1,331 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "IUIFeature.hpp" + +namespace PalUIExtension::Features +{ + using namespace RC; + using namespace RC::Unreal; + using namespace Helpers; + struct Tag_PalNumberWidget {}; + + class PalBoxPalNumber final : public IUIFeature + { + public: + + bool m_enabled = true; + + static constexpr const TCHAR* kInstanceName = STR("PalUI_PalDetail_PalNumber"); + + // nudge the rest of the canvas components down this much to make room for pal number + static constexpr double kCanvasNudgeY = 18.0; + static constexpr double kLabelOffsetY = 8.0; + static constexpr double kLabelOffsetXFraction = 0.4; + + auto Name() const -> const TCHAR* override { return STR("PalNumberLabel"); } + + auto InstallHooks() -> void override + { + Static::EventHookRegistry::Register( + STR("PalNumberLabel::BindFromHandle"), + Api::PalDetailWidget::BindFromHandlePath(), + &PalBoxPalNumber::OnBindFromHandle, + nullptr, + this); + + Static::EventHookRegistry::Register( + STR("PalNumberLabel::SetEmpty"), + Api::PalDetailWidget::SetEmptyPath(), + &PalBoxPalNumber::OnSetEmpty, + nullptr, + this); + } + + auto OnWorldTeardown() -> void override { + + ForgetAll(); + + } + + auto SetEnabled(bool bEnabled) -> void override { m_enabled = bEnabled; } + auto GetEnabled() -> bool override { return m_enabled; } + private: + + + + UObject* m_owner{}; // the detail panel this memo describes + UObject* m_label{}; // the injected widget under that panel + Helpers::PalKey m_key{}; + bool m_has_key{}; + int32 m_index{}; + bool m_has_index{}; + bool m_visible{ true }; // last visibility WE set + bool m_layout_done{}; // canvas nudge applied for m_owner + + // ---- hooks ---------------------------------------- + + static void OnBindFromHandle(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) + { + auto* Self = static_cast(CustomData); + if (!Self || !Ctx.Context) return; + + auto& Frame = Ctx.GetParams(); + Self->HandleBind(Ctx.Context, Frame.targetHandle); + } + + static void OnSetEmpty(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) + { + auto* Self = static_cast(CustomData); + if (!Self || !Ctx.Context) return; + Self->HandleEmpty(Ctx.Context); + } + + // ---- the actual work ----------------------------------------- + + auto HandleBind(UObject* Owner, UObject* Handle) -> void + { + if (Owner != m_owner) AdoptOwner(Owner); + + const auto Key = ReadKey(Handle); + if (!Key.has_value()) + { + HideLabel(); + m_has_key = false; + return; + } + + if (m_has_key && SamePalIndividual(m_key, *Key)) return; // the early out that matters + + UObject* Label = ResolveLabel(Owner); + if (!Label) return; + + Api::Mod::PalDetail_PalNumberWidget W{ Label }; + if (!W.IsValid()) return; + + m_key = *Key; + m_has_key = true; + + const auto Species = W.SetPalBinding(BuildBindingParam(m_key)); + if (!Species.has_value()) + { + ShowLabel(W); + return; + } + + const auto Index = Api::Pal::PalUtility::Instance().ZukanIndexFor(*Species, Owner); + + ShowLabel(W); + ApplyIndex(W, Index); + } + + auto HandleEmpty(UObject* Owner) -> void + { + if (Owner != m_owner) AdoptOwner(Owner); + m_has_key = false; + HideLabel(); + } + + auto ApplyIndex(Api::Mod::PalDetail_PalNumberWidget& W, std::optional Index) -> void + { + const int32 Resolved = Index.value_or(kUnknownIndex); + + if (m_has_index && m_index == Resolved) return; + + m_index = Resolved; + m_has_index = true; + + const FString Text = (Resolved == kUnknownIndex) ? FString(STR("?")) : FString::Printf(STR("%d"), Resolved); + + if (!W.SetPalNumberText(Text)) + { + m_has_index = false; + SendLog(LogLevel::Error, STR("[PalNumberLabel] SetPalNumberText failed on {}\n"), W.Get() ? W.Get()->GetFullName() : STR("")); + + } + } + + // ---- widget resolution --------------------------------------- + + auto ResolveLabel(UObject* Owner) -> UObject* + { + if (m_label && IsOurLabel(m_label, Owner)) return m_label; + + const auto Key = RegistryKey(Owner); + + if (UObject* Cached = Static::WidgetRegistry::GetObject(Key); + Cached && IsOurLabel(Cached, Owner)) + { + m_label = Cached; + return m_label; + } + + bool bWasNew = false; + UObject* Found = WidgetApi().FindOrConstruct(Owner, kInstanceName, bWasNew); + if (!Found) + { + SendLog(LogLevel::Error, STR("[PalNumberLabel] could not find or construct '{}' under {}\n"), kInstanceName, Owner->GetFullName()); + return nullptr; + } + + if (bWasNew && !AttachLabel(Owner, Found)) + { + return nullptr; + } + + m_label = Found; + m_visible = true; // freshly attached widgets are visible + Static::WidgetRegistry::Register(Key, Found); + return m_label; + } + + auto AttachLabel(UObject* Owner, UObject* Label) -> bool + { + Api::PalDetailWidget Panel{ Owner }; + if (!Panel.IsValid()) + { + Output::send( + STR("[PalNumberLabel] {} is not a PalDetailWidget\n"), Owner->GetFullName()); + return false; + } + + Api::CanvasPanel Canvas = Panel.CanvasInfo(); + if (!Canvas.IsValid()) + { + Canvas = Panel.Child( + STR("Canvas_Info"), Api::WidgetTree::EScope::IncludeNestedUserWidgets); + } + if (!Canvas.IsValid()) return false; + + if (!m_layout_done) + { + + UObject* CanvasSlotObj = Canvas.GetSlotObject(); + Api::CanvasPanelSlot CanvasSlot = CanvasSlotObj + ? Api::CanvasPanelSlot{ CanvasSlotObj } : Api::CanvasPanelSlot{}; + if (CanvasSlot.IsValid()) + { + Api::FVector2D_ Pos = CanvasSlot.GetPosition().value_or(Api::FVector2D_{ 0, 0 }); + Pos.Y += kCanvasNudgeY; + CanvasSlot.SetPosition(Pos); + } + m_layout_done = true; + } + + Api::PanelSlot Added = Canvas.AddChild(Label); + if (!Added.IsValid()) return false; + + Api::CanvasPanelSlot LabelSlot{ Added.Get() }; + if (LabelSlot.IsValid()) + { + const Api::FVector2D_ Size = LabelSlot.GetSize().value_or(Api::FVector2D_{ 0, 0 }); + LabelSlot.SetPosition(Api::FVector2D_{ Size.X * kLabelOffsetXFraction, kLabelOffsetY }); + } + return true; + } + + // ---- visibility ---------------------------------------------- + + auto ShowLabel(Api::Mod::PalDetail_PalNumberWidget& W) -> void + { + if (m_visible) return; + if (W.SetVisibility(Api::ESlateVisibility_::SelfHitTestInvisible)) m_visible = true; + } + + auto HideLabel() -> void + { + if (!m_visible || !m_label) return; + + Api::Widget W{ m_label }; + if (!W.IsValid()) return; + if (W.SetVisibility(Api::ESlateVisibility_::Collapsed)) m_visible = false; + } + + // ---- identity ------------------------------------------------- + + static auto ReadKey(UObject* Handle) -> std::optional + { + if (!Handle) return std::nullopt; + + Api::Pal::LooseObject H{ Handle, STR("PalIndividualCharacterHandle") }; + auto Id = H.CallForReturn(STR("GetIndividualID")); + if (!Id.has_value()) return std::nullopt; + + return PalKey{ Id->PlayerUID, Id->InstanceID }; + } + + static auto BuildBindingParam(const PalKey& Key) -> Api::Pal::FPalInstanceID + { + return Api::Pal::FPalInstanceID{ Key.PlayerUID, Key.InstanceID, FString{} }; + } + + // ---- memo management ----------------------------------------- + + auto AdoptOwner(UObject* Owner) -> void + { + m_owner = Owner; + m_label = nullptr; + m_has_key = false; + m_has_index = false; + m_visible = true; + m_layout_done = false; + } + + auto ForgetAll() -> void + { + m_owner = nullptr; + m_label = nullptr; + m_has_key = false; + m_has_index = false; + m_visible = true; + m_layout_done = false; + } + + static auto IsOurLabel(UObject* Candidate, UObject* Owner) -> bool + { + if (!Candidate || !Owner) return false; + if (Candidate->GetOuterPrivate() != Owner) return false; + return Api::ObjectIsA(Candidate); + } + + static auto RegistryKey(UObject* Owner) -> Static::WidgetRegistry::KeyType + { + static_assert(std::is_same_v, + "RegistryKey builds its suffix with std::to_wstring"); + + return Static::WidgetRegistry::KeyType{ STR("PalBoxNumberLabel:") } + + std::to_wstring(reinterpret_cast(Owner)); + } + + static auto WidgetApi() -> Reflection::UnrealApiCall& + { + static Reflection::UnrealApiCall Api{ + Api::Mod::PalDetail_PalNumberWidget::ClassPath(), + Api::Mod::PalDetail_PalNumberWidget::DebugTag() }; + return Api; + } + + static constexpr int32 kUnknownIndex = -1; + }; +} diff --git a/Features/PalStatusPalNumber.hpp b/Features/PalStatusPalNumber.hpp new file mode 100644 index 0000000..4b5ae78 --- /dev/null +++ b/Features/PalStatusPalNumber.hpp @@ -0,0 +1,281 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "IUIFeature.hpp" + +namespace PalUIExtension::Features +{ + using namespace RC; + using namespace RC::Unreal; + + struct Tag_PalStatusNumberWidget {}; + class PalStatusPalNumber final : public IUIFeature + { + public: + + bool m_enabled = true; + + static constexpr const TCHAR* kInstanceName = STR("PalUI_PalStatus_PalNumber"); + + // nudge the rest of the canvas components down this much to make room for pal number + static constexpr double kLabelOffsetY = -25; + static constexpr double kLabelOffsetXFraction = 2.4; + + auto Name() const -> const TCHAR* override { return STR("PalStatusNumberLabel"); } + + auto InstallHooks() -> void override + { + Static::EventHookRegistry::Register( + STR("PalStatusPalNumber::BindFromHandle"), + Api::MainMenuPalWidget::BindFromHandlePath(), + &PalStatusPalNumber::OnBindFromHandle, + nullptr, + this); + } + + auto OnWorldTeardown() -> void override { ForgetAll(); } + + auto SetEnabled(bool bEnabled) -> void override { m_enabled = bEnabled; } + auto GetEnabled() -> bool override { return m_enabled; } + + private: + + UObject* m_owner{}; // the status panel this memo describes + UObject* m_label{}; // the injected widget under that panel + PalKey m_key{}; + bool m_has_key{}; + int32 m_index{}; + bool m_has_index{}; + bool m_visible{ true }; // last visibility WE set + bool m_layout_done{}; // canvas nudge applied for m_owner + + // ---- hooks ---------------------------------------- + + static void OnBindFromHandle(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) + { + auto* Self = static_cast(CustomData); + if (!Self || !Ctx.Context) return; + + auto& Frame = Ctx.GetParams(); + Self->HandleBind(Ctx.Context, Frame.targetHandle); + } + + auto HandleBind(UObject* Owner, UObject* Handle) -> void + { + if (Owner != m_owner) AdoptOwner(Owner); + + const auto Key = ReadKey(Handle); + if (!Key.has_value()) + { + HideLabel(); + m_has_key = false; + return; + } + + if (m_has_key && SamePalIndividual(m_key, *Key)) return; // the early out that matters + + UObject* Label = ResolveLabel(Owner); + if (!Label) return; + + Api::Mod::PalStatus_PalNumberWidget W{ Label }; + if (!W.IsValid()) return; + + m_key = *Key; + m_has_key = true; + + const auto Species = W.SetPalBinding(BuildBindingParam(m_key)); + if (!Species.has_value()) + { + ShowLabel(W); + return; + } + + const auto Index = Api::Pal::PalUtility::Instance().ZukanIndexFor(*Species, Owner); + + ShowLabel(W); + ApplyIndex(W, Index); + } + + auto ApplyIndex(Api::Mod::PalStatus_PalNumberWidget& W, std::optional Index) -> void + { + const int32 Resolved = Index.value_or(kUnknownIndex); + + if (m_has_index && m_index == Resolved) return; + + m_index = Resolved; + m_has_index = true; + + const FString Text = (Resolved == kUnknownIndex) ? FString(STR("?")) : FString::Printf(STR("%d"), Resolved); + + if (!W.SetPalNumberText(Text)) + { + m_has_index = false; + SendLog(LogLevel::Error, STR("SetPalNumberText failed on {}\n"), W.Get() ? W.Get()->GetFullName() : STR("")); + } + } + + // ---- widget resolution --------------------------------------- + + auto ResolveLabel(UObject* Owner) -> UObject* + { + if (m_label && IsOurLabel(m_label, Owner)) return m_label; + + const auto Key = RegistryKey(Owner); + + if (UObject* Cached = Static::WidgetRegistry::GetObject(Key); + Cached && IsOurLabel(Cached, Owner)) + { + m_label = Cached; + return m_label; + } + + bool bWasNew = false; + UObject* Found = WidgetApi().FindOrConstruct(Owner, kInstanceName, bWasNew); + if (!Found) + { + SendLog(LogLevel::Error, STR("could not find or construct '{}' under {}\n"), kInstanceName, Owner->GetFullName()); + return nullptr; + } + + if (bWasNew && !AttachLabel(Owner, Found)) + { + return nullptr; + } + + m_label = Found; + m_visible = true; // freshly attached widgets are visible + Static::WidgetRegistry::Register(Key, Found); + return m_label; + } + + auto AttachLabel(UObject* Owner, UObject* Label) -> bool + { + Api::MainMenuPalWidget Panel{ Owner }; + if (!Panel.IsValid()) + { + Output::send( + STR("[PalNumberLabel] {} is not a MainMenuPalWidget\n"), Owner->GetFullName()); + return false; + } + Api::CanvasPanel Canvas = Panel.CanvasPalName(); + if (!Canvas.IsValid()) + { + Canvas = Panel.Child(STR("Canvas_PalName")); + } + if (!Canvas.IsValid()) return false; + + + Api::PanelSlot Added = Canvas.AddChild(Label); + if (!Added.IsValid()) return false; + + Api::CanvasPanelSlot LabelSlot{ Added.Get() }; + if (LabelSlot.IsValid()) + { + const Api::FVector2D_ Size = LabelSlot.GetSize().value_or(Api::FVector2D_{ 0, 0 }); + LabelSlot.SetPosition(Api::FVector2D_{ Size.X * kLabelOffsetXFraction, kLabelOffsetY }); + } + return true; + } + + // ---- visibility ---------------------------------------------- + + auto ShowLabel(Api::Mod::PalStatus_PalNumberWidget& W) -> void + { + if (m_visible) return; + if (W.SetVisibility(Api::ESlateVisibility_::SelfHitTestInvisible)) m_visible = true; + } + + auto HideLabel() -> void + { + if (!m_visible || !m_label) return; + + Api::Widget W{ m_label }; + if (!W.IsValid()) return; + if (W.SetVisibility(Api::ESlateVisibility_::Collapsed)) m_visible = false; + } + + // ---- identity ------------------------------------------------- + + static auto ReadKey(UObject* Handle) -> std::optional + { + if (!Handle) return std::nullopt; + + Api::Pal::LooseObject H{ Handle, STR("PalIndividualCharacterHandle") }; + auto Id = H.CallForReturn(STR("GetIndividualID")); + if (!Id.has_value()) return std::nullopt; + + return PalKey{ Id->PlayerUID, Id->InstanceID }; + } + + static auto BuildBindingParam(const PalKey& Key) -> Api::Pal::FPalInstanceID + { + return Api::Pal::FPalInstanceID{ Key.PlayerUID, Key.InstanceID, FString{} }; + } + + auto AdoptOwner(UObject* Owner) -> void + { + m_owner = Owner; + m_label = nullptr; + m_has_key = false; + m_has_index = false; + m_visible = true; + } + + auto ForgetAll() -> void + { + m_owner = nullptr; + m_label = nullptr; + m_has_key = false; + m_has_index = false; + m_visible = true; + + } + + static auto IsOurLabel(UObject* Candidate, UObject* Owner) -> bool + { + if (!Candidate || !Owner) return false; + if (Candidate->GetOuterPrivate() != Owner) return false; + return Api::ObjectIsA(Candidate); + } + + static auto RegistryKey(UObject* Owner) -> Static::WidgetRegistry::KeyType + { + static_assert(std::is_same_v, + "RegistryKey builds its suffix with std::to_wstring"); + + return Static::WidgetRegistry::KeyType{ STR("PalStatusNumberLabel:") } + + std::to_wstring(reinterpret_cast(Owner)); + } + + static auto WidgetApi() -> Reflection::UnrealApiCall& + { + static Reflection::UnrealApiCall Api{ + Api::Mod::PalStatus_PalNumberWidget::ClassPath(), + Api::Mod::PalStatus_PalNumberWidget::DebugTag() }; + return Api; + } + + static constexpr int32 kUnknownIndex = -1; + }; +} diff --git a/Helpers/PalHelpers.hpp b/Helpers/PalHelpers.hpp new file mode 100644 index 0000000..9d198bb --- /dev/null +++ b/Helpers/PalHelpers.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include + +namespace PalUIExtension::Helpers +{ + using namespace RC; + using namespace RC::Unreal; + + + struct PalKey + { + Api::Pal::FGuid_ PlayerUID{}; + Api::Pal::FGuid_ InstanceID{}; + }; + + auto SamePalIndividual(const PalKey& A, const PalKey& B) -> bool + { + + return A.InstanceID.A == B.InstanceID.A && A.InstanceID.B == B.InstanceID.B && + A.InstanceID.C == B.InstanceID.C && A.InstanceID.D == B.InstanceID.D && + A.PlayerUID.A == B.PlayerUID.A && A.PlayerUID.B == B.PlayerUID.B && + A.PlayerUID.C == B.PlayerUID.C && A.PlayerUID.D == B.PlayerUID.D; + } + +} \ No newline at end of file diff --git a/Helpers/TextFromString.hpp b/Helpers/TextFromString.hpp new file mode 100644 index 0000000..250acd8 --- /dev/null +++ b/Helpers/TextFromString.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +#include +#include + +namespace PalUIExtension::Helpers +{ + using namespace RC; + using namespace RC::Unreal; + + + class FTextFromString + { + public: + explicit FTextFromString(const TCHAR* Str) + { + static Reflection::UnrealObjectRef KismetTextCDO{ + STR("/Script/Engine.Default__KismetTextLibrary"), STR("KismetText") }; + static Reflection::UnrealApiCall Api{ + STR("/Script/Engine.KismetTextLibrary"), STR("KismetText") }; + + UObject* CDO = KismetTextCDO.Get(); + if (!CDO) return; + + m_params.InString = FString(Str); + m_ok = Api.Invoke(CDO, STR("Conv_StringToText"), m_params); + } + + auto IsValid() const -> bool { return m_ok; } + auto Get() -> FText& { return m_params.ReturnValue; } + + private: + // Value-initialized deliberately -- see caveat 1. + struct Params { FString InString; FText ReturnValue; }; + Params m_params{}; + bool m_ok = false; + }; + +} \ No newline at end of file diff --git a/Reflection/UnrealApiCall.hpp b/Reflection/UnrealApiCall.hpp new file mode 100644 index 0000000..b49117d --- /dev/null +++ b/Reflection/UnrealApiCall.hpp @@ -0,0 +1,230 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace PalUIExtension::Reflection +{ + using namespace RC; + using namespace RC::Unreal; + + template 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( + nullptr, nullptr, m_class_path, false); + + if (!Found) + { + Output::send( + STR("[{}] ResolveClass: could not find class '{}'\n"), + m_debug_tag, m_class_path); + return nullptr; + } + + s_cached_class = static_cast(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( + STR("[{}] FindInstance: '{}' not found under outer {}\n"), + m_debug_tag, InstanceName, + Outer ? Outer->GetFullName() : STR("")); + } + return Instance; + } + + auto FindOrConstruct(UObject* Outer, const TCHAR* InstanceName, bool& bOutWasNew, bool bExactClass = false) -> UObject* + { + bOutWasNew = false; + + if (!InstanceName) + { + Output::send( + 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 + auto Invoke(UObject* Target, const TCHAR* FunctionName, ParamsStruct& Params) -> bool + { + if (!Target) + { + Output::send( + STR("[{}] Invoke: null target calling '{}'\n"), m_debug_tag, FunctionName); + return false; + } + + UFunction* Fn = Target->GetFunctionByNameInChain(FunctionName); + if (!Fn) + { + Output::send( + 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, char, TReturn>; + + template + auto InvokeForReturn(UObject* Target, const TCHAR* FunctionName, ParamsStruct& Params, + ReturnMemberType ParamsStruct::* ReturnMember) -> std::optional + { + static_assert(!std::is_void_v, + "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(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(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(EngineSize) != ActualStructSize) + { + Output::send( + 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) + { + if (ReturnProp) + { + Output::send( + 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( + 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(EnginePropSize) != sizeof(TReturn)) + { + Output::send( + 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 }; + }; +} diff --git a/Reflection/UnrealObjectRef.hpp b/Reflection/UnrealObjectRef.hpp new file mode 100644 index 0000000..318994a --- /dev/null +++ b/Reflection/UnrealObjectRef.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace PalUIExtension::Reflection +{ + using namespace RC; + using namespace RC::Unreal; + + class UnrealObjectRef + { + public: + explicit UnrealObjectRef(const TCHAR* Path, const TCHAR* DebugTag) + : m_path(Path), m_debug_tag(DebugTag) { + Static::ClassCache::RegisterSlot(&m_cached); + } + + auto Get() -> UObject* + { + if (m_cached) return m_cached; + + m_cached = UObjectGlobals::StaticFindObject(nullptr, nullptr, m_path, false); + if (!m_cached) + { + Output::send(STR("[{}] could not resolve '{}'\n"), m_debug_tag, m_path); + } + return m_cached; + } + + private: + const TCHAR* m_path; + const TCHAR* m_debug_tag; + UObject* m_cached{}; + }; + +} \ No newline at end of file diff --git a/Screenshot 2026-09-06 153717.png b/Screenshot 2026-09-06 153717.png new file mode 100644 index 0000000..ba4f5e6 Binary files /dev/null and b/Screenshot 2026-09-06 153717.png differ diff --git a/Static/ClassCache.hpp b/Static/ClassCache.hpp new file mode 100644 index 0000000..5a400a3 --- /dev/null +++ b/Static/ClassCache.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include + +namespace PalUIExtension::Static +{ + using namespace RC; + using namespace RC::Unreal; + + class ClassCache + { + public: + + // Registers a slot to be nulled on Flush. The pointer must outlive the process -- every caller is a static, so this holds. + static auto RegisterSlot(UObject** Slot) -> void + { + if (!Slot) return; + s_slots.push_back(Slot); + } + + static auto RegisterSlot(UClass** Slot) -> void + { + if (!Slot) return; + s_class_slots.push_back(Slot); + } + + // Called on EVERY world teardown, before anything else runs. + + static auto Flush() -> void + { + for (UObject** Slot : s_slots) { if (Slot) *Slot = nullptr; } + for (UClass** Slot : s_class_slots){ if (Slot) *Slot = nullptr; } + + Output::send( + STR("[ClassCache] flushed {} object slots and {} class slots\n"), + static_cast(s_slots.size()), + static_cast(s_class_slots.size())); + } + + static auto SlotCount() -> size_t { return s_slots.size() + s_class_slots.size(); } + + private: + inline static std::vector s_slots{}; + inline static std::vector s_class_slots{}; + }; +} diff --git a/Static/EventHookRegistry.hpp b/Static/EventHookRegistry.hpp new file mode 100644 index 0000000..d663139 --- /dev/null +++ b/Static/EventHookRegistry.hpp @@ -0,0 +1,278 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace PalUIExtension::Static +{ + using namespace RC; + using namespace RC::Unreal; + + // Registry + deferred installer. + // + // Register() resolves the target UFunction immediately if it is already loaded. If it isn't, the request is parked in s_pending and a single + // ProcessEvent pre-callback watches for a call on a matching pair, then installs the real hook. + // + // NOT THREAD SAFE. + + class EventHookRegistry + { + public: + using StringType = std::basic_string; + using HookId = uint32_t; + + static constexpr HookId InvalidHook = 0; + + private: + struct HookRequest + { + HookId Id{}; + StringType Name; // owned, not a borrowed const TCHAR* + StringType FunctionPath; // owned + FName ClassName{}; // e.g. WBP_PalStatus_C + FName FunctionName{}; // e.g. Construct + + UnrealScriptFunctionCallable Pre{}; + UnrealScriptFunctionCallable Post{}; + void* CustomData{}; + }; + + struct InstalledHook + { + HookId Id{}; + StringType Name; + UFunction* Function{}; + int PreId{}; + int PostId{}; + }; + + inline static std::vector s_pending{}; + inline static std::vector s_installed{}; + inline static HookId s_next_id{ 1 }; + inline static bool s_watcher_installed{ false }; + inline static bool s_shutdown{ false }; + + public: + static void Initialize(size_t expected_hooks = 16) + { + s_shutdown = false; + s_pending.reserve(expected_hooks); + s_installed.reserve(expected_hooks); + } + + static HookId Register( + const TCHAR* HookName, + const TCHAR* FunctionPath, + UnrealScriptFunctionCallable Pre = [](UnrealScriptFunctionCallableContext&, void*) {}, + UnrealScriptFunctionCallable Post = [](UnrealScriptFunctionCallableContext&, void*) {}, + void* CustomData = nullptr) + { + if (!HookName || !FunctionPath) + { + Output::send(STR("[EventHookRegistry] null HookName or FunctionPath\n")); + return InvalidHook; + } + + HookRequest Req{}; + Req.Id = s_next_id++; + Req.Name = HookName; + Req.FunctionPath = FunctionPath; + Req.Pre = Pre; + Req.Post = Post; + Req.CustomData = CustomData; + + StringType ClassPart, FuncPart; + if (!SplitFunctionPath(Req.FunctionPath, ClassPart, FuncPart)) + { + Output::send( + STR("[EventHookRegistry] '{}': cannot parse '{}' -- expected ") + STR("/Path/To/Package.ClassName:FunctionName\n"), HookName, FunctionPath); + return InvalidHook; + } + Req.ClassName = FName(ClassPart.c_str(), FNAME_Add); + Req.FunctionName = FName(FuncPart.c_str(), FNAME_Add); + + // Fast path: already loaded, install now, never touch ProcessEvent. + if (UFunction* Fn = UObjectGlobals::StaticFindObject(nullptr, nullptr, Req.FunctionPath.c_str())) + { + Install(Req, Fn); + return Req.Id; + } + + Output::send( + STR("[EventHookRegistry] '{}': '{}' not loaded yet, deferring\n"), HookName, FunctionPath); + + s_pending.push_back(std::move(Req)); + EnsureWatcher(); + return s_next_id - 1; + } + + static bool Deregister(const TCHAR* HookName) + { + if (!HookName) return false; + const StringType Name{ HookName }; + + for (size_t i = 0; i < s_installed.size(); ++i) + { + if (s_installed[i].Name != Name) continue; + Uninstall(s_installed[i]); + s_installed.erase(s_installed.begin() + i); + return true; + } + + for (size_t i = 0; i < s_pending.size(); ++i) + { + if (s_pending[i].Name != Name) continue; + s_pending.erase(s_pending.begin() + i); + return true; + } + + Output::send( + STR("[EventHookRegistry] Deregister: no hook named '{}'\n"), HookName); + return false; + } + + static bool Deregister(HookId Id) + { + for (size_t i = 0; i < s_installed.size(); ++i) + { + if (s_installed[i].Id != Id) continue; + Uninstall(s_installed[i]); + s_installed.erase(s_installed.begin() + i); + return true; + } + for (size_t i = 0; i < s_pending.size(); ++i) + { + if (s_pending[i].Id != Id) continue; + s_pending.erase(s_pending.begin() + i); + return true; + } + return false; + } + + static bool IsInstalled(HookId Id) + { + for (const auto& H : s_installed) if (H.Id == Id) return true; + return false; + } + + static size_t PendingCount() { return s_pending.size(); } + + // The ProcessEvent callback cannot be unregistered in UE4SS, insteasd it drops all state and sets s_shutdown; the watcher + // then costs one bool test per ProcessEvent for the rest of the process. + static void Cleanup() + { + for (auto& H : s_installed) Uninstall(H); + s_installed.clear(); + s_pending.clear(); + s_shutdown = true; + } + + private: + static void EnsureWatcher() + { + if (s_watcher_installed) return; + Hook::RegisterProcessEventPreCallback(&EventHookRegistry::OnProcessEvent); + s_watcher_installed = true; + Output::send(STR("[EventHookRegistry] ProcessEvent watcher installed\n")); + } + + // HOT PATH. This runs for effectively every UFunction call in the game. + // Every line before the loop is there to make the common case a load, + // a branch, and a return. + static void OnProcessEvent(UObject* Context, UFunction* Function, void*) + { + if (s_shutdown || s_pending.empty()) return; + if (!Context || !Function) return; + + UClass* Cls = Context->GetClassPrivate(); + if (!Cls) return; + const FName ClsName = Cls->GetNamePrivate(); + + // Backwards so erase() doesn't disturb indices we haven't visited. + for (size_t i = s_pending.size(); i-- > 0;) + { + HookRequest& Req = s_pending[i]; + if (Req.ClassName != ClsName) continue; + + UFunction* Target = UObjectGlobals::StaticFindObject(nullptr, nullptr, Req.FunctionPath.c_str()); + + if (!Target) + { + if (Req.FunctionName != Function->GetNamePrivate()) continue; + + Output::send( + STR("[EventHookRegistry] '{}': '{}' still will not resolve by path; ") + STR("falling back to the dispatched UFunction {}. If your hook fires on the ") + STR("wrong object, your path string is wrong.\n"), + Req.Name, Req.FunctionPath, Function->GetFullName()); + Target = Function; + } + + HookRequest Owned = std::move(Req); + s_pending.erase(s_pending.begin() + i); + Install(Owned, Target); + } + } + + static void Install(HookRequest& Req, UFunction* Fn) + { + if (!Fn) return; + + for (const auto& H : s_installed) + { + if (H.Function != Fn) continue; + Output::send( + STR("[EventHookRegistry] '{}': {} is already hooked by '{}'. ") + STR("Installing anyway -- both callbacks will fire.\n"), + Req.Name, Fn->GetFullName(), H.Name); + break; + } + + auto _pre = Req.Pre; + if (!_pre) _pre = [](UnrealScriptFunctionCallableContext&, void*) {}; + auto _post = Req.Post; + if (!_post) _post = [](UnrealScriptFunctionCallableContext&, void*) {}; + + auto [PreId, PostId] = UObjectGlobals::RegisterHook(Fn, _pre, _post, Req.CustomData); + + s_installed.push_back(InstalledHook{ Req.Id, Req.Name, Fn, PreId, PostId }); + + Output::send( + STR("[EventHookRegistry] '{}': installed on {} (pre={}, post={})\n"), + Req.Name, Fn->GetFullName(), PreId, PostId); + } + + static void Uninstall(InstalledHook& H) + { + if (!H.Function) return; + UObjectGlobals::UnregisterHook(H.Function, std::make_tuple(H.PreId, H.PostId)); + H.Function = nullptr; + } + + // "/Game/Pal/Blueprint/UI/PalStatus/WBP_PalStatus.WBP_PalStatus_C:Construct" + // -> ClassName "WBP_PalStatus_C", FunctionName "Construct" + static bool SplitFunctionPath(const StringType& Path, StringType& OutClass, StringType& OutFunction) + { + const auto Colon = Path.rfind(STR(':')); + if (Colon == StringType::npos || Colon + 1 >= Path.size()) return false; + + OutFunction = Path.substr(Colon + 1); + + const StringType ObjectPart = Path.substr(0, Colon); + const auto Dot = ObjectPart.rfind(STR('.')); + OutClass = (Dot == StringType::npos) ? ObjectPart : ObjectPart.substr(Dot + 1); + + return !OutClass.empty() && !OutFunction.empty(); + } + }; +} \ No newline at end of file diff --git a/Static/LogGate.hpp b/Static/LogGate.hpp new file mode 100644 index 0000000..ef4038f --- /dev/null +++ b/Static/LogGate.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace PalUIExtension::Static +{ + + inline constexpr bool kVerboseLogging = false; + inline constexpr bool kProfileHotPath = true; + +} \ No newline at end of file diff --git a/Static/WidgetRegistry.hpp b/Static/WidgetRegistry.hpp new file mode 100644 index 0000000..7d7b495 --- /dev/null +++ b/Static/WidgetRegistry.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include + +namespace PalUIExtension::Static +{ + using namespace RC; + using namespace RC::Unreal; + using Api::Widget; + + class WidgetRegistry + { + public: + using Generation = uint32_t; + using KeyType = std::basic_string; + + // ---- generation control -------------------------------------- + + // Advances the generation and drops every existing entry. + + static auto BeginGeneration() -> Generation + { + ++s_generation; + s_entries.clear(); + Output::send(STR("[WidgetRegistry] generation advanced to {}\n"), s_generation); + return s_generation; + } + + // Calls Widget::RemoveFromParent on every entry still in the + // current generation, THEN advances the generation. + // + // If an entry's pointer is already stale, this will call RemoveFromParent on whatever + // object now lives at that address. That is the one real cost of the gap this class cannot close. Keep your generations short. + static auto TeardownGeneration() -> void + { + for (auto& [Key, E] : s_entries) + { + if (E.Gen != s_generation || !E.Obj) continue; + Widget W{ E.Obj }; + if (W) W.RemoveFromParent(); + } + BeginGeneration(); + } + + static auto CurrentGeneration() -> Generation { return s_generation; } + + // ---- registration ----------------------------------------------- + + static auto Register(const KeyType& Key, UObject* Obj) -> bool + { + if (!Obj){ + Output::send(STR("[WidgetRegistry] Register('{}'): refusing a null object\n"), Key.c_str()); + return false; + } + + if (s_entries.find(Key) != s_entries.end()) + { + Output::send(STR("[WidgetRegistry] Register('{}'): overwriting existing entry\n"), Key.c_str()); + } + + s_entries[Key] = Entry{ Obj, s_generation }; + return true; + } + + static auto Register(const TCHAR* Key, UObject* Obj) -> bool + { + return Register(KeyType{ Key }, Obj); + } + + static auto Unregister(const KeyType& Key) -> bool + { + return s_entries.erase(Key) > 0; + } + + // ---- lookup ----------------------------------------------------- + + // Returns nullptr if the key is missing OR if the stored entry predates the current generation. + // A stale generation is treated identically to "never registered," and the dead entry is erased + // on the way out so it doesn't accumulate. + static auto GetObject(const KeyType& Key) -> UObject* + { + auto It = s_entries.find(Key); + if (It == s_entries.end()) return nullptr; + + if (It->second.Gen != s_generation) + { + Output::send(STR("[WidgetRegistry] GetObject('{}'): stale generation ({} vs current {}), dropping\n"), Key.c_str(), It->second.Gen, s_generation); + s_entries.erase(It); + return nullptr; + } + + return It->second.Obj; + } + + static auto GetObject(const TCHAR* Key) -> UObject* + { + return GetObject(KeyType{ Key }); + } + + // Typed lookup. T must be one of the wrapper types in this tree (Widget, TextBlock, CanvasPanel, PanelWidget, ...) exposing + // ClassOf() -- the same requirement UnrealWrapper's derived types already have. Constructing T{Obj} runs T's own IsA() + // check, so a cross-class stale pointer comes back as an invalid wrapper (IsValid() == false) instead of a wrong-typed one. + template + static auto Get(const KeyType& Key) -> T + { + UObject* Obj = GetObject(Key); + return Obj ? T{ Obj } : T{}; + } + + template + static auto Get(const TCHAR* Key) -> T + { + return Get(KeyType{ Key }); + } + + static auto Contains(const KeyType& Key) -> bool + { + return GetObject(Key) != nullptr; + } + + static auto Count() -> size_t { return s_entries.size(); } + + // Drops everything without touching the Slate tree. Same caveat as BeginGeneration(): this does not remove widgets from their parents, it just makes the registry stop tracking them. + static auto Clear() -> void { s_entries.clear(); } + + private: + struct Entry { UObject* Obj{}; Generation Gen{}; }; + + inline static std::unordered_map s_entries{}; + inline static Generation s_generation{ 1 }; + }; +} \ No newline at end of file diff --git a/dllmain.cpp b/dllmain.cpp new file mode 100644 index 0000000..3ca339f --- /dev/null +++ b/dllmain.cpp @@ -0,0 +1,181 @@ +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +using namespace RC; +using namespace RC::Unreal; + +// D:\Project\PalworldMods\PalUIExtension\post_build.bat "$(TargetPath)" "C:\Program Files (x86)\Steam\steamapps\common\Palworld\Pal\Binaries\Win64\ue4ss\Mods\PalUIExtension\dlls\main.dll" +namespace PalUIExtension +{ + class PalUIExtension : public RC::CppUserModBase + { + private: + std::vector> m_features{}; + bool m_installed = false; + + // The static back-reference exists because UE4SS hook callbacks + // for FUNCTIONS take a void* CustomData but the ENGINE-level + // callbacks (LoadMap) do not -- they take a std::function with no + // user data slot. A capturing lambda would work for the latter, + // but then the two teardown paths would reach the mod by two + // different mechanisms, and a reader would have to work out + // whether that difference meant anything. It does not. One + // pointer, one mechanism. + // + // Single-instance is a safe assumption here: UE4SS constructs + // exactly one CppUserModBase per mod DLL via start_mod(). + inline static PalUIExtension* s_instance{ nullptr }; + + public: + PalUIExtension() : CppUserModBase() + { + ModName = STR("PalUIExtension"); + ModVersion = STR("0.2"); + ModDescription = STR("PalUIExtension"); + ModAuthors = STR("FerrenFX"); + + s_instance = this; + } + + ~PalUIExtension() override + { + // Order matters: hooks first, so nothing can fire a callback + // into features that are about to be destroyed. + Static::EventHookRegistry::Cleanup(); + m_features.clear(); + + if (s_instance == this) s_instance = nullptr; + } + + auto on_unreal_init() -> void override + { + if (m_installed) return; + + BuildFeatures(); + + // +2 for the game-mode hooks below, which go through the same + // registry and would otherwise force a reallocation. + Static::EventHookRegistry::Initialize(m_features.size() * 4 + 2); + + for (auto& Feature : m_features) + { + Feature->InstallHooks(); + Output::send( + STR("[PalUIExtension] installed feature '{}'\n"), Feature->Name()); + } + + InstallWorldLifecycleHooks(); + + m_installed = true; + } + + private: + auto BuildFeatures() -> void + { + m_features.push_back(std::make_unique()); + m_features.push_back(std::make_unique()); + m_features.push_back(std::make_unique()); + } + + auto InstallWorldLifecycleHooks() -> void + { + const auto RestartId = Static::EventHookRegistry::Register( + STR("PalUIExtension::GameModeRestart"), + Api::Pal::PalGameMode::RestartGamePath(), + &PalUIExtension::OnRestartGamePre, // PRE: the world is still intact + nullptr, + this); + + if (RestartId == Static::EventHookRegistry::InvalidHook) + { + Output::send( + STR("[PalUIExtension] RestartGame hook was rejected. Teardown falls back to " + "LoadMap only, which is later but still correct.\n")); + } + + // Engine level. No CustomData parameter, hence s_instance. + Hook::RegisterLoadMapPreCallback( + [](UEngine*, FWorldContext&, FURL, UPendingNetGame*, FString&) + -> std::pair + { + if (s_instance) + { + s_instance->TeardownAllFeatures(STR("LoadMap")); + } + + return { true, true }; + }); + + Output::send( + STR("[PalUIExtension] world lifecycle hooks installed (restart={})\n"), RestartId); + } + + static void OnRestartGamePre(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) + { + auto* Self = static_cast(CustomData); + if (!Self) return; + + Self->TeardownAllFeatures(STR("RestartGame")); + } + auto TeardownAllFeatures(const TCHAR* Reason) -> void + { + + Static::ClassCache::Flush(); + Static::WidgetRegistry::BeginGeneration(); + + int32 TornDown = 0; + + for (auto& Feature : m_features) + { + if (!Feature) continue; + + try + { + Feature->OnWorldTeardown(); + ++TornDown; + } + catch (...) + { + Output::send( + STR("[PalUIExtension] '{}' threw during teardown; its cached pointers " + "are now stale.\n"), Feature->Name()); + } + } + + Output::send( + STR("[PalUIExtension] teardown via {}: {} features\n"), Reason, TornDown); + } + + static auto to_wstring(const char* s) -> std::wstring + { + if (!s) return std::wstring{}; + return std::wstring(s, s + std::strlen(s)); // ASCII only; what() is never non-ASCII here + } + }; +} + +#define MY_AWESOME_MOD_API __declspec(dllexport) +extern "C" +{ + MY_AWESOME_MOD_API RC::CppUserModBase* start_mod() + { + return new PalUIExtension::PalUIExtension(); + } + + MY_AWESOME_MOD_API void uninstall_mod(RC::CppUserModBase* mod) + { + delete mod; + } +} \ No newline at end of file diff --git a/night_work_icons.jpg b/night_work_icons.jpg new file mode 100644 index 0000000..8dddac5 Binary files /dev/null and b/night_work_icons.jpg differ diff --git a/palbox_numbers.jpg b/palbox_numbers.jpg new file mode 100644 index 0000000..20aadb9 Binary files /dev/null and b/palbox_numbers.jpg differ diff --git a/palstatus_numbers.png b/palstatus_numbers.png new file mode 100644 index 0000000..095f1d1 Binary files /dev/null and b/palstatus_numbers.png differ diff --git a/post_build.bat b/post_build.bat new file mode 100644 index 0000000..58b8166 --- /dev/null +++ b/post_build.bat @@ -0,0 +1,12 @@ +echo off +set output_dll=%1 +set game_mod_output_dll=%2 +set game_mod_output_dir=%game_mod_output_dll:main.dll = % +set game_mod_back=%game_mod_output_dll:~0,-3%.bak +echo Creating backup at %game_mod_output_dir% +COPY %output_dll% %game_mod_output_dir% +DEL %game_mod_output_dll% +echo Copying %output_dll% to %game_mod_output_dir% +COPY %output_dll% %game_mod_output_dir% +echo Removing backup at %game_mod_back% +DEL %game_mod_back% \ No newline at end of file