fix(ui): finish retail stack selector polish

Keep the horizontal thumb at its raw pointer position so both endpoints are reachable, honor the stack entry's authored right alignment, and preserve PublicWeenieDesc plural names from CreateObject through the object table. Port retail's singular s/es fallback when no plural was sent.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 10:22:11 +02:00
parent a20e5c68c7
commit 6005c51c4d
22 changed files with 233 additions and 36 deletions

View file

@ -228,7 +228,8 @@ public class DatWidgetFactoryTests
{
ImportedLayout layout = FixtureLoader.LoadToolbar();
Assert.IsType<UiField>(layout.FindElement(0x100001A3u));
var entry = Assert.IsType<UiField>(layout.FindElement(0x100001A3u));
Assert.True(entry.RightAligned);
var bar = Assert.IsType<UiScrollbar>(layout.FindElement(0x100001A4u));
Assert.True(bar.Horizontal);
Assert.Equal(0x06004CF6u, bar.TrackSprite);

View file

@ -283,17 +283,32 @@ public class SelectedObjectControllerTests
Assert.Equal("17", entry.Text);
Assert.Equal(17u, h.SplitQuantity.Value);
Assert.Equal(17u, h.SplitQuantity.Maximum);
Assert.Equal(1f, slider.ScalarValue!());
Assert.Equal(1f, slider.ScalarPosition);
Assert.Equal("17 Healing Kits", nameEl.Children.OfType<UiText>().Single().LinesProvider().Single().Text);
slider.SetScalarPosition(0.5f);
slider.ScalarChanged!(0.5f);
Assert.Equal(9u, h.SplitQuantity.Value);
Assert.Equal("9", entry.Text);
Assert.Equal(0.5f, slider.ScalarPosition);
entry.SetText("999");
entry.Submit();
slider.SetScalarPosition(0f);
slider.ScalarChanged!(0f);
Assert.Equal(1u, h.SplitQuantity.Value);
Assert.Equal("1", entry.Text);
Assert.Equal(0f, slider.ScalarPosition);
slider.SetScalarPosition(1f);
slider.ScalarChanged!(1f);
Assert.Equal(17u, h.SplitQuantity.Value);
Assert.Equal("17", entry.Text);
Assert.Equal(1f, slider.ScalarPosition);
entry.SetText("4");
entry.Submit();
Assert.Equal(4u, h.SplitQuantity.Value);
Assert.Equal("4", entry.Text);
Assert.Equal(4f / 17f, slider.ScalarPosition, 5);
}
[Fact]

View file

@ -88,15 +88,17 @@ public class UiScrollbarTests
Width = 90f,
Height = 14f,
Horizontal = true,
ScalarValue = () => value,
ScalarChanged = next => value = next,
};
bar.SetScalarPosition(1f);
Assert.True(bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseDown, Data1: 8)));
Assert.Equal(0f, value, 3);
Assert.Equal(0f, bar.ScalarPosition, 3);
Assert.True(bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseMove, Data1: 45)));
Assert.Equal(0.5f, value, 3);
Assert.Equal(0.5f, bar.ScalarPosition, 3);
Assert.True(bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseUp, Data1: 45)));
}

View file

@ -549,6 +549,23 @@ public sealed class CreateObjectTests
Assert.Equal(0x500000F0u, parsed.Value.ContainerId);
}
[Fact]
public void WeenieHeader_pluralName_isPreserved()
{
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
guid: 0x50000023u,
name: "Pyreal Scarab",
itemType: (uint)ItemType.Misc,
weenieFlags: 0x00000001u | 0x00001000u,
stackSize: 2,
pluralName: "Pyreal Scarabs");
var parsed = CreateObject.TryParse(body);
Assert.NotNull(parsed);
Assert.Equal("Pyreal Scarabs", parsed.Value.PluralName);
}
private static byte[] BuildMinimalCreateObjectWithWeenieHeader(
uint guid,
string name,
@ -580,6 +597,7 @@ public sealed class CreateObjectTests
uint? currentWieldedLocation = null,
uint? priority = null,
float? workmanship = null,
string? pluralName = null,
byte? radarBlipColor = null,
byte? radarBehavior = null,
byte? combatUse = null,
@ -621,7 +639,7 @@ public sealed class CreateObjectTests
// WorldObject_Networking.cs:87-206. Each field is written only when
// its weenieFlags bit is set, matching the parser's walker exactly.
// Fields not parameterized above default to 0.
if ((weenieFlags & 0x00000001u) != 0) { /* PluralName — not parameterized */ }
if ((weenieFlags & 0x00000001u) != 0) WriteString16L(bytes, pluralName ?? "");
if ((weenieFlags & 0x00000002u) != 0) bytes.Add(itemsCapacity ?? 0); // ItemsCapacity u8
if ((weenieFlags & 0x00000004u) != 0) bytes.Add(containersCapacity ?? 0); // ContainersCapacity u8
if ((weenieFlags & 0x00000100u) != 0) WriteU16(bytes, 0); // AmmoType u16

View file

@ -22,7 +22,7 @@ public sealed class ObjectTableWiringTests
{
// Every EntitySpawn item field is set to a DISTINCT recognisable value so
// a positional transposition in ObjectTableWiring.ToWeenieData would trip
// at least one Assert. All 22 WeenieData fields are verified below.
// at least one Assert. Every mapped WeenieData field is verified below.
var spawn = new WorldSession.EntitySpawn(
Guid: 0x00000600u,
Position: null,
@ -63,6 +63,7 @@ public sealed class ObjectTableWiringTests
RadarBehavior = 3,
ObjectDescriptionFlags = 0x20800200u,
CombatUse = 2,
PluralName = "Iron Swords",
};
var d = ObjectTableWiring.ToWeenieData(spawn);
@ -108,6 +109,7 @@ public sealed class ObjectTableWiringTests
Assert.Equal((byte)3, d.RadarBehavior);
Assert.Equal(0x20800200u, d.PublicWeenieBitfield);
Assert.Equal((byte)2, d.CombatUse);
Assert.Equal("Iron Swords", d.PluralName);
}
// -------------------------------------------------------------------------

View file

@ -28,6 +28,17 @@ public sealed class WorldSessionRadarTests
Assert.Null(spawn.RadarBehavior);
}
[Fact]
public void ToEntitySpawn_PreservesPluralName()
{
var spawn = WorldSession.ToEntitySpawn(MinimalParsed() with
{
PluralName = "Pyreal Scarabs",
});
Assert.Equal("Pyreal Scarabs", spawn.PluralName);
}
private static CreateObject.Parsed MinimalParsed() => new(
Guid: 0x50000001u,
Position: null,

View file

@ -0,0 +1,45 @@
using AcDream.Core.Items;
using Xunit;
namespace AcDream.Core.Tests.Items;
public sealed class ClientObjectNameTests
{
[Fact]
public void AppropriateName_usesWirePluralForStack()
{
var item = new ClientObject
{
Name = "Pyreal Scarab",
PluralName = "Pyreal Scarabs",
StackSize = 2,
};
Assert.Equal("Pyreal Scarabs", item.GetAppropriateName());
}
[Theory]
[InlineData("Arrow", "Arrows")]
[InlineData("Pyreal", "Pyreals")]
[InlineData("Compass", "Compasses")]
public void AppropriateName_withoutWirePlural_usesRetailSuffixFallback(
string singular, string expected)
{
var item = new ClientObject { Name = singular, StackSize = 2 };
Assert.Equal(expected, item.GetAppropriateName());
}
[Fact]
public void AppropriateName_forSingleItem_remainsSingular()
{
var item = new ClientObject
{
Name = "Pyreal Scarab",
PluralName = "Pyreal Scarabs",
StackSize = 1,
};
Assert.Equal("Pyreal Scarab", item.GetAppropriateName());
}
}

View file

@ -277,6 +277,20 @@ public sealed class ClientObjectTableTests
Assert.Equal((uint)ItemType.Creature, item.TargetType);
}
[Fact]
public void Ingest_PluralName_PreservesWireValue()
{
var table = new ClientObjectTable();
table.Ingest(FullWeenie(0x500000B9u, name: "Pyreal Scarab", stack: 2) with
{
PluralName = "Pyreal Scarabs",
});
Assert.Equal("Pyreal Scarabs", table.Get(0x500000B9u)!.PluralName);
Assert.Equal("Pyreal Scarabs", table.Get(0x500000B9u)!.GetAppropriateName());
}
[Fact]
public void Ingest_RadarMetadata_PatchesOnlyPresentWireFields()
{