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 IDisposable Wire( WorldSession session, ClientObjectTable table, Func? playerGuid = null, LocalPlayerState? localPlayer = null, Func? accepting = null) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(table); var subscriptions = new SubscriptionSet(); // 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. Action objectIntUpdated = u => { if (accepting?.Invoke() == false) return; table.UpdateIntProperty(u.Guid, u.Property, u.Value); }; session.ObjectIntPropertyUpdated += objectIntUpdated; subscriptions.Add(() => session.ObjectIntPropertyUpdated -= objectIntUpdated); // 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. Action playerIntUpdated = u => { if (accepting?.Invoke() == false) return; if (playerGuid is not null) table.UpdateIntProperty(playerGuid(), u.Property, u.Value); }; session.PlayerIntPropertyUpdated += playerIntUpdated; subscriptions.Add(() => session.PlayerIntPropertyUpdated -= playerIntUpdated); // 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. Action playerInt64Updated = u => { if (accepting?.Invoke() == false) return; ApplyPlayerInt64PropertyUpdate( table, localPlayer, playerGuid?.Invoke() ?? 0u, u); }; session.PlayerInt64PropertyUpdated += playerInt64Updated; subscriptions.Add(() => session.PlayerInt64PropertyUpdated -= playerInt64Updated); // B-Wire: SetStackSize (0x0197) — update the object's stack count + value. Action stackSizeUpdated = u => { if (accepting?.Invoke() == false) return; table.UpdateStackSize(u.Guid, u.StackSize, u.Value); }; session.StackSizeUpdated += stackSizeUpdated; subscriptions.Add(() => session.StackSizeUpdated -= stackSizeUpdated); // B-Wire: InventoryRemoveObject (0x0024) — the object left the player's inventory // view; drop it from the table (retail ClientUISystem removes it from maintenance). Action inventoryObjectRemoved = guid => { if (accepting?.Invoke() == false) return; table.Remove(guid); }; session.InventoryObjectRemoved += inventoryObjectRemoved; subscriptions.Add(() => session.InventoryObjectRemoved -= inventoryObjectRemoved); return subscriptions; } /// /// 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 bool ApplyEntitySpawn( ClientObjectTable table, WorldSession.EntitySpawn spawn, bool replaceGeneration = false, Func? accepting = null) { ArgumentNullException.ThrowIfNull(table); if (accepting?.Invoke() == false) return false; WeenieData data = ToWeenieData(spawn); if (replaceGeneration) { return table.ReplaceGeneration( data, spawn.InstanceSequence, accepting) is not null; } else table.Ingest(data); // Ingest publishes ObjectAdded/ObjectUpdated synchronously. A nested // replacement wins; report the invalidated outer application so its // remaining CreateObject tail cannot run. return accepting?.Invoke() != false; } /// /// 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); }