#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "IUIFeature.hpp" namespace PalUIExtension::Features { using namespace RC; using namespace RC::Unreal; using namespace Helpers; struct Tag_PalNumberWidget {}; class PalBoxPalNumber final : public IUIFeature { public: bool m_enabled = true; static constexpr const TCHAR* kInstanceName = STR("PalUI_PalDetail_PalNumber"); // nudge the rest of the canvas components down this much to make room for pal number static constexpr double kCanvasNudgeY = 18.0; static constexpr double kLabelOffsetY = 8.0; static constexpr double kLabelOffsetXFraction = 0.4; auto Name() const -> const TCHAR* override { return STR("PalNumberLabel"); } auto InstallHooks() -> void override { Static::EventHookRegistry::Register( STR("PalNumberLabel::BindFromHandle"), Api::PalDetailWidget::BindFromHandlePath(), &PalBoxPalNumber::OnBindFromHandle, nullptr, this); Static::EventHookRegistry::Register( STR("PalNumberLabel::SetEmpty"), Api::PalDetailWidget::SetEmptyPath(), &PalBoxPalNumber::OnSetEmpty, nullptr, this); } auto OnWorldTeardown() -> void override { ForgetAll(); } auto SetEnabled(bool bEnabled) -> void override { m_enabled = bEnabled; } auto GetEnabled() -> bool override { return m_enabled; } private: UObject* m_owner{}; // the detail panel this memo describes UObject* m_label{}; // the injected widget under that panel Helpers::PalKey m_key{}; bool m_has_key{}; int32 m_index{}; bool m_has_index{}; bool m_visible{ true }; // last visibility WE set bool m_layout_done{}; // canvas nudge applied for m_owner // ---- hooks ---------------------------------------- static void OnBindFromHandle(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) { auto* Self = static_cast(CustomData); if (!Self || !Ctx.Context) return; auto& Frame = Ctx.GetParams(); Self->HandleBind(Ctx.Context, Frame.targetHandle); } static void OnSetEmpty(UnrealScriptFunctionCallableContext& Ctx, void* CustomData) { auto* Self = static_cast(CustomData); if (!Self || !Ctx.Context) return; Self->HandleEmpty(Ctx.Context); } // ---- the actual work ----------------------------------------- auto HandleBind(UObject* Owner, UObject* Handle) -> void { if (Owner != m_owner) AdoptOwner(Owner); const auto Key = ReadKey(Handle); if (!Key.has_value()) { HideLabel(); m_has_key = false; return; } if (m_has_key && SamePalIndividual(m_key, *Key)) return; // the early out that matters UObject* Label = ResolveLabel(Owner); if (!Label) return; Api::Mod::PalDetail_PalNumberWidget W{ Label }; if (!W.IsValid()) return; m_key = *Key; m_has_key = true; const auto Species = W.SetPalBinding(BuildBindingParam(m_key)); if (!Species.has_value()) { ShowLabel(W); return; } const auto Index = Api::Pal::PalUtility::Instance().ZukanIndexFor(*Species, Owner); ShowLabel(W); ApplyIndex(W, Index); } auto HandleEmpty(UObject* Owner) -> void { if (Owner != m_owner) AdoptOwner(Owner); m_has_key = false; HideLabel(); } auto ApplyIndex(Api::Mod::PalDetail_PalNumberWidget& W, std::optional Index) -> void { const int32 Resolved = Index.value_or(kUnknownIndex); if (m_has_index && m_index == Resolved) return; m_index = Resolved; m_has_index = true; const FString Text = (Resolved == kUnknownIndex) ? FString(STR("?")) : FString::Printf(STR("%d"), Resolved); if (!W.SetPalNumberText(Text)) { m_has_index = false; SendLog(LogLevel::Error, STR("[PalNumberLabel] SetPalNumberText failed on {}\n"), W.Get() ? W.Get()->GetFullName() : STR("")); } } // ---- widget resolution --------------------------------------- auto ResolveLabel(UObject* Owner) -> UObject* { if (m_label && IsOurLabel(m_label, Owner)) return m_label; const auto Key = RegistryKey(Owner); if (UObject* Cached = Static::WidgetRegistry::GetObject(Key); Cached && IsOurLabel(Cached, Owner)) { m_label = Cached; return m_label; } bool bWasNew = false; UObject* Found = WidgetApi().FindOrConstruct(Owner, kInstanceName, bWasNew); if (!Found) { SendLog(LogLevel::Error, STR("[PalNumberLabel] could not find or construct '{}' under {}\n"), kInstanceName, Owner->GetFullName()); return nullptr; } if (bWasNew && !AttachLabel(Owner, Found)) { return nullptr; } m_label = Found; m_visible = true; // freshly attached widgets are visible Static::WidgetRegistry::Register(Key, Found); return m_label; } auto AttachLabel(UObject* Owner, UObject* Label) -> bool { Api::PalDetailWidget Panel{ Owner }; if (!Panel.IsValid()) { Output::send( STR("[PalNumberLabel] {} is not a PalDetailWidget\n"), Owner->GetFullName()); return false; } Api::CanvasPanel Canvas = Panel.CanvasInfo(); if (!Canvas.IsValid()) { Canvas = Panel.Child( STR("Canvas_Info"), Api::WidgetTree::EScope::IncludeNestedUserWidgets); } if (!Canvas.IsValid()) return false; if (!m_layout_done) { UObject* CanvasSlotObj = Canvas.GetSlotObject(); Api::CanvasPanelSlot CanvasSlot = CanvasSlotObj ? Api::CanvasPanelSlot{ CanvasSlotObj } : Api::CanvasPanelSlot{}; if (CanvasSlot.IsValid()) { Api::FVector2D_ Pos = CanvasSlot.GetPosition().value_or(Api::FVector2D_{ 0, 0 }); Pos.Y += kCanvasNudgeY; CanvasSlot.SetPosition(Pos); } m_layout_done = true; } Api::PanelSlot Added = Canvas.AddChild(Label); if (!Added.IsValid()) return false; Api::CanvasPanelSlot LabelSlot{ Added.Get() }; if (LabelSlot.IsValid()) { const Api::FVector2D_ Size = LabelSlot.GetSize().value_or(Api::FVector2D_{ 0, 0 }); LabelSlot.SetPosition(Api::FVector2D_{ Size.X * kLabelOffsetXFraction, kLabelOffsetY }); } return true; } // ---- visibility ---------------------------------------------- auto ShowLabel(Api::Mod::PalDetail_PalNumberWidget& W) -> void { if (m_visible) return; if (W.SetVisibility(Api::ESlateVisibility_::SelfHitTestInvisible)) m_visible = true; } auto HideLabel() -> void { if (!m_visible || !m_label) return; Api::Widget W{ m_label }; if (!W.IsValid()) return; if (W.SetVisibility(Api::ESlateVisibility_::Collapsed)) m_visible = false; } // ---- identity ------------------------------------------------- static auto ReadKey(UObject* Handle) -> std::optional { if (!Handle) return std::nullopt; Api::Pal::LooseObject H{ Handle, STR("PalIndividualCharacterHandle") }; auto Id = H.CallForReturn(STR("GetIndividualID")); if (!Id.has_value()) return std::nullopt; return PalKey{ Id->PlayerUID, Id->InstanceID }; } static auto BuildBindingParam(const PalKey& Key) -> Api::Pal::FPalInstanceID { return Api::Pal::FPalInstanceID{ Key.PlayerUID, Key.InstanceID, FString{} }; } // ---- memo management ----------------------------------------- auto AdoptOwner(UObject* Owner) -> void { m_owner = Owner; m_label = nullptr; m_has_key = false; m_has_index = false; m_visible = true; m_layout_done = false; } auto ForgetAll() -> void { m_owner = nullptr; m_label = nullptr; m_has_key = false; m_has_index = false; m_visible = true; m_layout_done = false; } static auto IsOurLabel(UObject* Candidate, UObject* Owner) -> bool { if (!Candidate || !Owner) return false; if (Candidate->GetOuterPrivate() != Owner) return false; return Api::ObjectIsA(Candidate); } static auto RegistryKey(UObject* Owner) -> Static::WidgetRegistry::KeyType { static_assert(std::is_same_v, "RegistryKey builds its suffix with std::to_wstring"); return Static::WidgetRegistry::KeyType{ STR("PalBoxNumberLabel:") } + std::to_wstring(reinterpret_cast(Owner)); } static auto WidgetApi() -> Reflection::UnrealApiCall& { static Reflection::UnrealApiCall Api{ Api::Mod::PalDetail_PalNumberWidget::ClassPath(), Api::Mod::PalDetail_PalNumberWidget::DebugTag() }; return Api; } static constexpr int32 kUnknownIndex = -1; }; }