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
|
|
@ -17,11 +17,23 @@ public static class DeleteObject
|
|||
{
|
||||
public const uint Opcode = 0xF747u;
|
||||
|
||||
public readonly record struct Parsed(uint Guid, ushort InstanceSequence);
|
||||
/// <param name="FromPickup">
|
||||
/// <see langword="true"/> when this delete was sourced from a
|
||||
/// <c>PickupEvent (0xF74A)</c> — the object left the 3-D world view but
|
||||
/// the weenie record still exists (it is moving into a container).
|
||||
/// <see langword="false"/> (default) when sourced from <c>DeleteObject
|
||||
/// (0xF747)</c> — the weenie was destroyed and must be evicted from
|
||||
/// <see cref="AcDream.Core.Items.ClientObjectTable"/>.
|
||||
/// Retail two-table model: PickupEvent removes from <c>object_table</c>;
|
||||
/// DeleteObject removes from <c>weenie_object_table</c>.
|
||||
/// </param>
|
||||
public readonly record struct Parsed(uint Guid, ushort InstanceSequence, bool FromPickup = false);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a 0xF747 body. <paramref name="body"/> must start with the
|
||||
/// 4-byte opcode, matching every other parser in this namespace.
|
||||
/// The returned <see cref="Parsed.FromPickup"/> is always <see langword="false"/>
|
||||
/// — this parser handles true destroys only.
|
||||
/// </summary>
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
|
|
@ -34,6 +46,6 @@ public static class DeleteObject
|
|||
|
||||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(4, 4));
|
||||
ushort instanceSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(8, 2));
|
||||
return new Parsed(guid, instanceSequence);
|
||||
return new Parsed(guid, instanceSequence, FromPickup: false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,13 @@ public static class ObjectTableWiring
|
|||
ArgumentNullException.ThrowIfNull(table);
|
||||
|
||||
session.EntitySpawned += s => table.Ingest(ToWeenieData(s));
|
||||
session.EntityDeleted += d => table.Remove(d.Guid);
|
||||
// Retail two-table model:
|
||||
// PickupEvent (0xF74A) → object left the 3-D world view; the weenie persists
|
||||
// (it is moving into a container, e.g. unwield-to-pack). Remove the 3-D
|
||||
// render entity only — do NOT evict from ClientObjectTable.
|
||||
// DeleteObject (0xF747) → weenie is DESTROYED; evict from both tables.
|
||||
// FromPickup = true guards the weenie eviction so it only fires for true destroys.
|
||||
session.EntityDeleted += d => { if (!d.FromPickup) table.Remove(d.Guid); };
|
||||
|
||||
// B-Wire: apply EVERY PropertyInt update on a visible object (0x02CE), not just
|
||||
// UiEffects — the server is the authority on object properties. UpdateIntProperty
|
||||
|
|
|
|||
|
|
@ -807,16 +807,18 @@ public sealed class WorldSession : IDisposable
|
|||
}
|
||||
else if (op == PickupEvent.Opcode)
|
||||
{
|
||||
// ACE sends PickupEvent (0xF74A) instead of DeleteObject
|
||||
// when a player picks up a world item (Player_Tracking
|
||||
// .RemoveTrackedObject with fromPickup=true). Downstream
|
||||
// view-removal semantics are identical, so we adapt to
|
||||
// DeleteObject.Parsed and reuse the existing handler.
|
||||
// ACE sends PickupEvent (0xF74A) when an object leaves the 3-D world view
|
||||
// (player picks it up, or a player unwields gear that goes back to their pack).
|
||||
// The WEENIE is NOT destroyed — it is moving into a container. We adapt to
|
||||
// DeleteObject.Parsed so the render/entity layer (GameWindow.OnLiveEntityDeleted)
|
||||
// still removes the 3-D WorldEntity, but FromPickup = true tells ObjectTableWiring
|
||||
// to skip the ClientObjectTable eviction (retail two-table model: PickupEvent
|
||||
// targets object_table only; DeleteObject 0xF747 targets weenie_object_table).
|
||||
var parsed = PickupEvent.TryParse(body);
|
||||
if (parsed is not null)
|
||||
EntityDeleted?.Invoke(
|
||||
new DeleteObject.Parsed(
|
||||
parsed.Value.Guid, parsed.Value.InstanceSequence));
|
||||
parsed.Value.Guid, parsed.Value.InstanceSequence, FromPickup: true));
|
||||
}
|
||||
else if (op == UpdateMotion.Opcode)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue