using AcDream.Core.Items; using AcDream.Core.Player; namespace AcDream.Core.Net; /// /// 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). /// public static class ObjectTableWiring { /// /// Subscribe to quality, inventory, and property /// updates whose freshness is independent of the physics timestamp pack. /// public static void Wire( WorldSession session, ClientObjectTable table, Func? 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); } /// /// 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. /// 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); } /// /// Applies a true DeleteObject only after the owning runtime accepts the /// exact object incarnation. Pickup still removes only the 3-D projection. /// public static void ApplyEntityDelete( ClientObjectTable table, Messages.DeleteObject.Parsed delete) { table.RemoveLogicalGeneration(delete.Guid, delete.InstanceSequence); } /// Shared application step kept internal for byte-to-state conformance tests. 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); } /// Translate the wire spawn into the table's merge patch. 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, SpellId: s.SpellId); }