using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; namespace AcDream.Core.Net.Tests; /// /// 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. /// 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. All 22 WeenieData fields are verified below. var spawn = new WorldSession.EntitySpawn( Guid: 0x00000600u, Position: null, SetupTableId: null, AnimPartChanges: System.Array.Empty(), TextureChanges: System.Array.Empty(), SubPalettes: System.Array.Empty(), 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, }; 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); } // ------------------------------------------------------------------------- // The handler that ObjectTableWiring.Wire subscribes to session.EntityDeleted. // Testing it directly avoids needing a live WorldSession (UDP socket). // ------------------------------------------------------------------------- private static Action 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); /// /// 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. /// [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)); } /// /// DeleteObject path: FromPickup = false → weenie IS evicted from ClientObjectTable. /// Regression guard — true destroys (0xF747) must still clean up the table. /// [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)); } }