332 lines
11 KiB
C++
332 lines
11 KiB
C++
|
|
#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;
|
||
|
|
};
|
||
|
|
}
|