initial commit; cv 0.0.3
This commit is contained in:
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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user