feat(net): port retail physics spawn and event timestamps

Parse the complete PhysicsDesc plus F754/F755 packets, correct every PhysicsState bit, and gate all nine retail update channels with generation-safe immutable snapshots. Preserve ForcePosition, teleport, placement, velocity, parent, pickup, delete, and same-generation CreateObject ordering from the named client.

Separate accepted logical lifecycle notifications from retained UI qualities, make GUID replacement and session reset clear every projection exactly once, and add packet, wraparound, malformed-input, parent FIFO, canonical-position, reconnect, and GUID-reuse conformance coverage.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-14 00:22:17 +02:00
parent d53fe30ffe
commit 8a5d77f7f4
50 changed files with 3809 additions and 649 deletions

View file

@ -4,21 +4,20 @@ using AcDream.Core.Player;
namespace AcDream.Core.Net;
/// <summary>
/// Wires WorldSession GameMessage-level object events into the client object
/// table: CreateObject (0xF745) = canonical merge-upsert, DeleteObject (0xF747)
/// = evict, PublicUpdatePropertyInt (0x02CE) = live int apply, PrivateUpdatePropertyInt
/// Wires WorldSession quality/property events into the client object table:
/// PublicUpdatePropertyInt (0x02CE) = live int apply, PrivateUpdatePropertyInt
/// (0x02CD) = player int (burden), PrivateUpdatePropertyInt64 (0x02CF) = player
/// 64-bit qualities (experience), SetStackSize (0x0197) = stack count,
/// InventoryRemoveObject (0x0024) = inventory-view removal.
/// Keeps object ingestion in Core.Net (pure data, no GL) and off GameWindow.
/// CreateObject/DeleteObject application remains exposed as pure helpers so
/// the App live-entity owner can freshness-gate the shared projections first.
/// Retail: ACCObjectMaint::CreateObject / DeleteObject (the weenie_object_table side).
/// </summary>
public static class ObjectTableWiring
{
/// <summary>
/// Subscribe <paramref name="table"/> to the three object-lifecycle events
/// on <paramref name="session"/>. Call this BEFORE the render handler subscribes
/// to EntitySpawned so the table is populated before the render path runs.
/// Subscribe <paramref name="table"/> to quality, inventory, and property
/// updates whose freshness is independent of the physics timestamp pack.
/// </summary>
public static void Wire(
WorldSession session,
@ -29,14 +28,9 @@ public static class ObjectTableWiring
ArgumentNullException.ThrowIfNull(session);
ArgumentNullException.ThrowIfNull(table);
session.EntitySpawned += s => table.Ingest(ToWeenieData(s));
// 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); };
// Create/Delete/Pickup are deliberately not subscribed here. Their
// shared live-object timestamps must be accepted before either the
// retained weenie projection or the render projection mutates.
// B-Wire: apply EVERY PropertyInt update on a visible object (0x02CE), not just
// UiEffects — the server is the authority on object properties. UpdateIntProperty
@ -72,6 +66,34 @@ public static class ObjectTableWiring
session.InventoryObjectRemoved += guid => table.Remove(guid);
}
/// <summary>
/// Applies one CreateObject to the retained object projection after the
/// owning runtime accepts its physics generation. Internal for a
/// socket-free conformance test of the subscription body.
/// </summary>
public static void ApplyEntitySpawn(
ClientObjectTable table,
WorldSession.EntitySpawn spawn,
bool replaceGeneration = false)
{
WeenieData data = ToWeenieData(spawn);
if (replaceGeneration)
table.ReplaceGeneration(data, spawn.InstanceSequence);
else
table.Ingest(data);
}
/// <summary>
/// Applies a true DeleteObject only after the owning runtime accepts the
/// exact object incarnation. Pickup still removes only the 3-D projection.
/// </summary>
public static void ApplyEntityDelete(
ClientObjectTable table,
Messages.DeleteObject.Parsed delete)
{
table.RemoveLogicalGeneration(delete.Guid, delete.InstanceSequence);
}
/// <summary>Shared application step kept internal for byte-to-state conformance tests.</summary>
internal static void ApplyPlayerInt64PropertyUpdate(
ClientObjectTable table,