fix(D.2b): PickupEvent removes the 3D render, not the weenie — unwield lands in the pack
PickupEvent (0xF74A) and DeleteObject (0xF747) are semantically distinct: - 0xF747 = weenie DESTROYED → evict from ClientObjectTable (weenie_object_table) - 0xF74A = object LEFT THE 3D WORLD VIEW (moved into a container) → remove the 3D WorldEntity, but the weenie persists in ClientObjectTable Before this fix, both paths fired EntityDeleted identically, causing ObjectTableWiring to evict the weenie from ClientObjectTable. The follow-up InventoryPutObjInContainer (0x0022) then tried MoveItem on an unknown guid and no-op'd, so the unwielded item simply vanished. Fix: add `bool FromPickup` (default false) to DeleteObject.Parsed. WorldSession sets it true on the PickupEvent path and false on the DeleteObject path. ObjectTableWiring.Wire's EntityDeleted handler skips table.Remove when FromPickup is true, preserving the weenie for the container-move echo. GameWindow.OnLiveEntityDeleted (3D entity removal) is untouched — it fires for both pickups and destroys, as intended. Divergence register: AP-65 added (data ghosts for other-player pickups until teleport/relog clear; harmless — no UI queries ContainerId 0). Tests: +5 (DeleteObject FromPickup parser regression; wiring retain/evict semantics; Parsed default/explicit FromPickup). 343/343 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
058da60212
commit
db0cac03c4
6 changed files with 151 additions and 13 deletions
|
|
@ -7,10 +7,13 @@ namespace AcDream.Core.Net.Tests;
|
|||
/// <summary>
|
||||
/// D.5.4 Task 7 — ObjectTableWiring.
|
||||
///
|
||||
/// Integration test is omitted: WorldSession.EntitySpawned has no internal
|
||||
/// test seam to fire it without a real Tick + packet bytes, so subscription
|
||||
/// correctness is covered by build (type-checks) + the live run. Only the
|
||||
/// pure mapping (ToWeenieData) is unit-tested here.
|
||||
/// 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
|
||||
{
|
||||
|
|
@ -94,4 +97,80 @@ public sealed class ObjectTableWiringTests
|
|||
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<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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue