acdream/tests/AcDream.Core.Net.Tests/ObjectTableWiringTests.cs
Erik 2644d1d527 fix(ui): port exact selected health policy
Replace the toolbar's PWD-bit approximation with retail ObjectIsAttackable composed with the exact player and pet short-circuits. Carry second-header PetOwner through CreateObject, session, and ClientObject state so self, pet, and Free-PK cases match retail while friendly NPCs and attackable non-creatures remain name-only.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 11:42:04 +02:00

192 lines
8.3 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",
PetOwnerId = 0x50000001u,
};
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);
Assert.Equal(0x50000001u, d.PetOwnerId);
}
// -------------------------------------------------------------------------
// 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));
}
}