initial commit; cv 0.0.3
This commit is contained in:
196
Api/BoxPalList.hpp
Normal file
196
Api/BoxPalList.hpp
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#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<BoxPalListWidget>(), DebugTag()) {}
|
||||||
|
|
||||||
|
auto BaseObject() const -> UObject* { return BoundObject(STR("WBP_BoxPalListBase")); }
|
||||||
|
|
||||||
|
auto CanDragDrop() const -> std::optional<bool>
|
||||||
|
{
|
||||||
|
return PropertyBoolValue(STR("CanDragDrop"));
|
||||||
|
}
|
||||||
|
auto DisplaysBanMarkInExpedition() const -> std::optional<bool>
|
||||||
|
{
|
||||||
|
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<BoxPalListBaseWidget>(), DebugTag()) {}
|
||||||
|
|
||||||
|
auto ScrollList() const -> UObject* { return BoundObject(STR("WBP_BoxPalScrollList")); }
|
||||||
|
|
||||||
|
auto LastSelectedPage() const -> std::optional<int32> { return PropertyValue<int32>(STR("LastSelectedPageNum")); }
|
||||||
|
auto MaxPageNum() const -> std::optional<int32> { return PropertyValue<int32>(STR("MaxPageNum")); }
|
||||||
|
auto SkipPageNum() const -> std::optional<int32> { return PropertyValue<int32>(STR("SkipPageNum")); }
|
||||||
|
|
||||||
|
auto IsValidPageAction() const -> std::optional<bool> { return PropertyBoolValue(STR("bIsValidPageAction")); }
|
||||||
|
auto IsSkippingPage() const -> std::optional<bool> { 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<int32>
|
||||||
|
{
|
||||||
|
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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
0
Api/Canvas.hpp
Normal file
0
Api/Canvas.hpp
Normal file
32
Api/CanvasPanel.hpp
Normal file
32
Api/CanvasPanel.hpp
Normal file
@@ -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<CanvasPanel>(), 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{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
91
Api/CanvasPanelSlot.hpp
Normal file
91
Api/CanvasPanelSlot.hpp
Normal file
@@ -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<CanvasPanelSlot>(), 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<int32> { return PropertyValue<int32>(STR("ZOrder")); }
|
||||||
|
auto GetAutoSize() const -> std::optional<bool> { return PropertyValue<bool>(STR("bAutoSize")); }
|
||||||
|
auto GetLayout() const -> std::optional<FAnchorData_> { return PropertyValue<FAnchorData_>(STR("LayoutData")); }
|
||||||
|
|
||||||
|
auto GetSize() -> std::optional<FVector2D_> { return CallForReturn<FVector2D_>(STR("GetSize")); }
|
||||||
|
auto GetPosition() -> std::optional<FVector2D_> { return CallForReturn<FVector2D_>(STR("GetPosition")); }
|
||||||
|
auto GetAlignment() -> std::optional<FVector2D_> { return CallForReturn<FVector2D_>(STR("GetAlignment")); }
|
||||||
|
auto GetOffsets() -> std::optional<FMargin_> { return CallForReturn<FMargin_>(STR("GetOffsets")); }
|
||||||
|
auto GetAnchors() -> std::optional<FAnchors_> { return CallForReturn<FAnchors_>(STR("GetAnchors")); }
|
||||||
|
};
|
||||||
|
}
|
||||||
29
Api/HorizontalBox.hpp
Normal file
29
Api/HorizontalBox.hpp
Normal file
@@ -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<HorizontalBox>(), 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{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
52
Api/HorizontalBoxSlot.hpp
Normal file
52
Api/HorizontalBoxSlot.hpp
Normal file
@@ -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<HorizontalBoxSlot>(), 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<FSlateChildSize_> { return PropertyValue<FSlateChildSize_>(STR("Size")); }
|
||||||
|
auto GetPadding() const -> std::optional<FMargin_> { return PropertyValue<FMargin_>(STR("Padding")); }
|
||||||
|
auto GetHorizontalAlignment() const -> std::optional<EHorizontalAlignment_> { return PropertyValue<EHorizontalAlignment_>(STR("HorizontalAlignment")); }
|
||||||
|
auto GetVerticalAlignment() const -> std::optional<EVerticalAlignment_> { return PropertyValue<EVerticalAlignment_>(STR("VerticalAlignment")); }
|
||||||
|
};
|
||||||
|
}
|
||||||
246
Api/MainMenuPalWidget.hpp
Normal file
246
Api/MainMenuPalWidget.hpp
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#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 <Api/Pal/PalIndividual.hpp>
|
||||||
|
|
||||||
|
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<MainMenuPalWidget>(), 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<CanvasPanel>(Name); }
|
||||||
|
|
||||||
|
auto CachedHandle() const -> UObject* { return BoundObject(STR("CachedIndividualHandle")); }
|
||||||
|
|
||||||
|
auto CachedSpecies() const -> std::optional<FName>
|
||||||
|
{
|
||||||
|
return Pal::PalIndividual::CharacterIdFromHandle(CachedHandle());
|
||||||
|
}
|
||||||
|
auto CachedIdentity() const -> Pal::PalIdentity
|
||||||
|
{
|
||||||
|
return Pal::PalIndividual::Identify(CachedHandle());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- screen state --------------------------------------------
|
||||||
|
|
||||||
|
auto IsShowingStatusWithList() const -> std::optional<bool> { return PropertyBoolValue(STR("isShowStatusWithList")); }
|
||||||
|
auto IsLocked() const -> std::optional<bool> { return PropertyBoolValue(STR("Is Lock")); }
|
||||||
|
auto IsEditingNickName() const -> std::optional<bool> { return PropertyBoolValue(STR("IsEditingNickName")); }
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// WIDGET ARRAYS
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
auto PalPanels() const -> std::vector<UObject*> { return ReadObjectArray(STR("PalPanels")); }
|
||||||
|
auto ActiveSkillPanels() const -> std::vector<UObject*> { return ReadObjectArray(STR("ActiveSkillPanelArray")); }
|
||||||
|
auto PassiveSkillPanels() const -> std::vector<UObject*> { return ReadObjectArray(STR("PassiveSkillPanelArray")); }
|
||||||
|
auto ConditionWidgets() const -> std::vector<UObject*> { return ReadObjectArray(STR("ConditionWidgetArray")); }
|
||||||
|
auto RarityWidgets() const -> std::vector<UObject*> { return ReadObjectArray(STR("RarityWidgetArray")); }
|
||||||
|
|
||||||
|
// The engine's spelling of "SideInfo" is "SindeInfo". Lol.
|
||||||
|
auto SideInfoConditionWidgets() const -> std::vector<UObject*> { 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<UObject*>
|
||||||
|
{
|
||||||
|
std::vector<UObject*> Out;
|
||||||
|
|
||||||
|
auto* Arr = TryPropertyPtr<TArrayRead_<UObject*>>(PropertyName);
|
||||||
|
if (!Arr) return Out;
|
||||||
|
|
||||||
|
if (!Arr->IsPlausible(kMaxPlausibleEntries))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] '{}' does not look like a live TArray (Num={}, Max={})\n"),
|
||||||
|
m_tag, PropertyName, Arr->Num, Arr->Max);
|
||||||
|
return Out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Out.reserve(static_cast<size_t>(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) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
72
Api/Mod/PalDetail_PalIconNightEye.hpp
Normal file
72
Api/Mod/PalDetail_PalIconNightEye.hpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UmgTypes.hpp>
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#include <Api/Widget.hpp>
|
||||||
|
#include <Api/UserWidget.hpp>
|
||||||
|
#include <Api/WidgetTree.hpp>
|
||||||
|
#include <Api/TextBlock.hpp>
|
||||||
|
#include <Api/CanvasPanel.hpp>
|
||||||
|
|
||||||
|
#include <Api/Pal/PalIndividual.hpp>
|
||||||
|
#include <Api/Pal/PalIndividualCharacterHandle.hpp>
|
||||||
|
#include <Api/Pal/PalIndividualCharacterParameter.hpp>
|
||||||
|
|
||||||
|
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<PalDetail_PalIconNightEye>(), DebugTag()) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetIconVisibility() -> std::optional<bool>
|
||||||
|
{
|
||||||
|
return CallForReturn<bool>(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<LogLevel::Error>(
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
101
Api/Mod/PalDetail_PalNumber.hpp
Normal file
101
Api/Mod/PalDetail_PalNumber.hpp
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UmgTypes.hpp>
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#include <Api/Widget.hpp>
|
||||||
|
#include <Api/UserWidget.hpp>
|
||||||
|
#include <Api/WidgetTree.hpp>
|
||||||
|
#include <Api/TextBlock.hpp>
|
||||||
|
#include <Api/CanvasPanel.hpp>
|
||||||
|
|
||||||
|
#include <Api/Pal/PalIndividual.hpp>
|
||||||
|
#include <Api/Pal/PalIndividualCharacterHandle.hpp>
|
||||||
|
#include <Api/Pal/PalIndividualCharacterParameter.hpp>
|
||||||
|
|
||||||
|
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<PalDetail_PalNumberWidget>(), 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<FName>
|
||||||
|
{
|
||||||
|
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<bool>(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<FName>{
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
101
Api/Mod/PalStatus_PalNumber.hpp
Normal file
101
Api/Mod/PalStatus_PalNumber.hpp
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UmgTypes.hpp>
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#include <Api/Widget.hpp>
|
||||||
|
#include <Api/UserWidget.hpp>
|
||||||
|
#include <Api/WidgetTree.hpp>
|
||||||
|
#include <Api/TextBlock.hpp>
|
||||||
|
#include <Api/CanvasPanel.hpp>
|
||||||
|
|
||||||
|
#include <Api/Pal/PalIndividual.hpp>
|
||||||
|
#include <Api/Pal/PalIndividualCharacterHandle.hpp>
|
||||||
|
#include <Api/Pal/PalIndividualCharacterParameter.hpp>
|
||||||
|
|
||||||
|
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<PalStatus_PalNumberWidget>(), 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<FName>
|
||||||
|
{
|
||||||
|
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<bool>(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<FName>{
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
34
Api/Overlay.hpp
Normal file
34
Api/Overlay.hpp
Normal file
@@ -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<Overlay>(), 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{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
45
Api/OverlaySlot.hpp
Normal file
45
Api/OverlaySlot.hpp
Normal file
@@ -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<OverlaySlot>(), 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<FMargin_> { return PropertyValue<FMargin_>(STR("Padding")); }
|
||||||
|
auto GetHorizontalAlignment()const -> std::optional<EHorizontalAlignment_> { return PropertyValue<EHorizontalAlignment_>(STR("HorizontalAlignment")); }
|
||||||
|
auto GetVerticalAlignment() const -> std::optional<EVerticalAlignment_> { return PropertyValue<EVerticalAlignment_>(STR("VerticalAlignment")); }
|
||||||
|
};
|
||||||
|
}
|
||||||
136
Api/Pal/PalCharacter.hpp
Normal file
136
Api/Pal/PalCharacter.hpp
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#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<PalCharacter>(), 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<UObject*>(STR("GetIndividualParameter")).value_or(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Identify() -> PalIdentity
|
||||||
|
{
|
||||||
|
return PalIndividual::IdentifyFromParameter(GetIndividualParameterObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CharacterId() -> std::optional<FName>
|
||||||
|
{
|
||||||
|
return PalIndividual::CharacterId(GetIndividualParameterObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto BlueprintClassName() const -> std::optional<StringType>
|
||||||
|
{
|
||||||
|
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<bool> { return CallForReturn<bool>(STR("IsInitialized")); }
|
||||||
|
auto IsPart() -> std::optional<bool> { return CallForReturn<bool>(STR("IsPart")); }
|
||||||
|
auto IsCooping() -> std::optional<bool> { return CallForReturn<bool>(STR("IsCooping")); }
|
||||||
|
auto IsPreCooping() -> std::optional<bool> { return CallForReturn<bool>(STR("IsPreCooping")); }
|
||||||
|
auto IsLocalHiddenForCutscene() -> std::optional<bool> { return CallForReturn<bool>(STR("IsLocalHiddenForCutscene")); }
|
||||||
|
auto IsUseCustomAutoAimTarget() -> std::optional<bool> { return CallForReturn<bool>(STR("IsUseCustomAutoAimTarget")); }
|
||||||
|
auto IsAllActiveSkillCooldownFinished() -> std::optional<bool> { return CallForReturn<bool>(STR("IsAllActiveSkillCooldownFinished")); }
|
||||||
|
auto GetBattleMode() -> std::optional<bool> { return CallForReturn<bool>(STR("GetBattleMode")); }
|
||||||
|
auto GetTalkMode() -> std::optional<bool> { return CallForReturn<bool>(STR("GetTalkMode")); }
|
||||||
|
auto GetActiveActorFlag() -> std::optional<bool> { return CallForReturn<bool>(STR("GetActiveActorFlag")); }
|
||||||
|
|
||||||
|
auto IsActiveSkillCooldownFinished(EPalWazaID_ WazaID) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
369
Api/Pal/PalDatabaseCharacterParameter.hpp
Normal file
369
Api/Pal/PalDatabaseCharacterParameter.hpp
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#include <Static/LogGate.hpp>
|
||||||
|
#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<PalDatabaseCharacterParameter>(), DebugTag()) {}
|
||||||
|
|
||||||
|
|
||||||
|
static auto StripVariantPrefix(FName CharacterId) -> std::optional<FName>
|
||||||
|
{
|
||||||
|
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<int32>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 };
|
||||||
|
return CallForReturn(STR("GetZukanIndex"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetZukanIndexResolved(FName CharacterId) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
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<LogLevel::Verbose>(
|
||||||
|
STR("[PalDatabase] ZukanIndex: '{}' had none, resolved via base '{}' -> {}\n"),
|
||||||
|
CharacterId.ToString(), Base->ToString(), *Fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto GetLocalizedBaseCharacterName(FName CharacterId) -> std::optional<FText>
|
||||||
|
{
|
||||||
|
return GetLocalizedCharacterName(ResolveBaseRowName(CharacterId));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetTribe(FName RowName) -> std::optional<EPalTribeID_>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; EPalTribeID_ ReturnValue; } p{ RowName, 0 };
|
||||||
|
return CallForReturn(STR("GetTribe"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetSize(FName RowName) -> std::optional<EPalSizeType_>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; EPalSizeType_ ReturnValue; } p{ RowName, EPalSizeType_::None };
|
||||||
|
return CallForReturn(STR("GetSize"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetGenusCategory(FName RowName) -> std::optional<EPalGenusCategoryType_>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; EPalGenusCategoryType_ ReturnValue; } p{ RowName, EPalGenusCategoryType_::None };
|
||||||
|
return CallForReturn(STR("GetGenusCategory"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetOrganizationType(FName RowName) -> std::optional<EPalOrganizationType_>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; EPalOrganizationType_ ReturnValue; } p{ RowName, EPalOrganizationType_::None };
|
||||||
|
return CallForReturn(STR("GetOrganizationType"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetWeaponType(FName RowName) -> std::optional<EPalWeaponType_>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; EPalWeaponType_ ReturnValue; } p{ RowName, EPalWeaponType_::None };
|
||||||
|
return CallForReturn(STR("GetWeaponType"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetBattleBGM(FName RowName) -> std::optional<EPalBattleBGMType_>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; EPalBattleBGMType_ ReturnValue; } p{ RowName, EPalBattleBGMType_::None };
|
||||||
|
return CallForReturn(STR("GetBattleBGM"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetPalAwakeningItemElement(FName ItemId) -> std::optional<EPalElementType_>
|
||||||
|
{
|
||||||
|
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<ElementTypePair>
|
||||||
|
{
|
||||||
|
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<EPalWorkSuitability_>
|
||||||
|
{
|
||||||
|
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<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetWeaponEquip"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetUseBossHPGauge(FName RowName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetUseBossHPGauge"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetNocturnal(FName RowName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetNocturnal"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetIsTowerBoss(FName RowName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetIsTowerBoss"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetIsRaidBoss(FName RowName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetIsRaidBoss"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetIsPredatorBoss(FName RowName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetIsPredatorBoss"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetIsPal(FName RowName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetIsPal"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetIsLegend(FName RowName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetIsLegend"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetIsBoss(FName RowName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; bool ReturnValue; } p{ RowName, false };
|
||||||
|
return CallForReturn(STR("GetIsBoss"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto IsArenaUnusableItem(FName ItemId) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName ItemId; bool ReturnValue; } p{ ItemId, false };
|
||||||
|
return CallForReturn(STR("IsArenaUnusableItem"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetViewingDistance_cm(FName RowName) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 };
|
||||||
|
return CallForReturn(STR("GetViewingDistance_cm"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetViewingAngle_Degree(FName RowName) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 };
|
||||||
|
return CallForReturn(STR("GetViewingAngle_Degree"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetRarity(FName RowName) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 };
|
||||||
|
return CallForReturn(STR("GetRarity"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetFoodAmount(FName RowName) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; int32 ReturnValue; } p{ RowName, 0 };
|
||||||
|
return CallForReturn(STR("GetFoodAmount"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetPriceRate(FName RowName) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; float ReturnValue; } p{ RowName, 0.f };
|
||||||
|
return CallForReturn(STR("GetPriceRate"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetHearingRate(FName RowName) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; float ReturnValue; } p{ RowName, 0.f };
|
||||||
|
return CallForReturn(STR("GetHearingRate"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetExpRatio(FName RowName) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; float ReturnValue; } p{ RowName, 0.f };
|
||||||
|
return CallForReturn(STR("GetExpRatio"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetCaptureRateCorrect(FName RowName) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; float ReturnValue; } p{ RowName, 0.f };
|
||||||
|
return CallForReturn(STR("GetCaptureRateCorrect"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetBPClassName(FName RowName) -> std::optional<FName>
|
||||||
|
{
|
||||||
|
struct P { FName RowName; FName ReturnValue; } p{ RowName, {} };
|
||||||
|
return CallForReturn(STR("GetBPClassName"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetFirstDefeatRewardItemID(FName CharacterID) -> std::optional<FName>
|
||||||
|
{
|
||||||
|
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<FName>
|
||||||
|
{
|
||||||
|
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<FText>
|
||||||
|
{
|
||||||
|
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<FText>
|
||||||
|
{
|
||||||
|
struct P { FName UniqueNPCID; FText OutText; } p{ UniqueNPCID, {} };
|
||||||
|
if (!Call(STR("GetLocalizedUniqueNPCName"), p)) return std::nullopt;
|
||||||
|
return p.OutText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- friendship ------------------------------------------------
|
||||||
|
|
||||||
|
auto GetMinFriendshipRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetMinFriendshipRank")); }
|
||||||
|
auto GetMaxFriendshipRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetMaxFriendshipRank")); }
|
||||||
|
auto GetFriendshipRank(int32 FriendshipPoint) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 FriendshipPoint; int32 ReturnValue; } p{ FriendshipPoint, 0 };
|
||||||
|
return CallForReturn(STR("GetFriendshipRank"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto CalcFriendshipProgress(int32 FriendshipPoint) -> std::optional<float>
|
||||||
|
{
|
||||||
|
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<int32>
|
||||||
|
{
|
||||||
|
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<int32>
|
||||||
|
{
|
||||||
|
struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 };
|
||||||
|
return CallForReturn(STR("GetHP"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetMeleeAttack(UObject* IndividualParameter) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 };
|
||||||
|
return CallForReturn(STR("GetMeleeAttack"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetShotAttack(UObject* IndividualParameter) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 };
|
||||||
|
return CallForReturn(STR("GetShotAttack"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDefense(UObject* IndividualParameter) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 };
|
||||||
|
return CallForReturn(STR("GetDefense"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetCraftSpeed(UObject* IndividualParameter) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 };
|
||||||
|
return CallForReturn(STR("GetCraftSpeed"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetSupport(UObject* IndividualParameter) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { UObject* IndividualParameter; int32 ReturnValue; } p{ IndividualParameter, 0 };
|
||||||
|
return CallForReturn(STR("GetSupport"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto HigherLevelOtomoFromTrainer(UObject* IndividualCharacterParameter) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
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<int32>
|
||||||
|
{
|
||||||
|
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<CharType>(A - STR('a') + STR('A'));
|
||||||
|
if (B >= STR('a') && B <= STR('z')) B = static_cast<CharType>(B - STR('a') + STR('A'));
|
||||||
|
if (A != B) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
301
Api/Pal/PalGameMode.hpp
Normal file
301
Api/Pal/PalGameMode.hpp
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <Unreal/FString.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
|
||||||
|
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<PalGameModeBase>(), 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<EPalGameModeType_>
|
||||||
|
{
|
||||||
|
return PropertyValue<EPalGameModeType_>(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<PalGameMode>(), 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<TCHAR>: 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<PalGameModeLogin>(), 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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
127
Api/Pal/PalIndividual.hpp
Normal file
127
Api/Pal/PalIndividual.hpp
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api\UmgTypes.hpp>
|
||||||
|
#include <Api\UnrealWrapper.hpp>
|
||||||
|
|
||||||
|
#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<FName> CharacterId; // the species. "SheepBall", etc.
|
||||||
|
std::optional<int32> Level;
|
||||||
|
std::optional<int32> Rank;
|
||||||
|
std::optional<EPalGenderType_> Gender;
|
||||||
|
std::optional<bool> 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<UObject*>(STR("TryGetIndividualParameter")).value_or(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto CharacterId(UObject* Parameter) -> std::optional<FName>
|
||||||
|
{
|
||||||
|
if (!Parameter) return std::nullopt;
|
||||||
|
LooseObject P{ Parameter, STR("PalIndividualCharacterParameter") };
|
||||||
|
return P.CallForReturn<FName>(STR("GetCharacterID"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto CharacterIdFromHandle(UObject* Handle) -> std::optional<FName>
|
||||||
|
{
|
||||||
|
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<FName>(STR("GetCharacterID"));
|
||||||
|
Out.Level = P.CallForReturn<int32>(STR("GetLevel"));
|
||||||
|
Out.Rank = P.CallForReturn<int32>(STR("GetRank"));
|
||||||
|
Out.Gender = P.CallForReturn<EPalGenderType_>(STR("GetGenderType"));
|
||||||
|
Out.bRare = P.CallForReturn<bool>(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<FPalInstanceID>
|
||||||
|
{
|
||||||
|
if (!Handle) return std::nullopt;
|
||||||
|
// FPalInstanceID
|
||||||
|
LooseObject H{ Handle, STR("PalIndividualCharacterHandle") };
|
||||||
|
auto _Out = H.CallForReturn<FPalInstanceID>(STR("GetIndividualID"));
|
||||||
|
|
||||||
|
if (!_Out.has_value()) return std::nullopt;
|
||||||
|
return FPalInstanceID{ _Out.value().PlayerUID, _Out.value().InstanceID, _Out.value().DebugName };
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
56
Api/Pal/PalIndividualCharacterHandle.hpp
Normal file
56
Api/Pal/PalIndividualCharacterHandle.hpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#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<PalIndividualCharacterHandle>(), 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<int32>
|
||||||
|
{
|
||||||
|
struct P { UObject* Character; int32 ReturnValue; } p{ Character, 0 };
|
||||||
|
return CallForReturn(STR("GetPhantomIDByActor"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetInstanceIds() -> std::optional<FPalInstanceID>
|
||||||
|
{
|
||||||
|
return PalIndividual::GetInstanceIds(Get());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
480
Api/Pal/PalIndividualCharacterParameter.hpp
Normal file
480
Api/Pal/PalIndividualCharacterParameter.hpp
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#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<PalIndividualCharacterParameter>(), DebugTag()) {}
|
||||||
|
|
||||||
|
// ---- identity ---------------------------------------------------
|
||||||
|
|
||||||
|
auto GetCharacterID() -> std::optional<FName> { return CallForReturn<FName>(STR("GetCharacterID")); }
|
||||||
|
auto GetUniqueNPCID() -> std::optional<FName> { return CallForReturn<FName>(STR("GetUniqueNPCID")); }
|
||||||
|
auto GetSkinName() -> std::optional<FName> { return CallForReturn<FName>(STR("GetSkinName")); }
|
||||||
|
|
||||||
|
auto GetGenderType() -> std::optional<EPalGenderType_>
|
||||||
|
{
|
||||||
|
return CallForReturn<EPalGenderType_>(STR("GetGenderType"));
|
||||||
|
}
|
||||||
|
auto GetTribeID() -> std::optional<EPalTribeID_>
|
||||||
|
{
|
||||||
|
return CallForReturn<EPalTribeID_>(STR("GetTribeID"));
|
||||||
|
}
|
||||||
|
auto IsRarePal() -> std::optional<bool> { return CallForReturn<bool>(STR("IsRarePal")); }
|
||||||
|
auto IsFavoritePal() -> std::optional<bool> { return CallForReturn<bool>(STR("IsFavoritePal")); }
|
||||||
|
auto GetFavoriteIndex() -> std::optional<int32> { return CallForReturn<int32>(STR("GetFavoriteIndex")); }
|
||||||
|
auto IsSkinApplied() -> std::optional<bool> { return CallForReturn<bool>(STR("IsSkinApplied")); }
|
||||||
|
auto GetSkinAppliedCharacterId() -> std::optional<FGuid_>
|
||||||
|
{
|
||||||
|
return CallForReturn<FGuid_>(STR("GetSkinAppliedCharacterId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetPalId() -> std::optional<FPalInstanceID>
|
||||||
|
{
|
||||||
|
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<FString>
|
||||||
|
{
|
||||||
|
struct P { FString OutName; } p{};
|
||||||
|
if (!Call(STR("GetNickname"), p)) return std::nullopt;
|
||||||
|
return p.OutName;
|
||||||
|
}
|
||||||
|
auto GetNickNameWithOnlineID() -> std::optional<FString>
|
||||||
|
{
|
||||||
|
struct P { FString OutName; } p{};
|
||||||
|
if (!Call(STR("GetNickNameWithOnlineID"), p)) return std::nullopt;
|
||||||
|
return p.OutName;
|
||||||
|
}
|
||||||
|
auto GetNickNameByCheckBlockedUser() -> std::optional<FString>
|
||||||
|
{
|
||||||
|
struct P { FString OutName; } p{};
|
||||||
|
if (!Call(STR("GetNickNameByCheckBlockedUser"), p)) return std::nullopt;
|
||||||
|
return p.OutName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- level / experience ------------------------------------------
|
||||||
|
|
||||||
|
auto GetLevel() -> std::optional<int32> { return CallForReturn<int32>(STR("GetLevel")); }
|
||||||
|
auto IsLevelMax() -> std::optional<bool> { return CallForReturn<bool>(STR("IsLevelMax")); }
|
||||||
|
auto GetExp() -> std::optional<int64> { return CallForReturn<int64>(STR("GetExp")); }
|
||||||
|
auto GetRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetRank")); }
|
||||||
|
auto GetRankUpExp() -> std::optional<int32> { return CallForReturn<int32>(STR("GetRankUpExp")); }
|
||||||
|
auto GetPalSoulRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetPalSoulRank")); }
|
||||||
|
auto IsOverrideLevel() -> std::optional<bool> { return CallForReturn<bool>(STR("IsOverrideLevel")); }
|
||||||
|
auto GetOverrideLevel() -> std::optional<int32> { return CallForReturn<int32>(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<FFixedPoint64_> { return CallForReturn<FFixedPoint64_>(STR("GetHP")); }
|
||||||
|
auto GetMaxHP_withBuff() -> std::optional<FFixedPoint64_> { return CallForReturn<FFixedPoint64_>(STR("GetMaxHP_withBuff")); }
|
||||||
|
auto GetMaxHP() -> std::optional<int32> { return CallForReturn<int32>(STR("GetMaxHP")); }
|
||||||
|
auto GetHPRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetHPRank")); }
|
||||||
|
|
||||||
|
auto GetShieldHP() -> std::optional<FFixedPoint64_> { return CallForReturn<FFixedPoint64_>(STR("GetShieldHP")); }
|
||||||
|
auto GetShieldMaxHP() -> std::optional<FFixedPoint64_> { return CallForReturn<FFixedPoint64_>(STR("GetShieldMaxHP")); }
|
||||||
|
|
||||||
|
auto GetCurrentPhaseHPLimit() -> std::optional<FFixedPoint64_>
|
||||||
|
{
|
||||||
|
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<bool> { return CallForReturn<bool>(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<bool> { return CallForReturn<bool>(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<bool> { return CallForReturn<bool>(STR("IsDead")); }
|
||||||
|
auto GetPhysicalHealth() -> std::optional<EPalStatusPhysicalHealthType_>
|
||||||
|
{
|
||||||
|
return CallForReturn<EPalStatusPhysicalHealthType_>(STR("GetPhysicalHealth"));
|
||||||
|
}
|
||||||
|
auto SetPhysicalHealth(EPalStatusPhysicalHealthType_ PhysicalHealth) -> bool
|
||||||
|
{
|
||||||
|
struct P { EPalStatusPhysicalHealthType_ PhysicalHealth; } p{ PhysicalHealth };
|
||||||
|
return Call(STR("SetPhysicalHealth"), p);
|
||||||
|
}
|
||||||
|
auto IsRespawnReady() -> std::optional<bool> { return CallForReturn<bool>(STR("IsRespawnReady")); }
|
||||||
|
auto GetRespawnTime() -> std::optional<float> { return CallForReturn<float>(STR("GetRespawnTime")); }
|
||||||
|
|
||||||
|
// ---- combat stats -------------------------------------------------
|
||||||
|
|
||||||
|
auto GetMeleeAttack() -> std::optional<int32> { return CallForReturn<int32>(STR("GetMeleeAttack")); }
|
||||||
|
auto GetMeleeAttack_withBuff() -> std::optional<int32> { return CallForReturn<int32>(STR("GetMeleeAttack_withBuff")); }
|
||||||
|
auto GetShotAttack() -> std::optional<int32> { return CallForReturn<int32>(STR("GetShotAttack")); }
|
||||||
|
auto GetShotAttack_withBuff() -> std::optional<int32> { return CallForReturn<int32>(STR("GetShotAttack_withBuff")); }
|
||||||
|
auto GetDefense() -> std::optional<int32> { return CallForReturn<int32>(STR("GetDefense")); }
|
||||||
|
auto GetDefense_withBuff() -> std::optional<int32> { return CallForReturn<int32>(STR("GetDefense_withBuff")); }
|
||||||
|
auto GetAttackRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetAttackRank")); }
|
||||||
|
auto GetDefenceRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetDefenceRank")); }
|
||||||
|
|
||||||
|
auto HasMasteredWaza(EPalWazaID_ WazaID) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
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<float> { return CallForReturn<float>(STR("GetSanityValue")); }
|
||||||
|
auto GetMaxSanityValue() -> std::optional<float> { return CallForReturn<float>(STR("GetMaxSanityValue")); }
|
||||||
|
auto GetSanityRate() -> std::optional<float> { return CallForReturn<float>(STR("GetSanityRate")); }
|
||||||
|
auto GetAffectSanityValue() -> std::optional<float> { return CallForReturn<float>(STR("GetAffectSanityValue")); }
|
||||||
|
|
||||||
|
auto GetHungerType() -> std::optional<EPalStatusHungerType_>
|
||||||
|
{
|
||||||
|
return CallForReturn<EPalStatusHungerType_>(STR("GetHungerType"));
|
||||||
|
}
|
||||||
|
auto GetFullStomach() -> std::optional<float> { return CallForReturn<float>(STR("GetFullStomach")); }
|
||||||
|
auto GetMaxFullStomach() -> std::optional<float> { return CallForReturn<float>(STR("GetMaxFullStomach")); }
|
||||||
|
auto GetFullStomachRate() -> std::optional<float> { return CallForReturn<float>(STR("GetFullStomachRate")); }
|
||||||
|
auto GetFullStomachDecreasingRate() -> std::optional<float> { return CallForReturn<float>(STR("GetFullStomachDecreasingRate")); }
|
||||||
|
auto IsFullStomachDecreaseStoppedByFood() -> std::optional<bool>
|
||||||
|
{
|
||||||
|
return CallForReturn<bool>(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<FName> { return CallForReturn<FName>(STR("GetEffectFoodName")); }
|
||||||
|
auto GetEffectFoodName_FullStomachKeep() -> std::optional<FName> { return CallForReturn<FName>(STR("GetEffectFoodName_FullStomachKeep")); }
|
||||||
|
auto GetEffectFoodTimeRate() -> std::optional<float> { return CallForReturn<float>(STR("GetEffectFoodTimeRate")); }
|
||||||
|
auto GetEffectFoodTimeRate_FullStomachKeep() -> std::optional<float> { return CallForReturn<float>(STR("GetEffectFoodTimeRate_FullStomachKeep")); }
|
||||||
|
auto GetFoodStatusRate(EPalFoodStatusEffectType_ EffectType) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { EPalFoodStatusEffectType_ EffectType; float ReturnValue; } p{ EffectType, 0.f };
|
||||||
|
return CallForReturn(STR("GetFoodStatusRate"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- friendship -----------------------------------------------------
|
||||||
|
|
||||||
|
auto GetFriendshipRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetFriendshipRank")); }
|
||||||
|
auto GetFriendshipPoint() -> std::optional<int32> { return CallForReturn<int32>(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<int32> { return CallForReturn<int32>(STR("GetUnusedStatusPoint")); }
|
||||||
|
auto DecrementUnusedStatusPoint() -> bool { return CallVoid(STR("DecrementUnusedStatusPoint")); }
|
||||||
|
auto IsStatusPointAllMax() -> std::optional<bool> { return CallForReturn<bool>(STR("IsStatusPointAllMax")); }
|
||||||
|
|
||||||
|
auto GetStatusPoint(FName StatusName) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { FName StatusName; int32 ReturnValue; } p{ StatusName, 0 };
|
||||||
|
return CallForReturn(STR("GetStatusPoint"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetTotalStatusPoint(FName StatusName) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { FName StatusName; int32 ReturnValue; } p{ StatusName, 0 };
|
||||||
|
return CallForReturn(STR("GetTotalStatusPoint"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetExStatusPoint(FName StatusName) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
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<int32>
|
||||||
|
{
|
||||||
|
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<EPalWorkSuitability_>
|
||||||
|
{
|
||||||
|
return CallForReturn<EPalWorkSuitability_>(STR("GetCurrentWorkSuitability"));
|
||||||
|
}
|
||||||
|
auto GetWorkSpeedRank() -> std::optional<int32> { return CallForReturn<int32>(STR("GetWorkSpeedRank")); }
|
||||||
|
auto GetCraftSpeed() -> std::optional<int32> { return CallForReturn<int32>(STR("GetCraftSpeed")); }
|
||||||
|
auto GetCraftSpeed_withBuff() -> std::optional<int32> { return CallForReturn<int32>(STR("GetCraftSpeed_withBuff")); }
|
||||||
|
auto GetCraftSpeedSickRate() -> std::optional<float> { return CallForReturn<float>(STR("GetCraftSpeedSickRate")); }
|
||||||
|
auto GetBaseCampCraftSpeedBuffRate() -> std::optional<float> { return CallForReturn<float>(STR("GetBaseCampCraftSpeedBuffRate")); }
|
||||||
|
auto GetWorkerSick() -> std::optional<EPalBaseCampWorkerSickType_>
|
||||||
|
{
|
||||||
|
return CallForReturn<EPalBaseCampWorkerSickType_>(STR("GetWorkerSick"));
|
||||||
|
}
|
||||||
|
auto GetBaseCampWorkerEventType() -> std::optional<EPalBaseCampWorkerEventType_>
|
||||||
|
{
|
||||||
|
return CallForReturn<EPalBaseCampWorkerEventType_>(STR("GetBaseCampWorkerEventType"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HasWorkSuitability(EPalWorkSuitability_ Suitability) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { EPalWorkSuitability_ InWorkSuitability; bool ReturnValue; } p{ Suitability, false };
|
||||||
|
return CallForReturn(STR("HasWorkSuitability"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto HasWorkSuitabilityRank(EPalWorkSuitability_ Suitability, int32 Rank) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
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<int32>
|
||||||
|
{
|
||||||
|
struct P { EPalWorkSuitability_ InWorkSuitability; int32 ReturnValue; } p{ Suitability, 0 };
|
||||||
|
return CallForReturn(STR("GetWorkSuitabilityRank"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetWorkSuitabilityRankWithCharacterRank(EPalWorkSuitability_ Suitability) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { EPalWorkSuitability_ WorkSuitability; int32 ReturnValue; } p{ Suitability, 0 };
|
||||||
|
return CallForReturn(STR("GetWorkSuitabilityRankWithCharacterRank"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetRankBasedWorkSuitabilityBonus(EPalWorkSuitability_ Suitability) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { EPalWorkSuitability_ WorkSuitability; int32 ReturnValue; } p{ Suitability, 0 };
|
||||||
|
return CallForReturn(STR("GetRankBasedWorkSuitabilityBonus"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetCraftSpeedByWorkSuitability(EPalWorkSuitability_ Suitability) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { EPalWorkSuitability_ WorkSuitability; int32 ReturnValue; } p{ Suitability, 0 };
|
||||||
|
return CallForReturn(STR("GetCraftSpeedByWorkSuitability"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetCraftSpeedBuffRate(EPalWorkSuitability_ Suitability) -> std::optional<float>
|
||||||
|
{
|
||||||
|
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<int32>
|
||||||
|
{
|
||||||
|
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<FName>) 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<float>
|
||||||
|
{
|
||||||
|
struct P { EPalPassiveSkillEffectType_ EffectType; float ReturnValue; } p{ EffectType, 0.f };
|
||||||
|
return CallForReturn(STR("GetPassiveRateBySkillEffect"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetPassiveRateByEquipment(EPalPassiveSkillEffectType_ EffectType) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { EPalPassiveSkillEffectType_ EffectType; float ReturnValue; } p{ EffectType, 0.f };
|
||||||
|
return CallForReturn(STR("GetPassiveRateByEquipment"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- classification / misc flags ---------------------------------------
|
||||||
|
|
||||||
|
auto GetSizeType() -> std::optional<EPalSizeType_> { return CallForReturn<EPalSizeType_>(STR("GetSizeType")); }
|
||||||
|
auto GetGroupType() -> std::optional<EPalGroupType_> { return CallForReturn<EPalGroupType_>(STR("GetGroupType")); }
|
||||||
|
auto GetGroupId() -> std::optional<FGuid_> { return CallForReturn<FGuid_>(STR("GetGroupId")); }
|
||||||
|
auto GetBaseCampId() -> std::optional<FGuid_> { return CallForReturn<FGuid_>(STR("GetBaseCampId")); }
|
||||||
|
auto GetArenaRank() -> std::optional<EPalArenaRank_> { return CallForReturn<EPalArenaRank_>(STR("GetArenaRank")); }
|
||||||
|
auto GetArenaRankPoint() -> std::optional<int32> { return CallForReturn<int32>(STR("GetArenaRankPoint")); }
|
||||||
|
auto IsInArena() -> std::optional<bool> { return CallForReturn<bool>(STR("IsInArena")); }
|
||||||
|
auto IsInRaidArea() -> std::optional<bool> { return CallForReturn<bool>(STR("IsInRaidArea")); }
|
||||||
|
auto IsUncapturable() -> std::optional<bool> { return CallForReturn<bool>(STR("IsUncapturable")); }
|
||||||
|
auto IsForceCapturable() -> std::optional<bool> { return CallForReturn<bool>(STR("IsForceCapturable")); }
|
||||||
|
auto IsAwakening() -> std::optional<bool> { return CallForReturn<bool>(STR("IsAwakening")); }
|
||||||
|
auto IsPartBroken() -> std::optional<bool> { return CallForReturn<bool>(STR("IsPartBroken")); }
|
||||||
|
auto IsParts() -> std::optional<bool> { return CallForReturn<bool>(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<bool> { return CallForReturn<bool>(STR("IsAssignedToExpedition")); }
|
||||||
|
auto IsAssignedToExpeditionIn(FGuid_ MapObjectId) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FGuid_ MapObjectConcreteInstanceId; bool ReturnValue; } p{ MapObjectId, false };
|
||||||
|
return CallForReturn(STR("IsAssignedToExpeditionIn"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto IsExcludedFromTeamMission() -> std::optional<bool> { return CallForReturn<bool>(STR("IsExcludedFromTeamMission")); }
|
||||||
|
auto CanTargetFromAI() -> std::optional<bool> { return CallForReturn<bool>(STR("CanTargetFromAI")); }
|
||||||
|
auto HasGenusCategory(EPalGenusCategoryType_ Category) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { EPalGenusCategoryType_ Category; bool ReturnValue; } p{ Category, false };
|
||||||
|
return CallForReturn(STR("HasGenusCategory"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto CanAddTalentByItem(FName ItemName) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { FName ItemName; bool ReturnValue; } p{ ItemName, false };
|
||||||
|
return CallForReturn(STR("CanAddTalentByItem"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetNPCWeaponType() -> std::optional<EPalWeaponType_> { return CallForReturn<EPalWeaponType_>(STR("GetNPCWeaponType")); }
|
||||||
|
auto GetEquipItemContainerId() -> std::optional<FPalContainerId_>
|
||||||
|
{
|
||||||
|
return CallForReturn<FPalContainerId_>(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")); }
|
||||||
|
};
|
||||||
|
}
|
||||||
110
Api/Pal/PalTypes.hpp
Normal file
110
Api/Pal/PalTypes.hpp
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
155
Api/Pal/PalUIStatusModel.hpp
Normal file
155
Api/Pal/PalUIStatusModel.hpp
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#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<PalUIStatusModel>(), DebugTag()) {}
|
||||||
|
|
||||||
|
|
||||||
|
// Count of pals currently loaded into the model -- "pal number".
|
||||||
|
auto GetDisplayPalNum() -> std::optional<uint8>
|
||||||
|
{
|
||||||
|
return CallForReturn<uint8>(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<uint8>
|
||||||
|
{
|
||||||
|
return CallForReturn<uint8>(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<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 };
|
||||||
|
return CallForReturn(STR("GetDisplayLevel"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplaySpeed(int32 Index) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 };
|
||||||
|
return CallForReturn(STR("GetDisplaySpeed"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayMeleeAttack(int32 Index) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 };
|
||||||
|
return CallForReturn(STR("GetDisplayMeleeAttack"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayShotAttack(int32 Index) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 };
|
||||||
|
return CallForReturn(STR("GetDisplayShotAttack"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayDefence(int32 Index) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 };
|
||||||
|
return CallForReturn(STR("GetDisplayDefence"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayEatingHabits(int32 Index) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 };
|
||||||
|
return CallForReturn(STR("GetDisplayEatingHabits"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayNowExp(int32 Index) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 };
|
||||||
|
return CallForReturn(STR("GetDisplayNowExp"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayNextExp(int32 Index) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 ReturnValue; } p{ Index, 0 };
|
||||||
|
return CallForReturn(STR("GetDisplayNextExp"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayHunger(int32 Index) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; float ReturnValue; } p{ Index, 0.f };
|
||||||
|
return CallForReturn(STR("GetDisplayHunger"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayMaxHunger(int32 Index) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; float ReturnValue; } p{ Index, 0.f };
|
||||||
|
return CallForReturn(STR("GetDisplayMaxHunger"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetDisplayElementType1(int32 Index) -> std::optional<EPalElementType_>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; EPalElementType_ ReturnValue; } p{ Index, EPalElementType_::None };
|
||||||
|
return CallForReturn(STR("GetDisplayElementType1"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayElementType2(int32 Index) -> std::optional<EPalElementType_>
|
||||||
|
{
|
||||||
|
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<uint8>
|
||||||
|
{
|
||||||
|
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<FText>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; FText ReturnValue; } p{ Index, {} };
|
||||||
|
return CallForReturn(STR("GetDisplayDefaultName"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayCoopActionName(int32 Index) -> std::optional<FText>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; FText ReturnValue; } p{ Index, {} };
|
||||||
|
return CallForReturn(STR("GetDisplayCoopActionName"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetDisplayActiveSkillName(int32 Index, int32 SkillIndex) -> std::optional<FText>
|
||||||
|
{
|
||||||
|
struct P { int32 Index; int32 SkillIndex; FText ReturnValue; } p{ Index, SkillIndex, {} };
|
||||||
|
return CallForReturn(STR("GetDisplayActiveSkillName"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetDisplayNickName(int32 Index) -> std::optional<FString>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
123
Api/Pal/PalUtility.hpp
Normal file
123
Api/Pal/PalUtility.hpp
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <Reflection/UnrealObjectRef.hpp>
|
||||||
|
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#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<PalDatabaseCharacterParameter>(
|
||||||
|
STR("GetDatabaseCharacterParameter"), WorldContextObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto ZukanIndexFor(FName CharacterId, UObject* WorldContextObject) -> std::optional<int32>
|
||||||
|
{
|
||||||
|
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<bool>
|
||||||
|
{
|
||||||
|
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<PalUtility>(), DebugTag()) {}
|
||||||
|
|
||||||
|
static constexpr int32 kNoZukanEntry = -1;
|
||||||
|
static inline std::unordered_map<uint64_t, int32> s_zukan_cache{};
|
||||||
|
|
||||||
|
static inline std::unordered_map<uint64_t, uint8_t> s_nocturnal_cache{};
|
||||||
|
|
||||||
|
static auto NameKey(FName N) -> uint64_t
|
||||||
|
{
|
||||||
|
return (static_cast<uint64_t>(N.GetComparisonIndex().ToUnstableInt()) << 32)
|
||||||
|
| static_cast<uint64_t>(static_cast<uint32_t>(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 <typename T>
|
||||||
|
auto GetSingleton(const TCHAR* FunctionName, UObject* WorldContextObject) -> T
|
||||||
|
{
|
||||||
|
UObject* Obj = CallStaticObjectGetter(FunctionName, WorldContextObject);
|
||||||
|
return Obj ? T{ Obj } : T{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
284
Api/PalBoxPalListWidget.hpp
Normal file
284
Api/PalBoxPalListWidget.hpp
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Api/UmgTypes.hpp>
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#include <Api/Widget.hpp>
|
||||||
|
#include <Api/UserWidget.hpp>
|
||||||
|
#include <Api/WidgetTree.hpp>
|
||||||
|
#include <Api/TextBlock.hpp>
|
||||||
|
#include <Api/CanvasPanel.hpp>
|
||||||
|
|
||||||
|
#include <Api/Pal/PalIndividual.hpp>
|
||||||
|
#include <Api/Pal/PalTypes.hpp>
|
||||||
|
|
||||||
|
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<PalBoxPalListWidget>(), 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<UObject*>(STR("GetHandle")).value_or(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto SlotIsEmpty(UObject* Slot) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
if (!Slot) return std::nullopt;
|
||||||
|
Pal::LooseObject S{ Slot, STR("PalIndividualSlot") };
|
||||||
|
return S.CallForReturn<bool>(STR("IsEmpty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto SpeciesFromSlot(UObject* Slot) -> std::optional<FName>
|
||||||
|
{
|
||||||
|
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<bool>
|
||||||
|
{
|
||||||
|
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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
224
Api/PalBoxWidget.hpp
Normal file
224
Api/PalBoxWidget.hpp
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#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<PalBoxWidget>(), 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<PalBoxPalListWidget>
|
||||||
|
{
|
||||||
|
std::vector<PalBoxPalListWidget> 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<CanvasPanel>(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<bool>
|
||||||
|
{
|
||||||
|
return PropertyBoolValue(STR("bIsEnableDragDropControl"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PartyPalDetails() const -> std::vector<UObject*>
|
||||||
|
{
|
||||||
|
std::vector<UObject*> Out;
|
||||||
|
|
||||||
|
auto* Arr = TryPropertyPtr<TArrayRead_<UObject*>>(STR("PartyPalDetails"));
|
||||||
|
if (!Arr) return Out;
|
||||||
|
|
||||||
|
if (!Arr->IsPlausible(kMaxPlausibleEntries))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] PartyPalDetails does not look like a live TArray (Num={}, Max={})\n"),
|
||||||
|
m_tag, Arr->Num, Arr->Max);
|
||||||
|
return Out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Out.reserve(static_cast<size_t>(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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
155
Api/PalCharacterScrollList.hpp
Normal file
155
Api/PalCharacterScrollList.hpp
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <Unreal/UClass.hpp>
|
||||||
|
|
||||||
|
#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<PalCharacterScrollList>(), DebugTag()) {}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// THE TILES
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
auto TileContainer() const -> WrapBox
|
||||||
|
{
|
||||||
|
UObject* O = BoundObject(STR("MyBlock"));
|
||||||
|
return O ? WrapBox{ O } : WrapBox{};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Tiles() -> std::vector<UObject*>
|
||||||
|
{
|
||||||
|
WrapBox Box = TileContainer();
|
||||||
|
if (!Box.IsValid()) return {};
|
||||||
|
return Box.GetChildObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TileClass() const -> UClass*
|
||||||
|
{
|
||||||
|
return PropertyValue<UClass*>(STR("slotWidgetClass")).value_or(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TileClassName() const -> RC::StringType
|
||||||
|
{
|
||||||
|
UClass* C = TileClass();
|
||||||
|
return C ? C->GetFullName() : RC::StringType{ STR("<null>") };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// 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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
179
Api/PalCommonCharacterSlot.hpp
Normal file
179
Api/PalCommonCharacterSlot.hpp
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#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<PalCommonCharacterSlot>(), DebugTag()) {}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// ANCHORS
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
auto IconOverlay() -> Overlay
|
||||||
|
{
|
||||||
|
return Child<Overlay>(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<bool> { return PropertyBoolValue(STR("isBattleModeDisplay")); }
|
||||||
|
auto IsLowHealthBlinking() const -> std::optional<bool> { return PropertyBoolValue(STR("isLowHealthBlink")); }
|
||||||
|
auto IsHealthPercentDisplayed()const -> std::optional<bool> { return PropertyBoolValue(STR("isDisplayHealthPercent")); }
|
||||||
|
auto IsRarityDisplayed() const -> std::optional<bool> { return PropertyBoolValue(STR("isDisplayRarity")); }
|
||||||
|
auto LastWorkDetail() const -> std::optional<bool> { return PropertyBoolValue(STR("lastWorkDetail")); }
|
||||||
|
auto IsDopingEffectDisplayed() const -> std::optional<bool> { return PropertyBoolValue(STR("bIsDisplayDopingEffect")); }
|
||||||
|
auto IsFavoriteMarkDisplayed() const -> std::optional<bool> { return PropertyBoolValue(STR("bIsDisplayFavoriteMark")); }
|
||||||
|
auto IsAwakeningMarkDisplayed()const -> std::optional<bool> { return PropertyBoolValue(STR("bIsDisplayAwakeningMark")); }
|
||||||
|
|
||||||
|
auto CachedLastHPPercent() const -> std::optional<double> { return PropertyValue<double>(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<PalCommonCharacterIcon>(), 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<bool> { return PropertyBoolValue(STR("IsEmpty")); }
|
||||||
|
auto SphereMaskRadius() const -> std::optional<double> { return PropertyValue<double>(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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
139
Api/PalCommonCharacterSlotButton.hpp
Normal file
139
Api/PalCommonCharacterSlotButton.hpp
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#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<PalCommonCharacterSlotButton>(), 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<Overlay>(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<bool> { return PropertyBoolValue(STR("bIsDisplayDopingEffect")); }
|
||||||
|
auto HasCheckAnimPlayed() const -> std::optional<bool> { 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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
270
Api/PalDetailWidget.hpp
Normal file
270
Api/PalDetailWidget.hpp
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include "UmgTypes.hpp"
|
||||||
|
#include "UnrealWrapper.hpp"
|
||||||
|
#include "Widget.hpp"
|
||||||
|
#include "UserWidget.hpp"
|
||||||
|
#include "WidgetTree.hpp"
|
||||||
|
#include "TextBlock.hpp"
|
||||||
|
#include "CanvasPanel.hpp"
|
||||||
|
|
||||||
|
#include <Api/Pal/PalIndividual.hpp>
|
||||||
|
#include <Api/Pal/PalIndividualCharacterHandle.hpp>
|
||||||
|
#include <Api/Pal/PalIndividualCharacterParameter.hpp>
|
||||||
|
|
||||||
|
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<PalDetailWidget>(), 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<FName>
|
||||||
|
{
|
||||||
|
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<float> { return PropertyValue<float>(STR("savedMaxHunger")); }
|
||||||
|
auto SavedMaxSanity() const -> std::optional<float> { return PropertyValue<float>(STR("savedMaxSanity")); }
|
||||||
|
auto HidePassive() const -> std::optional<bool> { return PropertyValue<bool>(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<WorkSuitabilityCheck>
|
||||||
|
{
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
50
Api/PanelSlot.hpp
Normal file
50
Api/PanelSlot.hpp
Normal file
@@ -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<PanelSlot>(), 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<UObject*>(STR("Parent"));
|
||||||
|
return Ptr ? *Ptr : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetContentObject() const -> UObject*
|
||||||
|
{
|
||||||
|
auto Ptr = PropertyPtr<UObject*>(STR("Content"));
|
||||||
|
return Ptr ? *Ptr : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
PanelSlot(UObject* Obj, UClass* Expected, const TCHAR* Tag)
|
||||||
|
: UnrealWrapper(Obj, Expected, Tag) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
173
Api/PanelWidget.hpp
Normal file
173
Api/PanelWidget.hpp
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<PanelWidget>(), DebugTag()) {}
|
||||||
|
|
||||||
|
auto GetChildrenCount() -> std::optional<int32>
|
||||||
|
{
|
||||||
|
return CallForReturn<int32>(STR("GetChildrenCount"));
|
||||||
|
}
|
||||||
|
auto HasAnyChildren() -> std::optional<bool>
|
||||||
|
{
|
||||||
|
return CallForReturn<bool>(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<int32>
|
||||||
|
{
|
||||||
|
struct P { UObject* Content; int32 ReturnValue; } p{ Content, -1 };
|
||||||
|
return CallForReturn(STR("GetChildIndex"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HasChild(UObject* Content) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
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<bool>
|
||||||
|
{
|
||||||
|
struct P { UObject* Content; bool ReturnValue; } p{ Content, false };
|
||||||
|
return CallForReturn(STR("RemoveChild"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto RemoveChildAt(int32 Index) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
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<UWidget*> 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<Widget>
|
||||||
|
{
|
||||||
|
std::vector<Widget> Children;
|
||||||
|
|
||||||
|
auto Count = GetChildrenCount();
|
||||||
|
if (!Count || *Count <= 0) return Children;
|
||||||
|
|
||||||
|
Children.reserve(static_cast<size_t>(*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<UPanelSlot*> 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<UObject*>& Out) const -> bool
|
||||||
|
{
|
||||||
|
auto* Slots = TryPropertyPtr<TArrayRead_<UObject*>>(STR("Slots"));
|
||||||
|
if (!Slots) return false;
|
||||||
|
|
||||||
|
if (!Slots->IsPlausible(kMaxPlausibleChildren))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
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<size_t>(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<UObject*>
|
||||||
|
{
|
||||||
|
std::vector<UObject*> 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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
131
Api/SizeBox.hpp
Normal file
131
Api/SizeBox.hpp
Normal file
@@ -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<SizeBox>(), 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<float> { return PropertyValue<float>(STR("WidthOverride")); }
|
||||||
|
auto GetHeightOverride() const -> std::optional<float> { return PropertyValue<float>(STR("HeightOverride")); }
|
||||||
|
auto GetMinDesiredWidth() const -> std::optional<float> { return PropertyValue<float>(STR("MinDesiredWidth")); }
|
||||||
|
auto GetMinDesiredHeight() const -> std::optional<float> { return PropertyValue<float>(STR("MinDesiredHeight")); }
|
||||||
|
auto GetMaxDesiredWidth() const -> std::optional<float> { return PropertyValue<float>(STR("MaxDesiredWidth")); }
|
||||||
|
auto GetMaxDesiredHeight() const -> std::optional<float> { return PropertyValue<float>(STR("MaxDesiredHeight")); }
|
||||||
|
auto GetMinAspectRatio() const -> std::optional<float> { return PropertyValue<float>(STR("MinAspectRatio")); }
|
||||||
|
auto GetMaxAspectRatio() const -> std::optional<float> { return PropertyValue<float>(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<uint8>/PropertyPtr<uint8> 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<bool> { return PropertyBoolValue(STR("bOverride_WidthOverride")); }
|
||||||
|
auto IsHeightOverridden() const -> std::optional<bool> { return PropertyBoolValue(STR("bOverride_HeightOverride")); }
|
||||||
|
auto IsMinDesiredWidthOverridden() const -> std::optional<bool> { return PropertyBoolValue(STR("bOverride_MinDesiredWidth")); }
|
||||||
|
auto IsMinDesiredHeightOverridden() const -> std::optional<bool> { return PropertyBoolValue(STR("bOverride_MinDesiredHeight")); }
|
||||||
|
auto IsMaxDesiredWidthOverridden() const -> std::optional<bool> { return PropertyBoolValue(STR("bOverride_MaxDesiredWidth")); }
|
||||||
|
auto IsMaxDesiredHeightOverridden() const -> std::optional<bool> { return PropertyBoolValue(STR("bOverride_MaxDesiredHeight")); }
|
||||||
|
auto IsMinAspectRatioOverridden() const -> std::optional<bool> { return PropertyBoolValue(STR("bOverride_MinAspectRatio")); }
|
||||||
|
auto IsMaxAspectRatioOverridden() const -> std::optional<bool> { return PropertyBoolValue(STR("bOverride_MaxAspectRatio")); }
|
||||||
|
};
|
||||||
|
}
|
||||||
41
Api/SizeBoxSlot.hpp
Normal file
41
Api/SizeBoxSlot.hpp
Normal file
@@ -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<SizeBoxSlot>(), 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<FMargin_> { return PropertyValue<FMargin_>(STR("Padding")); }
|
||||||
|
auto GetHorizontalAlignment()const -> std::optional<EHorizontalAlignment_> { return PropertyValue<EHorizontalAlignment_>(STR("HorizontalAlignment")); }
|
||||||
|
auto GetVerticalAlignment() const -> std::optional<EVerticalAlignment_> { return PropertyValue<EVerticalAlignment_>(STR("VerticalAlignment")); }
|
||||||
|
};
|
||||||
|
}
|
||||||
289
Api/TextBlock.hpp
Normal file
289
Api/TextBlock.hpp
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
|
||||||
|
#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<TextBlock>(), 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<FText>
|
||||||
|
{
|
||||||
|
return CallForReturn<FText>(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<uint8>(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<UObject*> { return CallForReturn<UObject*>(STR("GetDynamicFontMaterial")); }
|
||||||
|
auto GetDynamicOutlineMaterial() -> std::optional<UObject*> { return CallForReturn<UObject*>(STR("GetDynamicOutlineMaterial")); }
|
||||||
|
|
||||||
|
// ---- property reads ---------------------------------------------------
|
||||||
|
|
||||||
|
auto GetMinDesiredWidth() const -> std::optional<float> { return PropertyValue<float>(STR("MinDesiredWidth")); }
|
||||||
|
auto GetShadowOffset() const -> std::optional<FVector2D_> { return PropertyValue<FVector2D_>(STR("ShadowOffset")); }
|
||||||
|
auto GetShadowColorAndOpacity()const -> std::optional<FLinearColor_> { return PropertyValue<FLinearColor_>(STR("ShadowColorAndOpacity")); }
|
||||||
|
auto GetColorAndOpacity() const -> std::optional<FSlateColor_> { return PropertyValue<FSlateColor_>(STR("ColorAndOpacity")); }
|
||||||
|
auto GetTextTransformPolicy() const -> std::optional<ETextTransformPolicy_>{ return PropertyValue<ETextTransformPolicy_>(STR("TextTransformPolicy")); }
|
||||||
|
auto GetTextOverflowPolicy() const -> std::optional<ETextOverflowPolicy_> { return PropertyValue<ETextOverflowPolicy_>(STR("TextOverflowPolicy")); }
|
||||||
|
auto GetSimpleTextMode() const -> std::optional<bool> { return PropertyValue<bool>(STR("bSimpleTextMode")); }
|
||||||
|
auto GetWrapWithInvalidationPanel() const -> std::optional<bool> { return PropertyValue<bool>(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<const FCompositeFont>.
|
||||||
|
// 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<float*>(SizePtr);
|
||||||
|
int32 AsInt = *reinterpret_cast<int32*>(SizePtr);
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
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<float*>(P) = NewSize; return PushFontToSlate(); }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SetFontSizeInt(int32 NewSize) -> bool
|
||||||
|
{
|
||||||
|
if (void* P = FontMemberPtr(STR("Size"), 4)) { *reinterpret_cast<int32*>(P) = NewSize; return PushFontToSlate(); }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SetFontLetterSpacing(int32 Value) -> bool
|
||||||
|
{
|
||||||
|
if (void* P = FontMemberPtr(STR("LetterSpacing"), 4)) { *reinterpret_cast<int32*>(P) = Value; return PushFontToSlate(); }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SetFontSkewAmount(float Value) -> bool
|
||||||
|
{
|
||||||
|
if (void* P = FontMemberPtr(STR("SkewAmount"), 4)) { *reinterpret_cast<float*>(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<FName*>(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<LogLevel::Warning>(STR("[TextBlock] no 'Font' property\n"));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FStructProperty::GetStruct() gives the UScriptStruct whose
|
||||||
|
// property chain describes FSlateFontInfo's members.
|
||||||
|
auto* AsStruct = static_cast<FStructProperty*>(FontProp);
|
||||||
|
UScriptStruct* FontStruct = AsStruct->GetStruct();
|
||||||
|
if (!FontStruct)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(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<LogLevel::Warning>(
|
||||||
|
STR("[TextBlock] FSlateFontInfo has no member '{}' on this build\n"), MemberName);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_cast<size_t>(Member->GetSize()) != ExpectedSize)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[TextBlock] FSlateFontInfo::{} is {} bytes, expected {}\n"),
|
||||||
|
MemberName, Member->GetSize(), ExpectedSize);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8* FontBase = reinterpret_cast<uint8*>(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<uint8*>(m_obj) + FontProp->GetOffset_Internal();
|
||||||
|
|
||||||
|
UFunction* Fn = m_obj->GetFunctionByNameInChain(STR("SetFont"));
|
||||||
|
if (!Fn)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(STR("[TextBlock] SetFont not found\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_obj->ProcessEvent(Fn, LiveFont);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
68
Api/TextLayoutWidget.hpp
Normal file
68
Api/TextLayoutWidget.hpp
Normal file
@@ -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<TextLayoutWidget>(), DebugTag()) {}
|
||||||
|
|
||||||
|
auto SetJustification(ETextJustify_ InJustification) -> bool
|
||||||
|
{
|
||||||
|
struct P { ETextJustify_ InJustification; } p{ InJustification };
|
||||||
|
return Call(STR("SetJustification"), p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- property reads -------------------------------------------------
|
||||||
|
auto GetJustification() const -> std::optional<ETextJustify_> { return PropertyValue<ETextJustify_>(STR("Justification")); }
|
||||||
|
auto GetWrappingPolicy() const -> std::optional<ETextWrappingPolicy_>{ return PropertyValue<ETextWrappingPolicy_>(STR("WrappingPolicy")); }
|
||||||
|
auto GetWrapTextAt() const -> std::optional<float> { return PropertyValue<float>(STR("WrapTextAt")); }
|
||||||
|
auto GetLineHeightPercentage()const -> std::optional<float> { return PropertyValue<float>(STR("LineHeightPercentage")); }
|
||||||
|
auto GetMargin() const -> std::optional<FMargin_> { return PropertyValue<FMargin_>(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<float>(STR("WrapTextAt"))) { *P = Value; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto SetLineHeightPercentage(float Value) -> bool
|
||||||
|
{
|
||||||
|
if (float* P = PropertyPtr<float>(STR("LineHeightPercentage"))) { *P = Value; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto SetMargin(FMargin_ Value) -> bool
|
||||||
|
{
|
||||||
|
if (FMargin_* P = PropertyPtr<FMargin_>(STR("Margin"))) { *P = Value; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
TextLayoutWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag)
|
||||||
|
: Widget(Obj, Expected, Tag) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
156
Api/UmgTypes.hpp
Normal file
156
Api/UmgTypes.hpp
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
|
||||||
|
namespace PalUIExtension::Api
|
||||||
|
{
|
||||||
|
using namespace RC;
|
||||||
|
using namespace RC::Unreal;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
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<ETextJustify::Type>
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
}
|
||||||
476
Api/UnrealWrapper.hpp
Normal file
476
Api/UnrealWrapper.hpp
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
|
||||||
|
#include <Unreal/UObjectGlobals.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
#include <Unreal/UClass.hpp>
|
||||||
|
#include <Unreal/CoreUObject/UObject/Class.hpp>
|
||||||
|
#include <Unreal/CoreUObject/UObject/UnrealType.hpp>
|
||||||
|
|
||||||
|
#include <Reflection/UnrealApiCall.hpp>
|
||||||
|
#include <Reflection/UnrealObjectRef.hpp>
|
||||||
|
|
||||||
|
namespace PalUIExtension::Api
|
||||||
|
{
|
||||||
|
using namespace RC;
|
||||||
|
using namespace RC::Unreal;
|
||||||
|
|
||||||
|
namespace Detail
|
||||||
|
{
|
||||||
|
struct Tag_Umg {};
|
||||||
|
|
||||||
|
// ONE dispatcher per return type, shared by every wrapper in this
|
||||||
|
// tree -- not one per wrapper class.
|
||||||
|
//
|
||||||
|
// The reason this is correct: UnrealApiCall::Invoke resolves the
|
||||||
|
// function with Target->GetFunctionByNameInChain(), which walks the
|
||||||
|
// *live object's* class chain. It never touches the ClassPath the
|
||||||
|
// UnrealApiCall was constructed with. So the class path is only
|
||||||
|
// used by ResolveClass/FindInstance/ConstructInstance, none of
|
||||||
|
// which we call here. Giving each wrapper class its own set of
|
||||||
|
// per-return-type statics would buy nothing but N redundant
|
||||||
|
// StaticFindObject calls.
|
||||||
|
//
|
||||||
|
// The corollary matters for the inheritance below: a base-class
|
||||||
|
// wrapper calling a function on a derived object works, because
|
||||||
|
// dispatch is by the object's chain, not by the wrapper's type.
|
||||||
|
//
|
||||||
|
// The path below is a placeholder that is never resolved. If you
|
||||||
|
// ever call ResolveClass on one of these, it will resolve UObject
|
||||||
|
// and the resulting IsA check will be meaningless.
|
||||||
|
template <typename R>
|
||||||
|
inline auto Api() -> Reflection::UnrealApiCall<Tag_Umg, R>&
|
||||||
|
{
|
||||||
|
static Reflection::UnrealApiCall<Tag_Umg, R> Instance{
|
||||||
|
STR("/Script/CoreUObject.Object"), STR("UMG") };
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves and caches the UClass for a wrapper type T.
|
||||||
|
// T must expose static ClassPath() and static DebugTag().
|
||||||
|
template <typename T>
|
||||||
|
inline auto ClassOf() -> UClass*
|
||||||
|
{
|
||||||
|
static Reflection::UnrealObjectRef Ref{ T::ClassPath(), T::DebugTag() };
|
||||||
|
return static_cast<UClass*>(Ref.Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Base for every UObject wrapper here.
|
||||||
|
//
|
||||||
|
// Lifetime: holds a raw UObject* and does nothing to keep it alive.
|
||||||
|
// These wrappers are meant to live inside one hook callback. Storing
|
||||||
|
// one across frames means storing a pointer the GC can invalidate,
|
||||||
|
// and a freed UObject slot will eventually be reused by a different
|
||||||
|
// object -- so a stale pointer does not read as null, it reads as
|
||||||
|
// some other widget.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
class UnrealWrapper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
auto IsValid() const -> bool { return m_obj != nullptr; }
|
||||||
|
explicit operator bool() const { return IsValid(); }
|
||||||
|
auto Get() const -> UObject* { return m_obj; }
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Diagnostics.
|
||||||
|
//
|
||||||
|
// These were copy-pasted identically into PalDetailWidget and
|
||||||
|
// MainMenuPalWidget, differing only in the log prefix -- which
|
||||||
|
// m_tag already carries. Hoisted here so every wrapper has them and
|
||||||
|
// there is one copy to fix when the reflection API shifts.
|
||||||
|
//
|
||||||
|
// LogFunctionSignature is the tool for settling a frame layout
|
||||||
|
// without guessing: it prints the engine's own offsets and sizes
|
||||||
|
// for a UFunction's parameter block, which is exactly what the
|
||||||
|
// hand-written ParamsStructs in this codebase have to match. Run it
|
||||||
|
// before trusting a new struct, not after it misbehaves.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
auto LogFunctionSignature(const TCHAR* FunctionName) const -> void
|
||||||
|
{
|
||||||
|
if (!m_obj) return;
|
||||||
|
|
||||||
|
UFunction* Fn = m_obj->GetFunctionByNameInChain(FunctionName);
|
||||||
|
if (!Fn)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] '{}' is not in the chain of {}\n"),
|
||||||
|
m_tag, FunctionName, m_obj->GetFullName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] {} -- frame is {} bytes:\n"),
|
||||||
|
m_tag, FunctionName, Fn->GetStructureSize());
|
||||||
|
|
||||||
|
for (FProperty* Prop : Fn->ForEachProperty())
|
||||||
|
{
|
||||||
|
if (!Prop) continue;
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR(" [0x{:X}] {} ({} bytes)\n"),
|
||||||
|
Prop->GetOffset_Internal(), Prop->GetName(), Prop->GetSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every UPROPERTY on the object's class chain, with offset and
|
||||||
|
// size. The companion to the above for the property side: this is
|
||||||
|
// how you find out what a widget is actually called internally, or
|
||||||
|
// whether the field you want is a promoted variable at all.
|
||||||
|
auto LogProperties() const -> void
|
||||||
|
{
|
||||||
|
if (!m_obj) return;
|
||||||
|
|
||||||
|
UClass* Cls = m_obj->GetClassPrivate();
|
||||||
|
if (!Cls)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] no class on {}\n"), m_tag, m_obj->GetFullName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] properties of {}:\n"), m_tag, m_obj->GetFullName());
|
||||||
|
|
||||||
|
for (FProperty* Prop : Cls->ForEachPropertyInChain())
|
||||||
|
{
|
||||||
|
if (!Prop) continue;
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR(" [0x{:X}] {} ({} bytes)\n"),
|
||||||
|
Prop->GetOffset_Internal(), Prop->GetName(), Prop->GetSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UnrealWrapper() = default;
|
||||||
|
|
||||||
|
UnrealWrapper(UObject* Obj, UClass* Expected, const TCHAR* DebugTag)
|
||||||
|
: m_tag(DebugTag ? DebugTag : STR("Wrapper"))
|
||||||
|
{
|
||||||
|
if (!Obj)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] constructed from null object\n"), m_tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Expected)
|
||||||
|
{
|
||||||
|
// Class path did not resolve. Accept the object rather than
|
||||||
|
// reject it -- Invoke dispatches by the object's own chain,
|
||||||
|
// so calls will still work; you just lost the type check.
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] class unresolved -- accepting {} unchecked\n"),
|
||||||
|
m_tag, Obj->GetFullName());
|
||||||
|
m_obj = Obj;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Obj->IsA(Expected))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] {} is not of the expected class\n"),
|
||||||
|
m_tag, Obj->GetFullName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_obj = Obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NoParams {};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Deliberate opt-out of the class check.
|
||||||
|
//
|
||||||
|
// The (Obj, Expected, Tag) constructor below treats a null Expected
|
||||||
|
// as "the ClassPath failed to resolve" and logs a Warning about it,
|
||||||
|
// which is right for that case and wrong for a caller that never
|
||||||
|
// had a ClassPath to begin with (LooseObject). The difference
|
||||||
|
// matters more than it looks: that Warning calls
|
||||||
|
// Obj->GetFullName(), which walks the whole outer chain calling
|
||||||
|
// GetName() -- an FName::ToString, i.e. a ProcessEvent -- at every
|
||||||
|
// level, then formats the line and writes it to every open output
|
||||||
|
// device, because Output::send has no level filter (see
|
||||||
|
// Static/LogGate.hpp).
|
||||||
|
//
|
||||||
|
// Paid once at startup for a genuinely unresolved class, that is
|
||||||
|
// fine. Paid on every LooseObject construction on a per-bind path,
|
||||||
|
// it is a dozen-plus ProcessEvent calls and a disk write per pal
|
||||||
|
// selection. This tag ctor says "unchecked on purpose" and stays
|
||||||
|
// silent.
|
||||||
|
//
|
||||||
|
// It does NOT weaken anything: LooseObject was already accepting
|
||||||
|
// the object unchecked. The only thing removed is a diagnostic
|
||||||
|
// that was describing a condition that wasn't happening.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
struct UncheckedTag { explicit UncheckedTag() = default; };
|
||||||
|
|
||||||
|
UnrealWrapper(UObject* Obj, UncheckedTag, const TCHAR* DebugTag)
|
||||||
|
: m_obj(Obj), m_tag(DebugTag ? DebugTag : STR("Wrapper"))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Void-returning UFUNCTIONs only. Detail::Api<void> will log if the
|
||||||
|
// engine function actually has a ReturnValue property, which is the
|
||||||
|
// signal that you meant to use CallForReturn.
|
||||||
|
template <typename ParamsStruct>
|
||||||
|
auto Call(const TCHAR* FunctionName, ParamsStruct& Params) const -> bool
|
||||||
|
{
|
||||||
|
if (!m_obj)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] '{}' on an invalid wrapper\n"), m_tag, FunctionName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Detail::Api<void>().Invoke(m_obj, FunctionName, Params);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CallVoid(const TCHAR* FunctionName) const -> bool
|
||||||
|
{
|
||||||
|
NoParams p{};
|
||||||
|
return Call(FunctionName, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero-parameter getter.
|
||||||
|
template <typename R>
|
||||||
|
auto CallForReturn(const TCHAR* FunctionName) const -> std::optional<R>
|
||||||
|
{
|
||||||
|
struct Params { R ReturnValue; };
|
||||||
|
Params p{};
|
||||||
|
return CallForReturn<Params, R>(FunctionName, p, &Params::ReturnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter with parameters. ReturnValue must be the LAST member of
|
||||||
|
// ParamsStruct, matching the engine's frame layout.
|
||||||
|
template <typename ParamsStruct, typename R>
|
||||||
|
auto CallForReturn(const TCHAR* FunctionName, ParamsStruct& Params,
|
||||||
|
R ParamsStruct::* ReturnMember) const -> std::optional<R>
|
||||||
|
{
|
||||||
|
if (!m_obj)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] '{}' on an invalid wrapper\n"), m_tag, FunctionName);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return Detail::Api<R>().InvokeForReturn(m_obj, FunctionName, Params, ReturnMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Direct UPROPERTY access.
|
||||||
|
//
|
||||||
|
// Cheaper than ProcessEvent and the only option for properties with
|
||||||
|
// no UFUNCTION accessor (UPanelSlot::Parent and ::Content, for
|
||||||
|
// instance -- that class exposes zero UFUNCTIONs).
|
||||||
|
//
|
||||||
|
// Two things this does NOT handle:
|
||||||
|
// 1. Bitfield bools (uint8 bFoo : 1). FProperty::GetSize() returns
|
||||||
|
// 1 for those too, so the size check passes and you read the
|
||||||
|
// whole byte -- every flag packed into it, not just yours.
|
||||||
|
// Use the UFUNCTION getter for those. That is why UWidget's
|
||||||
|
// bIsEnabled/bIsVariable are not exposed here.
|
||||||
|
// 2. Writes. Poking a property directly skips the setter's
|
||||||
|
// SynchronizeProperties call, so the Slate widget will not
|
||||||
|
// update. Read by property, write by UFUNCTION.
|
||||||
|
//
|
||||||
|
// UE4SS API name check: GetPropertyByNameInChain and
|
||||||
|
// GetOffset_Internal are what I expect this version to expose;
|
||||||
|
// verify against your UObject.hpp / UnrealType.hpp before assuming
|
||||||
|
// this compiles as written.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
template <typename T>
|
||||||
|
auto PropertyPtr(const TCHAR* PropertyName) const -> T*
|
||||||
|
{
|
||||||
|
if (!m_obj) return nullptr;
|
||||||
|
|
||||||
|
FProperty* Prop = m_obj->GetPropertyByNameInChain(PropertyName);
|
||||||
|
if (!Prop)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] property '{}' not found on {}\n"),
|
||||||
|
m_tag, PropertyName, m_obj->GetFullName());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_cast<size_t>(Prop->GetSize()) != sizeof(T))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] property '{}' is {} bytes engine-side, read as {} bytes\n"),
|
||||||
|
m_tag, PropertyName, Prop->GetSize(), sizeof(T));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reinterpret_cast<T*>(
|
||||||
|
reinterpret_cast<uint8*>(m_obj) + Prop->GetOffset_Internal());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// PropertyPtr for the case where ABSENCE IS AN EXPECTED ANSWER, not
|
||||||
|
// a mistake.
|
||||||
|
//
|
||||||
|
// PropertyPtr logs a Warning when the property is missing, and that
|
||||||
|
// log line calls Obj->GetFullName() -- an outer-chain walk with an
|
||||||
|
// FName::ToString at every level (see the UncheckedTag note above).
|
||||||
|
// That is the right behaviour when you are naming a property you
|
||||||
|
// believe exists and want to hear about it if you are wrong.
|
||||||
|
//
|
||||||
|
// It is the wrong behaviour when you are PROBING: "does this widget
|
||||||
|
// happen to be a UUserWidget, i.e. does it have a WidgetTree?" is a
|
||||||
|
// question asked once per node during a tree walk, and the answer is
|
||||||
|
// "no" for almost every node. Logging that is not a diagnostic, it's
|
||||||
|
// a per-node GetFullName() plus a disk write.
|
||||||
|
//
|
||||||
|
// A size mismatch still logs. That one is never an expected answer:
|
||||||
|
// the property exists but is not the shape you think it is, which is
|
||||||
|
// the silent-corruption case, not the absent case.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
template <typename T>
|
||||||
|
auto TryPropertyPtr(const TCHAR* PropertyName) const -> T*
|
||||||
|
{
|
||||||
|
if (!m_obj) return nullptr;
|
||||||
|
|
||||||
|
FProperty* Prop = m_obj->GetPropertyByNameInChain(PropertyName);
|
||||||
|
if (!Prop) return nullptr;
|
||||||
|
|
||||||
|
if (static_cast<size_t>(Prop->GetSize()) != sizeof(T))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] property '{}' is {} bytes engine-side, read as {} bytes\n"),
|
||||||
|
m_tag, PropertyName, Prop->GetSize(), sizeof(T));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reinterpret_cast<T*>(
|
||||||
|
reinterpret_cast<uint8*>(m_obj) + Prop->GetOffset_Internal());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
auto PropertyValue(const TCHAR* PropertyName) const -> std::optional<T>
|
||||||
|
{
|
||||||
|
if (T* Ptr = PropertyPtr<T>(PropertyName)) return *Ptr;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience for the most common property shape in this tree: an
|
||||||
|
// ObjectProperty read back as a bare pointer. Silent on absence.
|
||||||
|
auto TryObjectProperty(const TCHAR* PropertyName) const -> UObject*
|
||||||
|
{
|
||||||
|
auto Ptr = TryPropertyPtr<UObject*>(PropertyName);
|
||||||
|
return Ptr ? *Ptr : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// For `uint8 bFoo : 1;` UPROPERTY bitfields specifically. Do NOT
|
||||||
|
// reach for PropertyValue<uint8>/PropertyPtr<uint8> on one of
|
||||||
|
// these -- it is the wrong tool and it does not fail loudly.
|
||||||
|
//
|
||||||
|
// UHT reflects a bitfield as an FBoolProperty whose GetSize()
|
||||||
|
// reports the storage unit (1 byte), not "1 bit" -- there is no
|
||||||
|
// size check that can tell a lone byte-backed bool apart from one
|
||||||
|
// of several single-bit flags packed into a shared byte. When a
|
||||||
|
// header declares several `uint8 bX : 1;` fields back to back
|
||||||
|
// (exactly the bOverride_* run on USizeBox), the compiler is free
|
||||||
|
// to pack all of them into one physical byte. PropertyPtr<uint8>
|
||||||
|
// would then hand back that WHOLE byte -- every packed flag
|
||||||
|
// OR'd together -- for a call that named only one of them, and
|
||||||
|
// both the found-a-property and the size-matches checks would
|
||||||
|
// pass. That is a silent wrong answer, not a logged failure.
|
||||||
|
//
|
||||||
|
// FBoolProperty carries the two pieces of information that make
|
||||||
|
// this correct: GetByteOffset(), the offset of the specific byte
|
||||||
|
// within the struct (added ON TOP of the base FProperty offset
|
||||||
|
// ContainerPtrToValuePtr already applies -- see
|
||||||
|
// FBoolProperty::GetPropertyValue in UnrealType.hpp, which is
|
||||||
|
// exactly the two-offset addition this function replicates), and
|
||||||
|
// GetFieldMask(), the single bit within that byte belonging to
|
||||||
|
// this specific flag. Test the bit, don't read the byte.
|
||||||
|
auto PropertyBoolValue(const TCHAR* PropertyName) const -> std::optional<bool>
|
||||||
|
{
|
||||||
|
if (!m_obj) return std::nullopt;
|
||||||
|
|
||||||
|
FProperty* Prop = m_obj->GetPropertyByNameInChain(PropertyName);
|
||||||
|
if (!Prop)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] property '{}' not found on {}\n"),
|
||||||
|
m_tag, PropertyName, m_obj->GetFullName());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop);
|
||||||
|
if (!BoolProp)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] property '{}' is not a bool/bitfield property on {} -- ")
|
||||||
|
STR("use PropertyValue<T> for a plain field instead\n"),
|
||||||
|
m_tag, PropertyName, m_obj->GetFullName());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8* Base = reinterpret_cast<const uint8*>(m_obj)
|
||||||
|
+ Prop->GetOffset_Internal() + BoolProp->GetByteOffset();
|
||||||
|
return (*Base & BoolProp->GetFieldMask()) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* m_obj{};
|
||||||
|
const TCHAR* m_tag{ STR("Wrapper") };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// RawObject -- the wrapper for objects this tree has no class for.
|
||||||
|
//
|
||||||
|
// Every helper that wants to read a property off an object it cannot
|
||||||
|
// name a ClassPath for was previously spelling this inline as a local
|
||||||
|
// struct deriving UnrealWrapper with Expected=nullptr (see the old
|
||||||
|
// WidgetTree::FromUserWidget). That spelling takes the "class path did
|
||||||
|
// not resolve" branch of the checking constructor, which emits a
|
||||||
|
// Warning and calls GetFullName() EVERY TIME -- on a per-bind path,
|
||||||
|
// that is the exact cost the UncheckedTag comment above exists to
|
||||||
|
// avoid.
|
||||||
|
//
|
||||||
|
// This uses UncheckedTag, so it is silent by construction. It is the
|
||||||
|
// same idea as Pal/PalIndividual.hpp's LooseObject, hoisted to where
|
||||||
|
// the UMG side can reach it without including the Pal headers.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
class RawObject : public UnrealWrapper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RawObject() = default;
|
||||||
|
explicit RawObject(UObject* Obj, const TCHAR* Tag = STR("Raw"))
|
||||||
|
: UnrealWrapper(Obj, UncheckedTag{}, Tag) {}
|
||||||
|
|
||||||
|
using UnrealWrapper::Call;
|
||||||
|
using UnrealWrapper::CallVoid;
|
||||||
|
using UnrealWrapper::CallForReturn;
|
||||||
|
using UnrealWrapper::PropertyPtr;
|
||||||
|
using UnrealWrapper::PropertyValue;
|
||||||
|
using UnrealWrapper::TryPropertyPtr;
|
||||||
|
using UnrealWrapper::TryObjectProperty;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Cheap "is this object one of ours" probe.
|
||||||
|
//
|
||||||
|
// Wrapper constructors log when the IsA check fails, which is right for
|
||||||
|
// "I expected a CanvasPanel and got something else" and wrong for "walk
|
||||||
|
// this tree and tell me which nodes are panels." Use this to ask first,
|
||||||
|
// then construct.
|
||||||
|
//
|
||||||
|
// A false here can also mean the ClassPath never resolved. That is
|
||||||
|
// deliberately NOT treated as "yes" -- unlike the wrapper constructors,
|
||||||
|
// which accept unchecked on an unresolved class because refusing would
|
||||||
|
// break a call that would otherwise have worked. Here the answer feeds
|
||||||
|
// a branch, so guessing "yes" would send a non-panel down the panel
|
||||||
|
// path. ClassOf<T>() logs once on the failed resolve either way.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
template <typename T>
|
||||||
|
inline auto ObjectIsA(UObject* Obj) -> bool
|
||||||
|
{
|
||||||
|
if (!Obj) return false;
|
||||||
|
UClass* Cls = ClassOf<T>();
|
||||||
|
return Cls && Obj->IsA(Cls);
|
||||||
|
}
|
||||||
|
}
|
||||||
175
Api/UserWidget.hpp
Normal file
175
Api/UserWidget.hpp
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#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<UserWidget>(), 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<CanvasPanel>(STR("Canvas_Base"))
|
||||||
|
template <typename T>
|
||||||
|
auto FindWidgetAs(const TCHAR* Name,
|
||||||
|
WidgetTree::EScope Scope = WidgetTree::EScope::ThisTreeOnly) -> T
|
||||||
|
{
|
||||||
|
return GetWidgetTree().FindAs<T>(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 <typename T>
|
||||||
|
auto Child(const TCHAR* Name,
|
||||||
|
WidgetTree::EScope Scope = WidgetTree::EScope::ThisTreeOnly) -> T
|
||||||
|
{
|
||||||
|
UObject* O = ChildObject(Name, Scope);
|
||||||
|
if (!O)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] no child named '{}' (neither a bound variable nor in the tree)\n"),
|
||||||
|
m_tag, Name);
|
||||||
|
return T{};
|
||||||
|
}
|
||||||
|
if (!ObjectIsA<T>(O))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
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<bool> { return CallForReturn<bool>(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<bool> { return CallForReturn<bool>(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<int32> { return PropertyValue<int32>(STR("Priority")); }
|
||||||
|
auto GetColorAndOpacity()const -> std::optional<FLinearColor_>{ return PropertyValue<FLinearColor_>(STR("ColorAndOpacity")); }
|
||||||
|
auto GetPaddingValue() const -> std::optional<FMargin_> { return PropertyValue<FMargin_>(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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
27
Api/VerticalBox.hpp
Normal file
27
Api/VerticalBox.hpp
Normal file
@@ -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<VerticalBox>(), 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{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
49
Api/VerticalBoxSlot.hpp
Normal file
49
Api/VerticalBoxSlot.hpp
Normal file
@@ -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<VerticalBoxSlot>(), 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<FSlateChildSize_> { return PropertyValue<FSlateChildSize_>(STR("Size")); }
|
||||||
|
auto GetPadding() const -> std::optional<FMargin_> { return PropertyValue<FMargin_>(STR("Padding")); }
|
||||||
|
auto GetHorizontalAlignment() const -> std::optional<EHorizontalAlignment_> { return PropertyValue<EHorizontalAlignment_>(STR("HorizontalAlignment")); }
|
||||||
|
auto GetVerticalAlignment() const -> std::optional<EVerticalAlignment_> { return PropertyValue<EVerticalAlignment_>(STR("VerticalAlignment")); }
|
||||||
|
};
|
||||||
|
}
|
||||||
227
Api/Widget.hpp
Normal file
227
Api/Widget.hpp
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Unreal/FText.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#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<Widget>(), DebugTag()) {}
|
||||||
|
|
||||||
|
// ---- visibility / enable ------------------------------------------
|
||||||
|
|
||||||
|
auto SetVisibility(ESlateVisibility_ InVisibility) -> bool
|
||||||
|
{
|
||||||
|
struct P { ESlateVisibility_ InVisibility; } p{ InVisibility };
|
||||||
|
return Call(STR("SetVisibility"), p);
|
||||||
|
}
|
||||||
|
auto GetVisibility() -> std::optional<ESlateVisibility_>
|
||||||
|
{
|
||||||
|
return CallForReturn<ESlateVisibility_>(STR("GetVisibility"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SetIsEnabled(bool bInIsEnabled) -> bool
|
||||||
|
{
|
||||||
|
struct P { bool bInIsEnabled; } p{ bInIsEnabled };
|
||||||
|
return Call(STR("SetIsEnabled"), p);
|
||||||
|
}
|
||||||
|
auto GetIsEnabled() -> std::optional<bool>
|
||||||
|
{
|
||||||
|
return CallForReturn<bool>(STR("GetIsEnabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto IsVisible() -> std::optional<bool> { return CallForReturn<bool>(STR("IsVisible")); }
|
||||||
|
auto IsRendered() -> std::optional<bool> { return CallForReturn<bool>(STR("IsRendered")); }
|
||||||
|
auto IsInViewport() -> std::optional<bool> { return CallForReturn<bool>(STR("IsInViewport")); }
|
||||||
|
auto IsHovered() -> std::optional<bool> { return CallForReturn<bool>(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<float>
|
||||||
|
{
|
||||||
|
return CallForReturn<float>(STR("GetRenderTransformAngle"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SetRenderOpacity(float InOpacity) -> bool
|
||||||
|
{
|
||||||
|
struct P { float InOpacity; } p{ InOpacity };
|
||||||
|
return Call(STR("SetRenderOpacity"), p);
|
||||||
|
}
|
||||||
|
auto GetRenderOpacity() -> std::optional<float>
|
||||||
|
{
|
||||||
|
return CallForReturn<float>(STR("GetRenderOpacity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- clipping / cursor ---------------------------------------------
|
||||||
|
|
||||||
|
auto SetClipping(EWidgetClipping_ InClipping) -> bool
|
||||||
|
{
|
||||||
|
struct P { EWidgetClipping_ InClipping; } p{ InClipping };
|
||||||
|
return Call(STR("SetClipping"), p);
|
||||||
|
}
|
||||||
|
auto GetClipping() -> std::optional<EWidgetClipping_>
|
||||||
|
{
|
||||||
|
return CallForReturn<EWidgetClipping_>(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<bool> { return CallForReturn<bool>(STR("HasKeyboardFocus")); }
|
||||||
|
auto HasMouseCapture() -> std::optional<bool> { return CallForReturn<bool>(STR("HasMouseCapture")); }
|
||||||
|
auto HasAnyUserFocus() -> std::optional<bool> { return CallForReturn<bool>(STR("HasAnyUserFocus")); }
|
||||||
|
auto HasFocusedDescendants()-> std::optional<bool> { return CallForReturn<bool>(STR("HasFocusedDescendants")); }
|
||||||
|
|
||||||
|
auto HasUserFocus(UObject* PlayerController) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { UObject* PlayerController; bool ReturnValue; } p{ PlayerController, false };
|
||||||
|
return CallForReturn(STR("HasUserFocus"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto HasUserFocusedDescendants(UObject* PlayerController) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
struct P { UObject* PlayerController; bool ReturnValue; } p{ PlayerController, false };
|
||||||
|
return CallForReturn(STR("HasUserFocusedDescendants"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto HasMouseCaptureByUser(int32 UserIndex, int32 PointerIndex) -> std::optional<bool>
|
||||||
|
{
|
||||||
|
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<FVector2D_>
|
||||||
|
{
|
||||||
|
return CallForReturn<FVector2D_>(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<UObject*>
|
||||||
|
{
|
||||||
|
return CallForReturn<UObject*>(STR("GetParent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// UWidget::Slot is a UPROPERTY with no getter.
|
||||||
|
auto GetSlotObject() const -> UObject*
|
||||||
|
{
|
||||||
|
auto Ptr = PropertyPtr<UObject*>(STR("Slot"));
|
||||||
|
return Ptr ? *Ptr : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetOwningPlayer() -> std::optional<UObject*> { return CallForReturn<UObject*>(STR("GetOwningPlayer")); }
|
||||||
|
auto GetOwningLocalPlayer() -> std::optional<UObject*> { return CallForReturn<UObject*>(STR("GetOwningLocalPlayer")); }
|
||||||
|
auto GetGameInstance() -> std::optional<UObject*> { return CallForReturn<UObject*>(STR("GetGameInstance")); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Widget(UObject* Obj, UClass* Expected, const TCHAR* Tag)
|
||||||
|
: UnrealWrapper(Obj, Expected, Tag) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
45
Api/WidgetBlueprintGeneratedClass.hpp
Normal file
45
Api/WidgetBlueprintGeneratedClass.hpp
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Api/UnrealWrapper.hpp>
|
||||||
|
#include <Api/WidgetTree.hpp>
|
||||||
|
|
||||||
|
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<WidgetBlueprintGeneratedClass>(), DebugTag()) {}
|
||||||
|
|
||||||
|
static auto Of(UObject* Instance) -> WidgetBlueprintGeneratedClass
|
||||||
|
{
|
||||||
|
if (!Instance) return WidgetBlueprintGeneratedClass{};
|
||||||
|
|
||||||
|
UClass* Cls = Instance->GetClassPrivate();
|
||||||
|
if (!Cls) return WidgetBlueprintGeneratedClass{};
|
||||||
|
|
||||||
|
UClass* Expected = ClassOf<WidgetBlueprintGeneratedClass>();
|
||||||
|
if (!Expected || !Cls->IsA(Expected)) return WidgetBlueprintGeneratedClass{};
|
||||||
|
|
||||||
|
return WidgetBlueprintGeneratedClass{ static_cast<UObject*>(Cls) };
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TemplateTree() const -> WidgetTree
|
||||||
|
{
|
||||||
|
UObject* Tree = TryObjectProperty(STR("WidgetTree"));
|
||||||
|
return Tree ? WidgetTree{ Tree } : WidgetTree{};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TemplateTreeObject() const -> UObject*
|
||||||
|
{
|
||||||
|
return TryObjectProperty(STR("WidgetTree"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
125
Api/WidgetLayoutLibrary.hpp
Normal file
125
Api/WidgetLayoutLibrary.hpp
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <Reflection/UnrealObjectRef.hpp>
|
||||||
|
|
||||||
|
#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<CanvasPanelSlot>(STR("SlotAsCanvasSlot"), Widget);
|
||||||
|
}
|
||||||
|
auto SlotAsWrapBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsWrapBoxSlot"), Widget); }
|
||||||
|
auto SlotAsWidgetSwitcherSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsWidgetSwitcherSlot"), Widget); }
|
||||||
|
auto SlotAsVerticalBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsVerticalBoxSlot"), Widget); }
|
||||||
|
auto SlotAsUniformGridSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsUniformGridSlot"), Widget); }
|
||||||
|
auto SlotAsSizeBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsSizeBoxSlot"), Widget); }
|
||||||
|
auto SlotAsScrollBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsScrollBoxSlot"), Widget); }
|
||||||
|
auto SlotAsScaleBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsScaleBoxSlot"), Widget); }
|
||||||
|
auto SlotAsSafeBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsSafeBoxSlot"), Widget); }
|
||||||
|
auto SlotAsOverlaySlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsOverlaySlot"), Widget); }
|
||||||
|
auto SlotAsHorizontalBoxSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsHorizontalBoxSlot"), Widget); }
|
||||||
|
auto SlotAsGridSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(STR("SlotAsGridSlot"), Widget); }
|
||||||
|
auto SlotAsBorderSlot(UObject* Widget) -> PanelSlot { return GetSingleton<PanelSlot>(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<FVector2D_>
|
||||||
|
{
|
||||||
|
struct P { UObject* WorldContextObject; FVector2D_ ReturnValue; } p{ WorldContextObject, {} };
|
||||||
|
return CallForReturn(STR("GetViewportSize"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetViewportScale(UObject* WorldContextObject) -> std::optional<float>
|
||||||
|
{
|
||||||
|
struct P { UObject* WorldContextObject; float ReturnValue; } p{ WorldContextObject, 0.f };
|
||||||
|
return CallForReturn(STR("GetViewportScale"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetMousePositionOnViewport(UObject* WorldContextObject) -> std::optional<FVector2D_>
|
||||||
|
{
|
||||||
|
struct P { UObject* WorldContextObject; FVector2D_ ReturnValue; } p{ WorldContextObject, {} };
|
||||||
|
return CallForReturn(STR("GetMousePositionOnViewport"), p, &P::ReturnValue);
|
||||||
|
}
|
||||||
|
auto GetMousePositionOnPlatform() -> std::optional<FVector2D_>
|
||||||
|
{
|
||||||
|
return CallForReturn<FVector2D_>(STR("GetMousePositionOnPlatform"));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MousePositionDPI { float X{}; float Y{}; };
|
||||||
|
auto GetMousePositionScaledByDPI(UObject* Player) -> std::optional<MousePositionDPI>
|
||||||
|
{
|
||||||
|
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<FVector2D_>
|
||||||
|
{
|
||||||
|
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<WidgetLayoutLibrary>(), 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 <typename T>
|
||||||
|
auto GetSingleton(const TCHAR* FunctionName, UObject* InObject) -> T
|
||||||
|
{
|
||||||
|
UObject* Obj = CallStaticObjectGetter(FunctionName, InObject);
|
||||||
|
return Obj ? T{ Obj } : T{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
349
Api/WidgetTree.hpp
Normal file
349
Api/WidgetTree.hpp
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#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<WidgetTree>(), 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<WidgetTree>();
|
||||||
|
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<UObject*>(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 <typename Fn>
|
||||||
|
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<LogLevel::Warning>(
|
||||||
|
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 <typename T>
|
||||||
|
auto FindAs(const TCHAR* Name, EScope Scope = EScope::ThisTreeOnly) -> T
|
||||||
|
{
|
||||||
|
UObject* O = FindObjectByName(Name, Scope);
|
||||||
|
if (!O)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[WidgetTree] no widget named '{}' in this tree\n"), Name);
|
||||||
|
return T{};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ObjectIsA<T>(O))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[WidgetTree] '{}' resolved to {}, which is not a {}\n"),
|
||||||
|
Name, O->GetFullName(), T::DebugTag());
|
||||||
|
return T{};
|
||||||
|
}
|
||||||
|
return T{ O };
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
auto FindFirstOfType(EScope Scope = EScope::ThisTreeOnly) -> T
|
||||||
|
{
|
||||||
|
UClass* Cls = ClassOf<T>();
|
||||||
|
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 <typename T>
|
||||||
|
auto FindAllOfType(EScope Scope = EScope::ThisTreeOnly) -> std::vector<T>
|
||||||
|
{
|
||||||
|
std::vector<T> Out;
|
||||||
|
|
||||||
|
UClass* Cls = ClassOf<T>();
|
||||||
|
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<UObject*>
|
||||||
|
{
|
||||||
|
std::vector<UObject*> 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<LogLevel::Warning>(STR("[WidgetTree] Dump on an invalid tree\n"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[WidgetTree] dump of {}\n"), m_obj->GetFullName());
|
||||||
|
|
||||||
|
int32 Count = 0;
|
||||||
|
ForEach([&](UObject* Node, int32 Depth) -> EVisit
|
||||||
|
{
|
||||||
|
++Count;
|
||||||
|
|
||||||
|
std::basic_string<TCHAR> Indent;
|
||||||
|
Indent.reserve(static_cast<size_t>(Depth) * 2);
|
||||||
|
for (int32 i = 0; i < Depth; ++i) Indent += STR(" ");
|
||||||
|
|
||||||
|
UClass* Cls = Node->GetClassPrivate();
|
||||||
|
std::basic_string<TCHAR> ClassName =
|
||||||
|
Cls ? Cls->GetName() : std::basic_string<TCHAR>(STR("<no class>"));
|
||||||
|
|
||||||
|
if (ObjectIsA<PanelWidget>(Node))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR(" {}{} [{}] ({} children)\n"),
|
||||||
|
Indent.c_str(), Node->GetName().c_str(), ClassName.c_str(),
|
||||||
|
static_cast<int32>(PanelWidget{ Node }.GetChildObjects().size()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR(" {}{} [{}]\n"),
|
||||||
|
Indent.c_str(), Node->GetName().c_str(), ClassName.c_str());
|
||||||
|
}
|
||||||
|
return EVisit::Continue;
|
||||||
|
}, Scope);
|
||||||
|
|
||||||
|
Output::send<LogLevel::Warning>(STR("[WidgetTree] {} widgets\n"), Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================================
|
||||||
|
// CONSTRUCTION
|
||||||
|
// ==================================================================
|
||||||
|
|
||||||
|
auto ConstructWidget(UClass* WidgetClass, const TCHAR* InstanceName) -> UObject*
|
||||||
|
{
|
||||||
|
if (!m_obj)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[WidgetTree] ConstructWidget on an invalid tree\n"));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!WidgetClass)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
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<LogLevel::Warning>(
|
||||||
|
STR("[WidgetTree] ConstructWidget: StaticConstructObject returned null for '{}'\n"),
|
||||||
|
InstanceName ? InstanceName : STR("<unnamed>"));
|
||||||
|
}
|
||||||
|
return Obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
auto ConstructWidget(const TCHAR* InstanceName) -> T
|
||||||
|
{
|
||||||
|
UObject* O = ConstructWidget(ClassOf<T>(), 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 <typename Fn>
|
||||||
|
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<PanelWidget>(Node)) return;
|
||||||
|
|
||||||
|
for (UObject* Child : PanelWidget{ Node }.GetChildObjects())
|
||||||
|
{
|
||||||
|
if (State.bStopped) return;
|
||||||
|
WalkNode(Child, Depth + 1, State, Visit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
25
Api/WrapBox.hpp
Normal file
25
Api/WrapBox.hpp
Normal file
@@ -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<WrapBox>(), 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{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
8
CMakeLists.txt
Normal file
8
CMakeLists.txt
Normal file
@@ -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)
|
||||||
194
Features/IUIFeature.hpp
Normal file
194
Features/IUIFeature.hpp
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
|
||||||
|
#include <Static/LogGate.hpp>
|
||||||
|
|
||||||
|
namespace PalUIExtension::Features
|
||||||
|
{
|
||||||
|
using namespace RC;
|
||||||
|
using namespace RC::Unreal;
|
||||||
|
|
||||||
|
class IUIFeature
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using StringType = std::basic_string<TCHAR>;
|
||||||
|
|
||||||
|
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 <typename... Args>
|
||||||
|
auto SendLog(LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void
|
||||||
|
{
|
||||||
|
Emit(Level, Fmt, std::forward<Args>(Fargs)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
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<Args>(Fargs)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
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<std::chrono::milliseconds>(Now - Last).count() < IntervalMs)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Last = Now;
|
||||||
|
|
||||||
|
Emit(Level, Fmt, std::forward<Args>(Fargs)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
auto SendVerbose(const TCHAR* Fmt, Args&&... Fargs) const -> void
|
||||||
|
{
|
||||||
|
if constexpr (Static::kVerboseLogging)
|
||||||
|
{
|
||||||
|
Emit(LogLevel::Verbose, Fmt, std::forward<Args>(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("<null>") }; }
|
||||||
|
|
||||||
|
static auto FullNameOf(UObject* Obj) -> StringType { return Obj ? Obj->GetFullName() : StringType{ STR("<null>") }; }
|
||||||
|
|
||||||
|
static auto ClassNameOf(UObject* Obj) -> StringType
|
||||||
|
{
|
||||||
|
if (!Obj) return StringType{ STR("<null>") };
|
||||||
|
UClass* Cls = Obj->GetClassPrivate();
|
||||||
|
return Cls ? Cls->GetName() : StringType{ STR("<no class>") };
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto AddressOf(const void* P) -> uint64_t
|
||||||
|
{
|
||||||
|
return static_cast<uint64_t>(reinterpret_cast<uintptr_t>(P));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename... Args>
|
||||||
|
auto Emit(LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void
|
||||||
|
{
|
||||||
|
if (!Fmt) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (Level)
|
||||||
|
{
|
||||||
|
case LogLevel::Error:
|
||||||
|
Output::send<LogLevel::Error>(STR("PalUIExt [{}] "), Name());
|
||||||
|
Output::send<LogLevel::Error>(Fmt, std::forward<Args>(Fargs)...);
|
||||||
|
Output::send<LogLevel::Error>(STR("\n"));
|
||||||
|
break;
|
||||||
|
case LogLevel::Warning:
|
||||||
|
Output::send<LogLevel::Warning>(STR("PalUIExt [{}] "), Name());
|
||||||
|
Output::send<LogLevel::Warning>(Fmt, std::forward<Args>(Fargs)...);
|
||||||
|
Output::send<LogLevel::Warning>(STR("\n"));
|
||||||
|
break;
|
||||||
|
case LogLevel::Verbose:
|
||||||
|
Output::send<LogLevel::Verbose>(STR("PalUIExt [{}] "), Name());
|
||||||
|
Output::send<LogLevel::Verbose>(Fmt, std::forward<Args>(Fargs)...);
|
||||||
|
Output::send<LogLevel::Verbose>(STR("\n"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Output::send<LogLevel::Normal>(STR("PalUIExt [{}] "), Name());
|
||||||
|
Output::send<LogLevel::Normal>(Fmt, std::forward<Args>(Fargs)...);
|
||||||
|
Output::send<LogLevel::Normal>(STR("\n"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception&)
|
||||||
|
{
|
||||||
|
try { Output::send<LogLevel::Error>(STR("PalUIExt: malformed log format string\n")); }
|
||||||
|
catch (...) {}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutable std::unordered_set<StringType> m_log_once_keys;
|
||||||
|
mutable std::unordered_map<StringType, std::chrono::steady_clock::time_point> 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::microseconds>(
|
||||||
|
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{};
|
||||||
|
};
|
||||||
|
}
|
||||||
449
Features/PalBoxNightWorkIndicator.hpp
Normal file
449
Features/PalBoxNightWorkIndicator.hpp
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
#include <Unreal/Hooks.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
#include <Unreal/NameTypes.hpp>
|
||||||
|
|
||||||
|
#include <Reflection/UnrealApiCall.hpp>
|
||||||
|
|
||||||
|
#include <Api/UmgTypes.hpp>
|
||||||
|
#include <Api/Overlay.hpp>
|
||||||
|
#include <Api/WidgetTree.hpp>
|
||||||
|
#include <Api/WidgetBlueprintGeneratedClass.hpp>
|
||||||
|
#include <Api/PalCommonCharacterSlot.hpp>
|
||||||
|
#include <Api/Mod/PalDetail_PalIconNightEye.hpp>
|
||||||
|
#include <Api/Pal/PalIndividual.hpp>
|
||||||
|
#include <Api/Pal/PalUtility.hpp>
|
||||||
|
|
||||||
|
#include <Static/EventHookRegistry.hpp>
|
||||||
|
|
||||||
|
#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<int32>(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<UObject*, SlotEntry> m_slots;
|
||||||
|
|
||||||
|
// ---- hooks ---------------------------------------------------
|
||||||
|
|
||||||
|
static void OnUpdateSlotBinded(UnrealScriptFunctionCallableContext& Ctx, void* CustomData)
|
||||||
|
{
|
||||||
|
auto* Self = static_cast<PalBoxNightWorkIndicator*>(CustomData);
|
||||||
|
if (!Self || !Self->m_enabled || !Ctx.Context) return;
|
||||||
|
|
||||||
|
auto& Frame = Ctx.GetParams<Api::PalCommonCharacterSlot::UpdateSlotBindedFrame>();
|
||||||
|
Self->Apply(Ctx.Context, Frame.TargetSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnSetEmpty(UnrealScriptFunctionCallableContext& Ctx, void* CustomData)
|
||||||
|
{
|
||||||
|
auto* Self = static_cast<PalBoxNightWorkIndicator*>(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<FName>
|
||||||
|
{
|
||||||
|
if (!Slot) return std::nullopt;
|
||||||
|
|
||||||
|
Api::Pal::LooseObject S{ Slot, STR("PalIndividualSlot") };
|
||||||
|
UObject* Handle = S.CallForReturn<UObject*>(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<Api::Mod::PalDetail_PalIconNightEye>();
|
||||||
|
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<int32>(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<Api::Mod::PalDetail_PalIconNightEye>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
10
Features/PalBoxPalNumber.cpp
Normal file
10
Features/PalBoxPalNumber.cpp
Normal file
@@ -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;
|
||||||
|
}
|
||||||
331
Features/PalBoxPalNumber.hpp
Normal file
331
Features/PalBoxPalNumber.hpp
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
#include <Unreal/Hooks.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
|
||||||
|
#include <Reflection/UnrealApiCall.hpp>
|
||||||
|
|
||||||
|
#include <Api/CanvasPanel.hpp>
|
||||||
|
#include <Api/CanvasPanelSlot.hpp>
|
||||||
|
#include <Api/PalDetailWidget.hpp>
|
||||||
|
#include <Api/Mod/PalDetail_PalNumber.hpp>
|
||||||
|
#include <Api/Pal/PalIndividual.hpp>
|
||||||
|
#include <Api/Pal/PalTypes.hpp>
|
||||||
|
#include <Api/Pal/PalUtility.hpp>
|
||||||
|
#include <Helpers/PalHelpers.hpp>
|
||||||
|
#include <Static/EventHookRegistry.hpp>
|
||||||
|
#include <Static/LogGate.hpp>
|
||||||
|
#include <Static/WidgetRegistry.hpp>
|
||||||
|
|
||||||
|
#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<PalBoxPalNumber*>(CustomData);
|
||||||
|
if (!Self || !Ctx.Context) return;
|
||||||
|
|
||||||
|
auto& Frame = Ctx.GetParams<Api::PalDetailWidget::BindFromHandleFrame>();
|
||||||
|
Self->HandleBind(Ctx.Context, Frame.targetHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnSetEmpty(UnrealScriptFunctionCallableContext& Ctx, void* CustomData)
|
||||||
|
{
|
||||||
|
auto* Self = static_cast<PalBoxPalNumber*>(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<int32> 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("<null>"));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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<LogLevel::Error>(
|
||||||
|
STR("[PalNumberLabel] {} is not a PalDetailWidget\n"), Owner->GetFullName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Api::CanvasPanel Canvas = Panel.CanvasInfo();
|
||||||
|
if (!Canvas.IsValid())
|
||||||
|
{
|
||||||
|
Canvas = Panel.Child<Api::CanvasPanel>(
|
||||||
|
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<PalKey>
|
||||||
|
{
|
||||||
|
if (!Handle) return std::nullopt;
|
||||||
|
|
||||||
|
Api::Pal::LooseObject H{ Handle, STR("PalIndividualCharacterHandle") };
|
||||||
|
auto Id = H.CallForReturn<Api::Pal::FPalInstanceID>(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<Api::Mod::PalDetail_PalNumberWidget>(Candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto RegistryKey(UObject* Owner) -> Static::WidgetRegistry::KeyType
|
||||||
|
{
|
||||||
|
static_assert(std::is_same_v<TCHAR, wchar_t>,
|
||||||
|
"RegistryKey builds its suffix with std::to_wstring");
|
||||||
|
|
||||||
|
return Static::WidgetRegistry::KeyType{ STR("PalBoxNumberLabel:") } +
|
||||||
|
std::to_wstring(reinterpret_cast<uintptr_t>(Owner));
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto WidgetApi() -> Reflection::UnrealApiCall<Tag_PalNumberWidget>&
|
||||||
|
{
|
||||||
|
static Reflection::UnrealApiCall<Tag_PalNumberWidget> Api{
|
||||||
|
Api::Mod::PalDetail_PalNumberWidget::ClassPath(),
|
||||||
|
Api::Mod::PalDetail_PalNumberWidget::DebugTag() };
|
||||||
|
return Api;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int32 kUnknownIndex = -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
281
Features/PalStatusPalNumber.hpp
Normal file
281
Features/PalStatusPalNumber.hpp
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
#include <Unreal/Hooks.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
|
||||||
|
#include <Reflection/UnrealApiCall.hpp>
|
||||||
|
|
||||||
|
#include <Api/CanvasPanel.hpp>
|
||||||
|
#include <Api/CanvasPanelSlot.hpp>
|
||||||
|
#include <Api/MainMenuPalWidget.hpp>
|
||||||
|
#include <Api/Mod/PalStatus_PalNumber.hpp>
|
||||||
|
#include <Api/Pal/PalIndividual.hpp>
|
||||||
|
#include <Api/Pal/PalTypes.hpp>
|
||||||
|
#include <Api/Pal/PalUtility.hpp>
|
||||||
|
#include <Helpers/PalHelpers.hpp>
|
||||||
|
#include <Static/EventHookRegistry.hpp>
|
||||||
|
#include <Static/LogGate.hpp>
|
||||||
|
#include <Static/WidgetRegistry.hpp>
|
||||||
|
|
||||||
|
#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<PalStatusPalNumber*>(CustomData);
|
||||||
|
if (!Self || !Ctx.Context) return;
|
||||||
|
|
||||||
|
auto& Frame = Ctx.GetParams<Api::MainMenuPalWidget::BindFromHandleFrame>();
|
||||||
|
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<int32> 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("<null>"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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<LogLevel::Error>(
|
||||||
|
STR("[PalNumberLabel] {} is not a MainMenuPalWidget\n"), Owner->GetFullName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Api::CanvasPanel Canvas = Panel.CanvasPalName();
|
||||||
|
if (!Canvas.IsValid())
|
||||||
|
{
|
||||||
|
Canvas = Panel.Child<Api::CanvasPanel>(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<PalKey>
|
||||||
|
{
|
||||||
|
if (!Handle) return std::nullopt;
|
||||||
|
|
||||||
|
Api::Pal::LooseObject H{ Handle, STR("PalIndividualCharacterHandle") };
|
||||||
|
auto Id = H.CallForReturn<Api::Pal::FPalInstanceID>(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<Api::Mod::PalStatus_PalNumberWidget>(Candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto RegistryKey(UObject* Owner) -> Static::WidgetRegistry::KeyType
|
||||||
|
{
|
||||||
|
static_assert(std::is_same_v<TCHAR, wchar_t>,
|
||||||
|
"RegistryKey builds its suffix with std::to_wstring");
|
||||||
|
|
||||||
|
return Static::WidgetRegistry::KeyType{ STR("PalStatusNumberLabel:") } +
|
||||||
|
std::to_wstring(reinterpret_cast<uintptr_t>(Owner));
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto WidgetApi() -> Reflection::UnrealApiCall<Tag_PalStatusNumberWidget>&
|
||||||
|
{
|
||||||
|
static Reflection::UnrealApiCall<Tag_PalStatusNumberWidget> Api{
|
||||||
|
Api::Mod::PalStatus_PalNumberWidget::ClassPath(),
|
||||||
|
Api::Mod::PalStatus_PalNumberWidget::DebugTag() };
|
||||||
|
return Api;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int32 kUnknownIndex = -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
38
Helpers/PalHelpers.hpp
Normal file
38
Helpers/PalHelpers.hpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
|
||||||
|
#include <Unreal/UObjectGlobals.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
#include <Unreal/UClass.hpp>
|
||||||
|
|
||||||
|
#include <Reflection/UnrealObjectRef.hpp>
|
||||||
|
#include <Reflection/UnrealApiCall.hpp>
|
||||||
|
|
||||||
|
#include <Api/Pal/PalTypes.hpp>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
Helpers/TextFromString.hpp
Normal file
48
Helpers/TextFromString.hpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
|
||||||
|
#include <Unreal/UObjectGlobals.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
#include <Unreal/UClass.hpp>
|
||||||
|
|
||||||
|
#include <Reflection/UnrealObjectRef.hpp>
|
||||||
|
#include <Reflection/UnrealApiCall.hpp>
|
||||||
|
|
||||||
|
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<struct Tag_KismetText, FText> 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
230
Reflection/UnrealApiCall.hpp
Normal file
230
Reflection/UnrealApiCall.hpp
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
|
||||||
|
#include <Unreal/UObjectGlobals.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
#include <Unreal/UClass.hpp>
|
||||||
|
#include <Unreal/CoreUObject/UObject/Class.hpp>
|
||||||
|
#include <Unreal/CoreUObject/UObject/UnrealType.hpp>
|
||||||
|
#include <Static/ClassCache.hpp>
|
||||||
|
|
||||||
|
namespace PalUIExtension::Reflection
|
||||||
|
{
|
||||||
|
using namespace RC;
|
||||||
|
using namespace RC::Unreal;
|
||||||
|
|
||||||
|
template <typename TClass, typename TReturn = void> class UnrealApiCall {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
explicit UnrealApiCall(const TCHAR* ClassPath, const TCHAR* DebugTag) : m_class_path(ClassPath), m_debug_tag(DebugTag)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResolveClass() -> UClass*
|
||||||
|
{
|
||||||
|
if (s_cached_class) return s_cached_class;
|
||||||
|
|
||||||
|
UObject* Found = UObjectGlobals::StaticFindObject<UObject*>(
|
||||||
|
nullptr, nullptr, m_class_path, false);
|
||||||
|
|
||||||
|
if (!Found)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] ResolveClass: could not find class '{}'\n"),
|
||||||
|
m_debug_tag, m_class_path);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_cached_class = static_cast<UClass*>(Found);
|
||||||
|
|
||||||
|
// Registered on first successful resolve rather than at construction,
|
||||||
|
// because there is no constructor for a static inline member. Guarded
|
||||||
|
// so a re-resolve after a flush does not register the same slot
|
||||||
|
// twice -- the vector would still work, but the duplicate entries
|
||||||
|
// would grow without bound across worlds.
|
||||||
|
if (!s_slot_registered)
|
||||||
|
{
|
||||||
|
Static::ClassCache::RegisterSlot(&s_cached_class);
|
||||||
|
s_slot_registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_cached_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto FindInstance(UObject* Outer, const TCHAR* InstanceName, bool bExactClass = false) -> UObject*
|
||||||
|
{
|
||||||
|
UClass* Cls = ResolveClass();
|
||||||
|
if (!Cls)
|
||||||
|
{
|
||||||
|
// ResolveClass already logged the reason.
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* Instance = UObjectGlobals::FindObject(Cls, Outer, InstanceName, bExactClass);
|
||||||
|
if (!Instance)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] FindInstance: '{}' not found under outer {}\n"),
|
||||||
|
m_debug_tag, InstanceName,
|
||||||
|
Outer ? Outer->GetFullName() : STR("<null>"));
|
||||||
|
}
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto FindOrConstruct(UObject* Outer, const TCHAR* InstanceName, bool& bOutWasNew, bool bExactClass = false) -> UObject*
|
||||||
|
{
|
||||||
|
bOutWasNew = false;
|
||||||
|
|
||||||
|
if (!InstanceName)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] FindOrConstruct: null InstanceName\n"), m_debug_tag);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UObject* Existing = FindInstance(Outer, InstanceName, bExactClass))
|
||||||
|
{
|
||||||
|
return Existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
bOutWasNew = true;
|
||||||
|
return ConstructInstance(Outer, InstanceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ParamsStruct>
|
||||||
|
auto Invoke(UObject* Target, const TCHAR* FunctionName, ParamsStruct& Params) -> bool
|
||||||
|
{
|
||||||
|
if (!Target)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] Invoke: null target calling '{}'\n"), m_debug_tag, FunctionName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UFunction* Fn = Target->GetFunctionByNameInChain(FunctionName);
|
||||||
|
if (!Fn)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] Invoke: '{}' not found on {}\n"),
|
||||||
|
m_debug_tag, FunctionName, Target->GetFullName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
check_struct_size(Fn, FunctionName, sizeof(ParamsStruct));
|
||||||
|
check_return_type(Fn, FunctionName);
|
||||||
|
|
||||||
|
Target->ProcessEvent(Fn, &Params);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
using ReturnMemberType = std::conditional_t<std::is_void_v<TReturn>, char, TReturn>;
|
||||||
|
|
||||||
|
template <typename ParamsStruct>
|
||||||
|
auto InvokeForReturn(UObject* Target, const TCHAR* FunctionName, ParamsStruct& Params,
|
||||||
|
ReturnMemberType ParamsStruct::* ReturnMember) -> std::optional<ReturnMemberType>
|
||||||
|
{
|
||||||
|
static_assert(!std::is_void_v<TReturn>,
|
||||||
|
"InvokeForReturn requires a non-void TReturn; use Invoke() for void calls.");
|
||||||
|
|
||||||
|
if (!Invoke(Target, FunctionName, Params))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return Params.*ReturnMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ConstructInstance(UObject* Outer, const TCHAR* InstanceName = nullptr) -> UObject*
|
||||||
|
{
|
||||||
|
UClass* Cls = ResolveClass();
|
||||||
|
if (!Cls) return nullptr;
|
||||||
|
|
||||||
|
if (!Outer)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(STR("[{}] ConstructInstance: null Outer -- the object will be unreachable by GC and collected out from under you.\n"), m_debug_tag);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FStaticConstructObjectParameters Params{ Cls };
|
||||||
|
Params.Outer = Outer;
|
||||||
|
if (InstanceName) Params.Name = FName(InstanceName);
|
||||||
|
|
||||||
|
UObject* Obj = UObjectGlobals::StaticConstructObject(Params);
|
||||||
|
if (!Obj)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(STR("[{}] ConstructInstance: StaticConstructObject returned null\n"), m_debug_tag);
|
||||||
|
}
|
||||||
|
return Obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
auto check_struct_size(UFunction* Fn, const TCHAR* FunctionName, size_t ActualStructSize) -> void
|
||||||
|
{
|
||||||
|
// UStruct::GetStructureSize() (Class.hpp) reports the engine's own
|
||||||
|
// aligned size for the function's param block.
|
||||||
|
|
||||||
|
int32 EngineSize = Fn->GetStructureSize();
|
||||||
|
if (EngineSize > 0 && static_cast<size_t>(EngineSize) != ActualStructSize)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] Invoke: '{}' param struct size mismatch -- ")
|
||||||
|
STR("engine expects {} bytes, ParamsStruct is {} bytes. ")
|
||||||
|
STR("Check field order, padding, and widths (e.g. float vs double) ")
|
||||||
|
STR("before trusting this call's output.\n"),
|
||||||
|
m_debug_tag, FunctionName, EngineSize, ActualStructSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto check_return_type(UFunction* Fn, const TCHAR* FunctionName) -> void
|
||||||
|
{
|
||||||
|
FProperty* ReturnProp = Fn->GetReturnProperty();
|
||||||
|
|
||||||
|
if constexpr (std::is_void_v<TReturn>)
|
||||||
|
{
|
||||||
|
if (ReturnProp)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] Invoke: '{}' has a return value on the engine side, ")
|
||||||
|
STR("but this UnrealApiCall was declared with TReturn=void -- ")
|
||||||
|
STR("you're discarding a value the function actually produces.\n"),
|
||||||
|
m_debug_tag, FunctionName);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ReturnProp)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] Invoke: '{}' has no ReturnValue property on the engine side, ")
|
||||||
|
STR("but this UnrealApiCall expects a return -- the member you read back ")
|
||||||
|
STR("will not have been written by ProcessEvent.\n"),
|
||||||
|
m_debug_tag, FunctionName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 EnginePropSize = ReturnProp->GetSize();
|
||||||
|
if (static_cast<size_t>(EnginePropSize) != sizeof(TReturn))
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
STR("[{}] Invoke: '{}' ReturnValue is {} bytes on the engine side, ")
|
||||||
|
STR("TReturn is {} bytes -- this is a real mismatch, not a formality. ")
|
||||||
|
STR("Reading this value as TReturn will read garbage or truncate it.\n"),
|
||||||
|
m_debug_tag, FunctionName, EnginePropSize, sizeof(TReturn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TCHAR* m_class_path;
|
||||||
|
const TCHAR* m_debug_tag;
|
||||||
|
|
||||||
|
static inline UClass* s_cached_class{};
|
||||||
|
static inline bool s_slot_registered{ false };
|
||||||
|
};
|
||||||
|
}
|
||||||
46
Reflection/UnrealObjectRef.hpp
Normal file
46
Reflection/UnrealObjectRef.hpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
|
||||||
|
#include <Unreal/UObjectGlobals.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
#include <Unreal/UClass.hpp>
|
||||||
|
#include <Unreal/CoreUObject/UObject/Class.hpp>
|
||||||
|
#include <Unreal/CoreUObject/UObject/UnrealType.hpp>
|
||||||
|
#include <Static/ClassCache.hpp>
|
||||||
|
|
||||||
|
namespace PalUIExtension::Reflection
|
||||||
|
{
|
||||||
|
using namespace RC;
|
||||||
|
using namespace RC::Unreal;
|
||||||
|
|
||||||
|
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<UObject*>(nullptr, nullptr, m_path, false);
|
||||||
|
if (!m_cached)
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Warning>(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{};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
BIN
Screenshot 2026-09-06 153717.png
Normal file
BIN
Screenshot 2026-09-06 153717.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
49
Static/ClassCache.hpp
Normal file
49
Static/ClassCache.hpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
|
||||||
|
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<LogLevel::Verbose>(
|
||||||
|
STR("[ClassCache] flushed {} object slots and {} class slots\n"),
|
||||||
|
static_cast<int32>(s_slots.size()),
|
||||||
|
static_cast<int32>(s_class_slots.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto SlotCount() -> size_t { return s_slots.size() + s_class_slots.size(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
inline static std::vector<UObject**> s_slots{};
|
||||||
|
inline static std::vector<UClass**> s_class_slots{};
|
||||||
|
};
|
||||||
|
}
|
||||||
278
Static/EventHookRegistry.hpp
Normal file
278
Static/EventHookRegistry.hpp
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
|
||||||
|
#include <Unreal/Hooks.hpp>
|
||||||
|
#include <Unreal/UObjectGlobals.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
#include <Unreal/UClass.hpp>
|
||||||
|
#include <Unreal/UFunction.hpp>
|
||||||
|
|
||||||
|
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 <class name, function name> pair, then installs the real hook.
|
||||||
|
//
|
||||||
|
// NOT THREAD SAFE.
|
||||||
|
|
||||||
|
class EventHookRegistry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using StringType = std::basic_string<TCHAR>;
|
||||||
|
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<HookRequest> s_pending{};
|
||||||
|
inline static std::vector<InstalledHook> 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<LogLevel::Error>(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<LogLevel::Error>(
|
||||||
|
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<UFunction*>(nullptr, nullptr, Req.FunctionPath.c_str()))
|
||||||
|
{
|
||||||
|
Install(Req, Fn);
|
||||||
|
return Req.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Output::send<LogLevel::Verbose>(
|
||||||
|
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<LogLevel::Warning>(
|
||||||
|
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<LogLevel::Verbose>(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<UFunction*>(nullptr, nullptr, Req.FunctionPath.c_str());
|
||||||
|
|
||||||
|
if (!Target)
|
||||||
|
{
|
||||||
|
if (Req.FunctionName != Function->GetNamePrivate()) continue;
|
||||||
|
|
||||||
|
Output::send<LogLevel::Warning>(
|
||||||
|
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<LogLevel::Warning>(
|
||||||
|
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<LogLevel::Verbose>(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
9
Static/LogGate.hpp
Normal file
9
Static/LogGate.hpp
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace PalUIExtension::Static
|
||||||
|
{
|
||||||
|
|
||||||
|
inline constexpr bool kVerboseLogging = false;
|
||||||
|
inline constexpr bool kProfileHotPath = true;
|
||||||
|
|
||||||
|
}
|
||||||
139
Static/WidgetRegistry.hpp
Normal file
139
Static/WidgetRegistry.hpp
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
#include <Unreal/UObject.hpp>
|
||||||
|
|
||||||
|
#include <Api/Widget.hpp>
|
||||||
|
|
||||||
|
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<TCHAR>;
|
||||||
|
|
||||||
|
// ---- generation control --------------------------------------
|
||||||
|
|
||||||
|
// Advances the generation and drops every existing entry.
|
||||||
|
|
||||||
|
static auto BeginGeneration() -> Generation
|
||||||
|
{
|
||||||
|
++s_generation;
|
||||||
|
s_entries.clear();
|
||||||
|
Output::send<LogLevel::Verbose>(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<LogLevel::Warning>(STR("[WidgetRegistry] Register('{}'): refusing a null object\n"), Key.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_entries.find(Key) != s_entries.end())
|
||||||
|
{
|
||||||
|
Output::send<LogLevel::Verbose>(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<LogLevel::Verbose>(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<T>() -- 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 <typename T>
|
||||||
|
static auto Get(const KeyType& Key) -> T
|
||||||
|
{
|
||||||
|
UObject* Obj = GetObject(Key);
|
||||||
|
return Obj ? T{ Obj } : T{};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static auto Get(const TCHAR* Key) -> T
|
||||||
|
{
|
||||||
|
return Get<T>(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<KeyType, Entry> s_entries{};
|
||||||
|
inline static Generation s_generation{ 1 };
|
||||||
|
};
|
||||||
|
}
|
||||||
181
dllmain.cpp
Normal file
181
dllmain.cpp
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <Mod/CppUserModBase.hpp>
|
||||||
|
#include <DynamicOutput/DynamicOutput.hpp>
|
||||||
|
|
||||||
|
#include <Unreal/Hooks.hpp>
|
||||||
|
|
||||||
|
#include <Static/EventHookRegistry.hpp>
|
||||||
|
#include <Api/Pal/PalGameMode.hpp>
|
||||||
|
#include <Features/IUIFeature.hpp>
|
||||||
|
#include <Features/PalBoxPalNumber.hpp>
|
||||||
|
#include <Features/PalBoxNightWorkIndicator.hpp>
|
||||||
|
#include <Features/PalStatusPalNumber.hpp>
|
||||||
|
|
||||||
|
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<std::unique_ptr<Features::IUIFeature>> 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<LogLevel::Verbose>(
|
||||||
|
STR("[PalUIExtension] installed feature '{}'\n"), Feature->Name());
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallWorldLifecycleHooks();
|
||||||
|
|
||||||
|
m_installed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
auto BuildFeatures() -> void
|
||||||
|
{
|
||||||
|
m_features.push_back(std::make_unique<Features::PalBoxPalNumber>());
|
||||||
|
m_features.push_back(std::make_unique<Features::PalBoxNightWorkIndicator>());
|
||||||
|
m_features.push_back(std::make_unique<Features::PalStatusPalNumber>());
|
||||||
|
}
|
||||||
|
|
||||||
|
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<LogLevel::Warning>(
|
||||||
|
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<bool, bool>
|
||||||
|
{
|
||||||
|
if (s_instance)
|
||||||
|
{
|
||||||
|
s_instance->TeardownAllFeatures(STR("LoadMap"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { true, true };
|
||||||
|
});
|
||||||
|
|
||||||
|
Output::send<LogLevel::Verbose>(
|
||||||
|
STR("[PalUIExtension] world lifecycle hooks installed (restart={})\n"), RestartId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnRestartGamePre(UnrealScriptFunctionCallableContext& Ctx, void* CustomData)
|
||||||
|
{
|
||||||
|
auto* Self = static_cast<PalUIExtension*>(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<LogLevel::Error>(
|
||||||
|
STR("[PalUIExtension] '{}' threw during teardown; its cached pointers "
|
||||||
|
"are now stale.\n"), Feature->Name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Output::send<LogLevel::Verbose>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
night_work_icons.jpg
Normal file
BIN
night_work_icons.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
BIN
palbox_numbers.jpg
Normal file
BIN
palbox_numbers.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
BIN
palstatus_numbers.png
Normal file
BIN
palstatus_numbers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 915 KiB |
12
post_build.bat
Normal file
12
post_build.bat
Normal file
@@ -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%
|
||||||
Reference in New Issue
Block a user