acdream/src/AcDream.Core.Net/ObjectTableWiring.cs
Erik 8a5d77f7f4 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>
2026-07-14 00:22:17 +02:00

142 lines
6.3 KiB
C#

using AcDream.Core.Items;
using AcDream.Core.Player;
namespace AcDream.Core.Net;
/// <summary>
/// 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.
/// 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 quality, inventory, and property
/// updates whose freshness is independent of the physics timestamp pack.
/// </summary>
public static void Wire(
WorldSession session,
ClientObjectTable table,
Func<uint>? playerGuid = null,
LocalPlayerState? localPlayer = null)
{
ArgumentNullException.ThrowIfNull(session);
ArgumentNullException.ThrowIfNull(table);
// 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
// stores it in the bundle and still mirrors UiEffects → the typed Effects field.
session.ObjectIntPropertyUpdated += u =>
table.UpdateIntProperty(u.Guid, u.Property, u.Value);
// B-Wire: PrivateUpdatePropertyInt (0x02CD) carries no guid — it targets the
// local player. Route it to the player object so live EncumbranceVal updates the
// burden bar. The player ClientObject is created at login by the PD UpsertProperties
// call (which precedes any live 0x02CD), so UpdateIntProperty finds it. If it somehow
// hasn't yet, this no-ops (UpdateIntProperty returns false on an unknown guid) rather
// than creating a phantom — the next PD / CreateObject seeds it.
session.PlayerIntPropertyUpdated += u =>
{
if (playerGuid is not null)
table.UpdateIntProperty(playerGuid(), u.Property, u.Value);
};
// Retail's qualities system owns one local-player value and notifies every
// registered panel. acdream currently exposes that value through two projections:
// ClientObjectTable (retained UI) and LocalPlayerState (Core consumers/fallback).
// Apply the authoritative 0x02CF update to both in this one wiring owner so they
// cannot drift after the login PlayerDescription snapshot.
session.PlayerInt64PropertyUpdated += u => ApplyPlayerInt64PropertyUpdate(
table, localPlayer, playerGuid?.Invoke() ?? 0u, u);
// B-Wire: SetStackSize (0x0197) — update the object's stack count + value.
session.StackSizeUpdated += u => table.UpdateStackSize(u.Guid, u.StackSize, u.Value);
// B-Wire: InventoryRemoveObject (0x0024) — the object left the player's inventory
// view; drop it from the table (retail ClientUISystem removes it from maintenance).
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,
LocalPlayerState? localPlayer,
uint playerGuid,
WorldSession.PlayerInt64PropertyUpdate update)
{
if (playerGuid != 0u)
table.UpdateInt64Property(playerGuid, update.Property, update.Value);
localPlayer?.OnInt64PropertyUpdate(update.Property, update.Value);
}
/// <summary>Translate the wire spawn into the table's merge patch.</summary>
public static WeenieData ToWeenieData(WorldSession.EntitySpawn s) => new(
Guid: s.Guid,
Name: s.Name,
Type: s.ItemType is { } it ? (ItemType)it : (ItemType?)null,
WeenieClassId: s.WeenieClassId,
IconId: s.IconId,
IconOverlayId: s.IconOverlayId,
IconUnderlayId: s.IconUnderlayId,
Effects: s.UiEffects,
Value: s.Value,
StackSize: s.StackSize,
StackSizeMax: s.StackSizeMax,
Burden: s.Burden,
ContainerId: s.ContainerId,
WielderId: s.WielderId,
ValidLocations: s.ValidLocations,
CurrentWieldedLocation: s.CurrentWieldedLocation,
Priority: s.Priority,
ItemsCapacity: s.ItemsCapacity,
ContainersCapacity: s.ContainersCapacity,
Structure: s.Structure,
MaxStructure: s.MaxStructure,
Workmanship: s.Workmanship,
Useability: s.Useability,
TargetType: s.TargetType,
RadarBlipColor: s.RadarBlipColor,
RadarBehavior: s.RadarBehavior,
PublicWeenieBitfield: s.ObjectDescriptionFlags,
CombatUse: s.CombatUse,
PluralName: s.PluralName,
PetOwnerId: s.PetOwnerId,
AmmoType: s.AmmoType);
}