initial commit; cv 0.0.3

This commit is contained in:
2026-09-07 19:33:58 -04:00
commit abf065ddde
63 changed files with 8816 additions and 0 deletions

View 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;
};
}