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