acdream/src/AcDream.Core/Items/StackSplitQuantityState.cs
Erik ea72c395c9 fix(ui): port retail partial stack transfers
Route the selected stack quantity through retail GetObjectSplitSize semantics, send exact split-to-container and split-to-ground actions, and keep the original object in place until the server publishes the newly guided stack. Cover selection scoping, wire bytes, container placement, and world drops with conformance tests.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 10:34:13 +02:00

74 lines
2.5 KiB
C#

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);
/// <summary>
/// Ports <c>ItemHolder::GetObjectSplitSize @ 0x00586F00</c>: only the
/// currently selected object consumes <c>GenItemHolder::splitSize</c>.
/// Every other object operation uses that object's full stack size.
/// </summary>
public uint GetObjectSplitSize(uint objectId, uint selectedObjectId, uint objectStackSize)
{
if (objectId == selectedObjectId)
return Value;
return Math.Max(objectStackSize, 1u);
}
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();
}
}