feat(ui): port retail selected-stack quantity

Bind the authored stack count entry and horizontal slider to one Core split-quantity owner, preserve retail count-first naming and exact 1000-step rounding, refresh on stack changes, and consume the selected amount during merges. Conformance covers the production DAT fixture and retained pointer/focus paths.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 10:07:25 +02:00
parent dc1649c493
commit a20e5c68c7
19 changed files with 595 additions and 51 deletions

View file

@ -0,0 +1,62 @@
using System;
using System.Globalization;
namespace AcDream.Core.Items;
/// <summary>
/// Shared owner for retail <c>GenItemHolder::splitSize</c> and
/// <c>GenItemHolder::maxSplitSize</c>. The toolbar entry/slider edits this state and
/// item-holder operations consume the same value.
/// </summary>
public sealed class StackSplitQuantityState
{
public uint Value { get; private set; } = 1u;
public uint Maximum { get; private set; } = 1u;
public float Ratio => Value / (float)Maximum;
public event Action? Changed;
public void Reset(uint stackSize, uint? initialValue = null)
{
uint maximum = Math.Max(stackSize, 1u);
Set(initialValue ?? maximum, maximum);
}
public void SetFromText(string? text)
{
// Retail uses wcstoul, whose invalid/empty result is zero, then clamps to one.
uint parsed = uint.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out uint value)
? value
: 0u;
SetValue(parsed);
}
/// <summary>
/// Ports <c>gmToolbarUI::ListenToElementMessage @ 0x004BEFE0</c> and
/// <c>UIElement_Scrollbar::SetScrollbarPosition @ 0x00470EC0</c>. The scrollbar
/// first truncates its normalized position to an integer in [0,1000], then the
/// toolbar converts it to <c>1 + floor(position * max / 1000)</c> and clamps.
/// </summary>
public void SetFromSliderRatio(float ratio)
{
float clamped = Math.Clamp(ratio, 0f, 1f);
uint positionMillis = (uint)MathF.Truncate(clamped * 1000f);
ulong scaled = (ulong)positionMillis * Maximum;
uint value = 1u + (uint)(scaled / 1000u);
SetValue(value);
}
public void SetValue(uint value) => Set(value, Maximum);
private void Set(uint value, uint maximum)
{
maximum = Math.Max(maximum, 1u);
value = Math.Clamp(value, 1u, maximum);
if (Value == value && Maximum == maximum)
return;
Value = value;
Maximum = maximum;
Changed?.Invoke();
}
}