acdream/tests/AcDream.Core.Net.Tests/ObjectTableWiringTests.cs
Erik 6005c51c4d 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>
2026-07-11 10:22:11 +02:00

190 lines
8.1 KiB
C#

using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.Core.Net.Tests;
/// <summary>
/// D.5.4 Task 7 — ObjectTableWiring.
///
/// WorldSession.EntitySpawned/EntityDeleted have no in-process test seam (the
/// ctor opens a UDP socket), so the subscription wiring is tested via the guard
/// logic extracted as a local handler — this is the same lambda body that
/// ObjectTableWiring.Wire wires to session.EntityDeleted. The retail two-table
/// semantics are: PickupEvent (0xF74A) removes from the 3-D object_table but
/// KEEPS the weenie in ClientObjectTable; DeleteObject (0xF747) evicts the
/// weenie from both. FromPickup = true guards the eviction.
/// </summary>
public sealed class ObjectTableWiringTests
{
[Fact]
public void ToWeenieData_CopiesFieldsFromSpawn()
{
// Every EntitySpawn item field is set to a DISTINCT recognisable value so
// a positional transposition in ObjectTableWiring.ToWeenieData would trip
// at least one Assert. Every mapped WeenieData field is verified below.
var spawn = new WorldSession.EntitySpawn(
Guid: 0x00000600u,
Position: null,
SetupTableId: null,
AnimPartChanges: System.Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: System.Array.Empty<CreateObject.TextureChange>(),
SubPalettes: System.Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: null,
Name: "Iron Sword",
ItemType: (uint)ItemType.MeleeWeapon,
MotionState: null,
MotionTableId: null)
with
{
WeenieClassId = 0x00001001u,
IconId = 0x06001111u,
IconOverlayId = 0x06002222u,
IconUnderlayId = 0x06003333u,
UiEffects = 0x00000004u,
Value = 7,
StackSize = 1,
StackSizeMax = 1,
Burden = 300,
ContainerId = 0x000000C9u,
WielderId = 0x000000DAu,
ValidLocations = 0x00000012u, // MeleeWeapon wield mask
CurrentWieldedLocation = 0x00000002u, // right-hand
Priority = 0x00000005u,
ItemsCapacity = 0,
ContainersCapacity = 0,
Structure = 80,
MaxStructure = 100,
Workmanship = 4.5f,
Useability = 0x000A0008u,
TargetType = (uint)ItemType.Creature,
RadarBlipColor = 4,
RadarBehavior = 3,
ObjectDescriptionFlags = 0x20800200u,
CombatUse = 2,
PluralName = "Iron Swords",
};
var d = ObjectTableWiring.ToWeenieData(spawn);
// --- identity ---
Assert.Equal(0x00000600u, d.Guid);
Assert.Equal("Iron Sword", d.Name);
Assert.Equal(ItemType.MeleeWeapon, d.Type);
// --- weenie / icon ---
Assert.Equal(0x00001001u, d.WeenieClassId);
Assert.Equal(0x06001111u, d.IconId);
Assert.Equal(0x06002222u, d.IconOverlayId);
Assert.Equal(0x06003333u, d.IconUnderlayId);
Assert.Equal(0x00000004u, d.Effects);
// --- quantity / economy ---
Assert.Equal(7, d.Value);
Assert.Equal(1, d.StackSize);
Assert.Equal(1, d.StackSizeMax);
Assert.Equal(300, d.Burden);
// --- container / wielder ---
Assert.Equal(0x000000C9u, d.ContainerId);
Assert.Equal(0x000000DAu, d.WielderId);
// --- equip masks ---
Assert.Equal(0x00000012u, d.ValidLocations);
Assert.Equal(0x00000002u, d.CurrentWieldedLocation);
Assert.Equal(0x00000005u, d.Priority);
// --- capacity ---
Assert.Equal(0, d.ItemsCapacity);
Assert.Equal(0, d.ContainersCapacity);
// --- durability ---
Assert.Equal(80, d.Structure);
Assert.Equal(100, d.MaxStructure);
Assert.Equal(4.5f, d.Workmanship);
Assert.Equal(0x000A0008u, d.Useability);
Assert.Equal((uint)ItemType.Creature, d.TargetType);
Assert.Equal((byte)4, d.RadarBlipColor);
Assert.Equal((byte)3, d.RadarBehavior);
Assert.Equal(0x20800200u, d.PublicWeenieBitfield);
Assert.Equal((byte)2, d.CombatUse);
Assert.Equal("Iron Swords", d.PluralName);
}
// -------------------------------------------------------------------------
// The handler that ObjectTableWiring.Wire subscribes to session.EntityDeleted.
// Testing it directly avoids needing a live WorldSession (UDP socket).
// -------------------------------------------------------------------------
private static Action<DeleteObject.Parsed> MakeEntityDeletedHandler(ClientObjectTable table)
=> d => { if (!d.FromPickup) table.Remove(d.Guid); };
private static WeenieData MinimalWeenie(uint guid) => new(
Guid: guid,
Name: "Test Item",
Type: null,
WeenieClassId: 1u,
IconId: 0x06000001u,
IconOverlayId: 0u,
IconUnderlayId: 0u,
Effects: 0u,
Value: null,
StackSize: null,
StackSizeMax: null,
Burden: null,
ContainerId: null,
WielderId: null,
ValidLocations: null,
CurrentWieldedLocation: null,
Priority: null,
ItemsCapacity: null,
ContainersCapacity: null,
Structure: null,
MaxStructure: null,
Workmanship: null);
/// <summary>
/// PickupEvent path: FromPickup = true → weenie is RETAINED in ClientObjectTable.
/// The 3-D render entity is removed (EntityDeleted still fires), but the weenie
/// data persists so the follow-up InventoryPutObjInContainer can find it.
/// </summary>
[Fact]
public void EntityDeleted_FromPickupTrue_RetainsWeenieInTable()
{
var table = new ClientObjectTable();
var handler = MakeEntityDeletedHandler(table);
const uint guid = 0x80000100u;
table.Ingest(MinimalWeenie(guid));
Assert.NotNull(table.Get(guid)); // pre-condition: item is in the table
// Fire EntityDeleted as WorldSession does for PickupEvent (0xF74A).
// The handler itself represents the EntityDeleted subscription — firing it IS the event.
handler(new DeleteObject.Parsed(guid, 0, FromPickup: true));
// Post-condition: weenie still in table (it moved into a container, was NOT destroyed).
// EntityDeleted still fires (the handler ran), so the 3-D render layer would remove the WorldEntity.
Assert.NotNull(table.Get(guid));
}
/// <summary>
/// DeleteObject path: FromPickup = false → weenie IS evicted from ClientObjectTable.
/// Regression guard — true destroys (0xF747) must still clean up the table.
/// </summary>
[Fact]
public void EntityDeleted_FromPickupFalse_EvictsWeenieFromTable()
{
var table = new ClientObjectTable();
var handler = MakeEntityDeletedHandler(table);
const uint guid = 0x80000200u;
table.Ingest(MinimalWeenie(guid));
Assert.NotNull(table.Get(guid)); // pre-condition: item is in the table
// Fire EntityDeleted as WorldSession does for DeleteObject (0xF747).
handler(new DeleteObject.Parsed(guid, 0, FromPickup: false));
// Post-condition: weenie evicted (truly destroyed).
Assert.Null(table.Get(guid));
}
}