using System;
using System.Linq;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Spells;
namespace AcDream.App.UI.Layout;
///
/// Retained port of retail gmIndicatorsUI LayoutDesc
/// 0x21000071 and its custom indicator buttons. The DAT owns geometry,
/// sprites, and state names; this controller owns only live state selection and
/// authored input actions.
///
public sealed class IndicatorBarController : IRetainedPanelController
{
public const uint LayoutId = 0x21000071u;
public const uint BurdenClassId = 0x10000001u;
public const uint EffectsClassId = 0x10000002u;
public const uint LinkClassId = 0x10000003u;
public const uint MiniGameClassId = 0x10000004u;
public const uint VitaeClassId = 0x10000006u;
public const uint MiniGameButtonId = 0x100000F3u;
public const uint VitaeButtonId = 0x100000F4u;
public const uint HelpfulButtonId = 0x100000F5u;
public const uint HarmfulButtonId = 0x100000F6u;
public const uint BurdenButtonId = 0x100000F7u;
public const uint LinkButtonId = 0x100000F8u;
public const uint EndCharacterSessionButtonId = 0x100000FAu;
public const uint UnencumberedState = 14u;
public const uint EncumberedState = 15u;
public const uint HeavilyEncumberedState = 16u;
public const uint ConnectionGoodState = 17u;
public const uint ConnectionUncertainState = 18u;
public const uint ConnectionBadState = 19u;
public const uint ConnectionDisconnectedState = 20u;
private const uint EncumbranceValProperty = 5u;
private const uint EncumbranceAugProperty = 0xE6u;
private const double LinkUpdateSeconds = 4.0;
private const double LinkFlashSeconds = 0.75;
private readonly IndicatorBarBindings _bindings;
private readonly UiButton _link;
private readonly UiButton _helpful;
private readonly UiButton _harmful;
private readonly UiButton _vitae;
private readonly UiButton _burden;
private readonly UiButton _miniGame;
private readonly UiButton _endCharacterSession;
private LinkSemanticState _linkState = LinkSemanticState.Good;
private double _lastLinkUpdate;
private double _lastLinkFlash;
private bool _disposed;
private enum LinkSemanticState
{
Good = 1,
Uncertain = 2,
Bad = 3,
Disconnected = 4,
}
private IndicatorBarController(
IndicatorBarBindings bindings,
UiButton link,
UiButton helpful,
UiButton harmful,
UiButton vitae,
UiButton burden,
UiButton miniGame,
UiButton endCharacterSession)
{
_bindings = bindings;
_link = link;
_helpful = helpful;
_harmful = harmful;
_vitae = vitae;
_burden = burden;
_miniGame = miniGame;
_endCharacterSession = endCharacterSession;
_helpful.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.PositiveEffects);
_harmful.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.NegativeEffects);
_link.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.LinkStatus);
_vitae.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.Vitae);
_burden.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.CharacterInformation);
_miniGame.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.MiniGame);
_endCharacterSession.OnClick = bindings.RequestEndCharacterSession;
bindings.Spellbook.EnchantmentsChanged += UpdateEnchantments;
bindings.Objects.ObjectAdded += OnObjectChanged;
bindings.Objects.ObjectUpdated += OnObjectChanged;
bindings.Objects.ObjectRemoved += OnObjectChanged;
bindings.Objects.ObjectMoved += OnObjectMoved;
bindings.Objects.ContainerContentsReplaced += OnContainerContentsReplaced;
bindings.Objects.Cleared += OnObjectsCleared;
// gmUIElement_LinkStatusIndicator::PostInit @ 0x004E66D0 starts
// Good and does not query until the four-second time notice cadence.
double now = bindings.CurrentTime();
_lastLinkUpdate = now;
_lastLinkFlash = now;
_link.TrySetRetailState(ConnectionGoodState);
_miniGame.TrySetRetailState(UiButtonStateMachine.Ghosted);
// Ordinary button 0x100000FA has no hover state: retail begins in its
// authored Normal state (0x06007BB1) before any pointer notice.
_endCharacterSession.TrySetRetailState(UiButtonStateMachine.Normal);
UpdateEnchantments();
UpdateBurden();
}
public static IndicatorBarController? Bind(
ImportedLayout layout,
IndicatorBarBindings bindings)
{
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(bindings);
if (layout.FindElement(LinkButtonId) is not UiButton link
|| layout.FindElement(HelpfulButtonId) is not UiButton helpful
|| layout.FindElement(HarmfulButtonId) is not UiButton harmful
|| layout.FindElement(VitaeButtonId) is not UiButton vitae
|| layout.FindElement(BurdenButtonId) is not UiButton burden
|| layout.FindElement(MiniGameButtonId) is not UiButton miniGame
|| layout.FindElement(EndCharacterSessionButtonId) is not UiButton endSession)
return null;
return new IndicatorBarController(
bindings, link, helpful, harmful, vitae, burden, miniGame, endSession);
}
///
/// Port of gmUIElement_LinkStatusIndicator::UseTime @ 0x004E67C0.
/// The absolute monotonic clock is injected so the retail 4.0/0.75-second
/// boundaries are deterministic in tests.
///
public void Tick()
{
double now = _bindings.CurrentTime();
if (now - _lastLinkUpdate >= LinkUpdateSeconds)
{
UpdateLinkState(now, _bindings.LinkStatus());
_lastLinkUpdate = now;
}
if (_linkState == LinkSemanticState.Bad
&& now - _lastLinkFlash >= LinkFlashSeconds)
{
_link.TrySetRetailState(
_link.ActiveRetailStateId == ConnectionUncertainState
? ConnectionBadState
: ConnectionUncertainState);
_lastLinkFlash = now;
}
}
/// Retail BeginGame/EndGame notice surface for the future mini-game owner.
public void SetMiniGameActive(bool active)
=> _miniGame.TrySetRetailState(
active ? UiButtonStateMachine.Normal : UiButtonStateMachine.Ghosted);
private void UpdateLinkState(double now, LinkStatusSnapshot snapshot)
{
// gmUIElement_LinkStatusIndicator::UpdateLinkState @ 0x004E6730.
LinkSemanticState state = !snapshot.Connected || snapshot.SecondsSinceLastPacket >= 40d
? LinkSemanticState.Disconnected
: snapshot.SecondsSinceLastPacket >= 20d
? LinkSemanticState.Bad
: snapshot.SecondsSinceLastPacket >= 5d
? LinkSemanticState.Uncertain
: LinkSemanticState.Good;
SetLinkState(state, now);
}
private void SetLinkState(LinkSemanticState state, double now)
{
// gmUIElement_LinkStatusIndicator::SetLinkState @ 0x004E65E0 does
// nothing for an unchanged semantic state (including an active flash).
if (_linkState == state) return;
_linkState = state;
uint visual = state switch
{
LinkSemanticState.Good => ConnectionGoodState,
LinkSemanticState.Uncertain => ConnectionUncertainState,
LinkSemanticState.Bad => ConnectionBadState,
LinkSemanticState.Disconnected => ConnectionDisconnectedState,
_ => throw new InvalidOperationException($"Unknown link state {state}."),
};
_link.TrySetRetailState(visual);
if (state == LinkSemanticState.Bad)
_lastLinkFlash = now;
}
private void UpdateEnchantments()
{
_helpful.TrySetRetailState(HasMatchingEffect(beneficial: true)
? UiButtonStateMachine.Normal
: UiButtonStateMachine.Ghosted);
_harmful.TrySetRetailState(HasMatchingEffect(beneficial: false)
? UiButtonStateMachine.Normal
: UiButtonStateMachine.Ghosted);
// gmUIElement_VitaeIndicator::Update @ 0x004E5FE0 enables the
// indicator only when the singleton Vitae modifier is below 1.0.
bool hasVitae = _bindings.Spellbook.ActiveEnchantmentSnapshot.Any(record =>
record.Bucket == 4u
&& record.StatModValue is float modifier
&& modifier < 1f);
_vitae.TrySetRetailState(hasVitae
? UiButtonStateMachine.Normal
: UiButtonStateMachine.Ghosted);
}
private bool HasMatchingEffect(bool beneficial)
// Retail SpellEffectMatchesUIType @ 0x004B76C0 supplies this split.
=> _bindings.Spellbook.ActiveEnchantmentSnapshot.Any(record =>
// CountSpellsInList @ 0x00593CD0 scans raw mult/add nodes; it
// does not use the effects-list spell-category duel projection.
record.Bucket is 1u or 2u
&& _bindings.Spellbook.TryGetMetadata(
record.SpellId, out SpellMetadata metadata)
&& metadata.IsBeneficial == beneficial);
private void OnObjectChanged(ClientObject _) => UpdateBurden();
private void OnObjectMoved(ClientObjectMove _) => UpdateBurden();
private void OnContainerContentsReplaced(uint _) => UpdateBurden();
private void OnObjectsCleared() => UpdateBurden();
private void UpdateBurden()
{
uint player = _bindings.PlayerGuid();
ClientObject? playerObject = _bindings.Objects.Get(player);
int strength = _bindings.Strength() ?? 10;
int augmentation = playerObject?.Properties.GetInt(EncumbranceAugProperty) ?? 0;
int capacity = BurdenMath.EncumbranceCapacity(strength, augmentation);
int burden = playerObject?.Properties.Ints.TryGetValue(
EncumbranceValProperty, out int wireBurden) == true
? wireBurden
: _bindings.Objects.SumCarriedBurden(player);
float load = BurdenMath.LoadRatio(capacity, burden);
// gmUIElement_BurdenIndicator::Update @ 0x004E6CE0.
_burden.TrySetRetailState(load < 1f
? UnencumberedState
: load < 2f ? EncumberedState : HeavilyEncumberedState);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_bindings.Spellbook.EnchantmentsChanged -= UpdateEnchantments;
_bindings.Objects.ObjectAdded -= OnObjectChanged;
_bindings.Objects.ObjectUpdated -= OnObjectChanged;
_bindings.Objects.ObjectRemoved -= OnObjectChanged;
_bindings.Objects.ObjectMoved -= OnObjectMoved;
_bindings.Objects.ContainerContentsReplaced -= OnContainerContentsReplaced;
_bindings.Objects.Cleared -= OnObjectsCleared;
_link.OnClick = null;
_helpful.OnClick = null;
_harmful.OnClick = null;
_vitae.OnClick = null;
_burden.OnClick = null;
_miniGame.OnClick = null;
_endCharacterSession.OnClick = null;
}
}
public sealed record IndicatorBarBindings(
Spellbook Spellbook,
ClientObjectTable Objects,
Func PlayerGuid,
Func Strength,
Func LinkStatus,
Func CurrentTime,
Action TogglePanel,
Action RequestEndCharacterSession);