feat(ui): port retail selected-item mana
Parse and route the complete 0x0264 guid/fraction/valid response, send exact 0x0263 item-mana queries, and reproduce retail meter visibility plus guid-zero cancellation for mana and health selection changes. Keep the behavior in Core state and the retained selected-object controller with wire/state/UI conformance coverage. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
df460f94c3
commit
fa1da02783
21 changed files with 402 additions and 38 deletions
|
|
@ -7,15 +7,17 @@ using AcDream.Core.Selection;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A1).
|
||||
/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4).
|
||||
/// Analogue of retail <c>gmToolbarUI::HandleSelectionChanged</c>
|
||||
/// (<c>docs/research/named-retail/acclient_2013_pseudo_c.txt:198635</c>) +
|
||||
/// <c>RecvNotice_UpdateObjectHealth</c> (<c>:196213</c>).
|
||||
/// <c>RecvNotice_UpdateObjectHealth</c> (<c>:196213</c>) +
|
||||
/// <c>RecvNotice_UpdateItemMana</c> (<c>:196188</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// On selection change: clears the strip (name, overlay flash, health meter), then if a
|
||||
/// guid is provided it sets the name, flashes the selection overlay briefly, and (for
|
||||
/// health-bearing targets) sends a <c>QueryHealth (0x01BF)</c> request. The Health meter
|
||||
/// guid is provided it sets the name, flashes the selection overlay briefly, and sends
|
||||
/// either <c>QueryHealth (0x01BF)</c> for health-bearing targets or
|
||||
/// <c>QueryItemMana (0x0263)</c> for owned non-stack items. The Health meter
|
||||
/// becomes visible only when the server actually reports health for the selected guid —
|
||||
/// either an <c>UpdateHealth (0x01C0)</c> arrives (retail
|
||||
/// <c>RecvNotice_UpdateObjectHealth</c> → <c>SetVisible(1)</c>) or the value is already
|
||||
|
|
@ -54,6 +56,8 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
public const uint OverlayId = 0x100001A0;
|
||||
/// <summary>Selected-object health meter element id (retail m_pSelObjectHealthMeter).</summary>
|
||||
public const uint HealthMeterId = 0x100001A1;
|
||||
/// <summary>Selected-object item-mana meter element id (retail m_pSelObjectManaMeter).</summary>
|
||||
public const uint ManaMeterId = 0x100001A2;
|
||||
/// <summary>Editable stack quantity (retail m_pStackSizeEntryBox).</summary>
|
||||
public const uint StackSizeEntryId = 0x100001A3;
|
||||
/// <summary>Horizontal stack quantity slider (retail m_pStackSizeSlider).</summary>
|
||||
|
|
@ -80,19 +84,24 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
private readonly UiElement? _name;
|
||||
private readonly UiDatElement? _overlay;
|
||||
private readonly UiMeter? _healthMeter;
|
||||
private readonly UiMeter? _manaMeter;
|
||||
private readonly UiField? _stackSizeEntry;
|
||||
private readonly UiScrollbar? _stackSizeSlider;
|
||||
|
||||
// ── Captured delegates ───────────────────────────────────────────────────
|
||||
private readonly Func<uint, bool> _isHealthTarget;
|
||||
private readonly Func<uint, bool> _isOwnedByPlayer;
|
||||
private readonly Func<uint, string?> _resolveName;
|
||||
private readonly Func<uint, float> _healthPercent;
|
||||
private readonly Func<uint, bool> _hasHealth;
|
||||
private readonly Func<uint, uint> _stackSize;
|
||||
private readonly Action<uint> _sendQueryHealth;
|
||||
private readonly Func<uint, float> _manaPercent;
|
||||
private readonly Action<uint> _sendQueryItemMana;
|
||||
private readonly StackSplitQuantityState _splitQuantity;
|
||||
private readonly SelectionState _selection;
|
||||
private readonly Action<Action<uint, float>> _unsubscribeHealthChanged;
|
||||
private readonly Action<Action<uint, float, bool>> _unsubscribeItemManaChanged;
|
||||
private readonly Action<Action<ClientObject>> _unsubscribeObjectUpdated;
|
||||
|
||||
// ── Live state (read by closures on the per-frame draw path) ────────────
|
||||
|
|
@ -110,32 +119,42 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
SelectionState selection,
|
||||
Action<Action<uint, float>> subscribeHealthChanged,
|
||||
Action<Action<uint, float>> unsubscribeHealthChanged,
|
||||
Action<Action<uint, float, bool>> subscribeItemManaChanged,
|
||||
Action<Action<uint, float, bool>> unsubscribeItemManaChanged,
|
||||
Func<uint, bool> isHealthTarget,
|
||||
Func<uint, bool> isOwnedByPlayer,
|
||||
Func<uint, string?> name,
|
||||
Func<uint, float> healthPercent,
|
||||
Func<uint, bool> hasHealth,
|
||||
Func<uint, uint> stackSize,
|
||||
Action<uint> sendQueryHealth,
|
||||
Func<uint, float> manaPercent,
|
||||
Action<uint> sendQueryItemMana,
|
||||
UiDatFont? datFont,
|
||||
StackSplitQuantityState splitQuantity,
|
||||
Action<Action<ClientObject>> subscribeObjectUpdated,
|
||||
Action<Action<ClientObject>> unsubscribeObjectUpdated)
|
||||
{
|
||||
_isHealthTarget = isHealthTarget;
|
||||
_isOwnedByPlayer = isOwnedByPlayer;
|
||||
_resolveName = name;
|
||||
_healthPercent = healthPercent;
|
||||
_hasHealth = hasHealth;
|
||||
_stackSize = stackSize;
|
||||
_sendQueryHealth = sendQueryHealth;
|
||||
_manaPercent = manaPercent;
|
||||
_sendQueryItemMana = sendQueryItemMana;
|
||||
_splitQuantity = splitQuantity ?? throw new ArgumentNullException(nameof(splitQuantity));
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_unsubscribeHealthChanged = unsubscribeHealthChanged;
|
||||
_unsubscribeItemManaChanged = unsubscribeItemManaChanged;
|
||||
_unsubscribeObjectUpdated = unsubscribeObjectUpdated;
|
||||
|
||||
// Find elements — silently skip absent ones (partial/test layouts).
|
||||
_name = layout.FindElement(NameId);
|
||||
_overlay = layout.FindElement(OverlayId) as UiDatElement;
|
||||
_healthMeter = layout.FindElement(HealthMeterId) as UiMeter;
|
||||
_manaMeter = layout.FindElement(ManaMeterId) as UiMeter;
|
||||
_stackSizeEntry = layout.FindElement(StackSizeEntryId) as UiField;
|
||||
_stackSizeSlider = layout.FindElement(StackSizeSliderId) as UiScrollbar;
|
||||
|
||||
|
|
@ -151,6 +170,11 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
// Fill polls live: _current holds the currently-selected guid (or null).
|
||||
_healthMeter.Fill = () => _current is uint g ? _healthPercent(g) : (float?)0f;
|
||||
}
|
||||
if (_manaMeter is not null)
|
||||
{
|
||||
_manaMeter.Visible = false;
|
||||
_manaMeter.Fill = () => _current is uint g ? _manaPercent(g) : (float?)0f;
|
||||
}
|
||||
|
||||
if (_stackSizeEntry is not null)
|
||||
{
|
||||
|
|
@ -210,6 +234,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
_selection.Changed += OnSelectionTransition;
|
||||
_splitQuantity.Changed += OnSplitQuantityChanged;
|
||||
subscribeHealthChanged(OnHealthChanged);
|
||||
subscribeItemManaChanged(OnItemManaChanged);
|
||||
subscribeObjectUpdated(OnObjectUpdated);
|
||||
if (_selection.SelectedObjectId is { } initial)
|
||||
ApplySelection(initial);
|
||||
|
|
@ -237,12 +262,17 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
SelectionState selection,
|
||||
Action<Action<uint, float>> subscribeHealthChanged,
|
||||
Action<Action<uint, float>> unsubscribeHealthChanged,
|
||||
Action<Action<uint, float, bool>> subscribeItemManaChanged,
|
||||
Action<Action<uint, float, bool>> unsubscribeItemManaChanged,
|
||||
Func<uint, bool> isHealthTarget,
|
||||
Func<uint, bool> isOwnedByPlayer,
|
||||
Func<uint, string?> name,
|
||||
Func<uint, float> healthPercent,
|
||||
Func<uint, bool> hasHealth,
|
||||
Func<uint, uint> stackSize,
|
||||
Action<uint> sendQueryHealth,
|
||||
Func<uint, float> manaPercent,
|
||||
Action<uint> sendQueryItemMana,
|
||||
UiDatFont? datFont,
|
||||
StackSplitQuantityState splitQuantity,
|
||||
Action<Action<ClientObject>> subscribeObjectUpdated,
|
||||
|
|
@ -250,7 +280,9 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
=> new SelectedObjectController(
|
||||
layout, selection,
|
||||
subscribeHealthChanged, unsubscribeHealthChanged,
|
||||
isHealthTarget, name, healthPercent, hasHealth, stackSize, sendQueryHealth, datFont,
|
||||
subscribeItemManaChanged, unsubscribeItemManaChanged,
|
||||
isHealthTarget, isOwnedByPlayer, name, healthPercent, hasHealth, stackSize,
|
||||
sendQueryHealth, manaPercent, sendQueryItemMana, datFont,
|
||||
splitQuantity, subscribeObjectUpdated, unsubscribeObjectUpdated);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -259,9 +291,25 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
/// </summary>
|
||||
private void ApplySelection(uint? guid)
|
||||
{
|
||||
bool selectionChanged = _current != guid;
|
||||
|
||||
// gmToolbarUI::HandleSelectionChanged @ 0x004BF3D1: changing away from
|
||||
// a visible meter cancels its server query with object id zero.
|
||||
if (selectionChanged)
|
||||
{
|
||||
if (_healthMeter?.Visible == true)
|
||||
_sendQueryHealth(0);
|
||||
if (_manaMeter?.Visible == true)
|
||||
_sendQueryItemMana(0);
|
||||
}
|
||||
|
||||
// ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0)
|
||||
// + SetVisible(0) on the meters). ──────────────────────────────────────
|
||||
if (_healthMeter is not null) _healthMeter.Visible = false;
|
||||
if (selectionChanged)
|
||||
{
|
||||
if (_healthMeter is not null) _healthMeter.Visible = false;
|
||||
if (_manaMeter is not null) _manaMeter.Visible = false;
|
||||
}
|
||||
if (_stackSizeEntry is not null) _stackSizeEntry.Visible = false;
|
||||
if (_stackSizeSlider is not null) _stackSizeSlider.Visible = false;
|
||||
_splitQuantity.Reset(1u);
|
||||
|
|
@ -306,12 +354,18 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
// ── 4. Health: query, and show the meter only if real health is already known.
|
||||
// Otherwise the meter appears when OnHealthChanged fires for this guid
|
||||
// (retail RecvNotice_UpdateObjectHealth :196213). ──────────────────────────
|
||||
if (_isHealthTarget(g))
|
||||
if (stackSize <= 1u && _isHealthTarget(g))
|
||||
{
|
||||
_sendQueryHealth(g);
|
||||
if (selectionChanged)
|
||||
_sendQueryHealth(g);
|
||||
if (_hasHealth(g) && _healthMeter is not null)
|
||||
_healthMeter.Visible = true;
|
||||
}
|
||||
else if (stackSize <= 1u && _isOwnedByPlayer(g))
|
||||
{
|
||||
if (selectionChanged)
|
||||
_sendQueryItemMana(g);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -339,6 +393,25 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
_overlay?.TrySetRetailState(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port of <c>gmToolbarUI::RecvNotice_UpdateItemMana @ 0x004BD0C0</c>.
|
||||
/// Invalid results cancel with object id zero; valid results reveal the meter.
|
||||
/// </summary>
|
||||
public void OnItemManaChanged(uint guid, float percent, bool valid)
|
||||
{
|
||||
if (_current != guid)
|
||||
return;
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
_sendQueryItemMana(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_manaMeter is not null)
|
||||
_manaMeter.Visible = true;
|
||||
}
|
||||
|
||||
private void CommitStackEntry(string text)
|
||||
=> _splitQuantity.SetFromText(text);
|
||||
|
||||
|
|
@ -380,6 +453,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
_selection.Changed -= OnSelectionTransition;
|
||||
_splitQuantity.Changed -= OnSplitQuantityChanged;
|
||||
_unsubscribeHealthChanged(OnHealthChanged);
|
||||
_unsubscribeItemManaChanged(OnItemManaChanged);
|
||||
_unsubscribeObjectUpdated(OnObjectUpdated);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,17 +35,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
0x100006BC, 0x100006BD, 0x100006BE, 0x100006BF,
|
||||
};
|
||||
|
||||
// Elements hidden by default in retail gmToolbarUI::PostInit.
|
||||
// Ids confirmed from the toolbar LayoutDesc dump.
|
||||
// 0x100001A1 (health meter) is now OWNED by SelectedObjectController (D.5.3a) —
|
||||
// it hides A1 at bind and shows it on a health-target selection, so A1 is removed
|
||||
// from here to avoid double-ownership. 0x100001A2 (mana meter), 0x100001A3 (stack-size
|
||||
// entry box) and 0x100001A4 (stack slider) are DEFERRED features (mana #140, stack-split
|
||||
// UI) with no controller yet, so they stay hidden here — otherwise their dat sprites
|
||||
// render as stray bars / a black box on the toolbar. Retail hides A3/A4 in
|
||||
// gmToolbarUI::HandleSelectionChanged (acclient_2013_pseudo_c.txt:198660/198742),
|
||||
// showing them only for a stacked-item selection.
|
||||
private static readonly uint[] HiddenIds = { 0x100001A2 };
|
||||
// SelectedObjectController owns the health/mana meters and both stack controls,
|
||||
// including retail's initial-hidden state and selection-driven visibility.
|
||||
|
||||
// Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time.
|
||||
// Index 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
|
||||
|
|
@ -161,10 +152,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
_inventoryButton.OnItemDrop = HandleInventoryButtonDrop;
|
||||
}
|
||||
|
||||
// Hide target-object meters + stack slider (gmToolbarUI::PostInit).
|
||||
foreach (var id in HiddenIds)
|
||||
if (layout.FindElement(id) is { } e) e.Visible = false;
|
||||
|
||||
// Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669):
|
||||
// exactly one indicator visible at a time. Default to NonCombat (peace) — the player
|
||||
// always spawns in peace mode; retail has not yet called SetVisible when PostInit runs.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public sealed record ToolbarRuntimeBindings(
|
|||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Action<uint> UseItem,
|
||||
CombatState Combat,
|
||||
ItemManaState ItemMana,
|
||||
Action ToggleCombat,
|
||||
ItemInteractionController ItemInteraction,
|
||||
Action<ShortcutEntry>? SendAddShortcut,
|
||||
|
|
@ -54,6 +55,7 @@ public sealed record ToolbarRuntimeBindings(
|
|||
Func<uint, bool> HasHealth,
|
||||
Func<uint, uint> StackSize,
|
||||
Action<uint> SendQueryHealth,
|
||||
Action<uint> SendQueryItemMana,
|
||||
Func<uint> PlayerGuid,
|
||||
Action<uint, uint, int>? SendPutItemInContainer);
|
||||
|
||||
|
|
@ -380,12 +382,17 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
b.Selection,
|
||||
b.SubscribeHealthChanged,
|
||||
b.UnsubscribeHealthChanged,
|
||||
handler => b.ItemMana.ItemManaChanged += handler,
|
||||
handler => b.ItemMana.ItemManaChanged -= handler,
|
||||
b.IsHealthTarget,
|
||||
b.ItemInteraction.IsOwnedByPlayer,
|
||||
b.ResolveName,
|
||||
b.HealthPercent,
|
||||
b.HasHealth,
|
||||
b.StackSize,
|
||||
b.SendQueryHealth,
|
||||
b.ItemMana.GetManaPercent,
|
||||
b.SendQueryItemMana,
|
||||
_bindings.Assets.DefaultFont,
|
||||
StackSplitQuantity,
|
||||
handler => b.Objects.ObjectUpdated += handler,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue