using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace AcDream.Core.Items; /// /// Live item-state mirror — the client-side view of every item the /// server has spawned for this session. Owns /// records, tracks which container holds each item, and fires events so /// UI panels (inventory, paperdoll, hotbars) can redraw on change. /// /// /// Retail semantics (r06): /// /// /// Every item is a with a unique /// ObjectId. CreateObject seeds it when the server tells us /// the item exists (in our inventory, on the ground, in a /// vendor's list, etc). /// /// /// Moves happen via -carrying messages: /// WieldObject, InventoryPutObjInContainer, /// InventoryPutObjectIn3D, ViewContents, /// CloseGroundContainer. /// /// /// InventoryServerSaveFailed reverts a speculative local /// state change (e.g. when a drag-drop was rejected server-side). /// /// /// /// /// /// Thread safety: designed for single-threaded use from the render /// thread; the event delegates run synchronously on the caller's /// thread. A backs the /// map so plugin code can look up items from any thread without /// corrupting state. /// /// public sealed class ItemRepository { private readonly ConcurrentDictionary _items = new(); private readonly ConcurrentDictionary _containers = new(); /// Fires when an item is first added to the session. public event Action? ItemAdded; /// /// Fires when an item's container / slot changes (moved between /// packs, equipped, unequipped, dropped on ground). Old and new /// container ids are 0 if origin or destination is "world" / "nowhere". /// public event Action? ItemMoved; /// Fires when an item is removed from the session. public event Action? ItemRemoved; /// Fires when an item's properties are updated (typically after Appraise). public event Action? ItemPropertiesUpdated; public int ItemCount => _items.Count; public int ContainerCount => _containers.Count; public IEnumerable Items => _items.Values; public IEnumerable Containers => _containers.Values; /// /// Look up an item by its server-assigned ObjectId. /// public ItemInstance? GetItem(uint objectId) => _items.TryGetValue(objectId, out var item) ? item : null; /// /// Look up a container by object id, creating a lightweight stub if /// the id doesn't match any known container (defensive — avoids losing /// references when the server announces a move into a container it /// hasn't described yet). /// public Container? GetContainer(uint objectId) => _containers.TryGetValue(objectId, out var c) ? c : null; /// /// Register / refresh an item in the repository. Called on /// CreateObject for item-typed weenies and on IdentifyObjectResponse /// to fill in detail properties. /// public void AddOrUpdate(ItemInstance item) { ArgumentNullException.ThrowIfNull(item); bool existed = _items.ContainsKey(item.ObjectId); _items[item.ObjectId] = item; if (!existed) ItemAdded?.Invoke(item); else ItemPropertiesUpdated?.Invoke(item); } /// /// Register a container. Idempotent. /// public void AddContainer(Container container) { ArgumentNullException.ThrowIfNull(container); _containers[container.ObjectId] = container; } /// /// Handle a server-driven move — called from /// InventoryPutObjInContainer (0x0022) and WieldObject (0x0023) /// handlers. Updates ContainerId / ContainerSlot / CurrentlyEquippedLocation /// and fires ItemMoved. /// public bool MoveItem(uint itemId, uint newContainerId, int newSlot = -1, EquipMask newEquipLocation = EquipMask.None) { if (!_items.TryGetValue(itemId, out var item)) return false; uint oldContainer = item.ContainerId; item.ContainerId = newContainerId; item.ContainerSlot = newSlot; item.CurrentlyEquippedLocation = newEquipLocation; ItemMoved?.Invoke(item, oldContainer, newContainerId); return true; } /// /// Handle a server-driven remove (destroyed item, dropped into 3D /// space, stolen, etc). /// public bool Remove(uint itemId) { if (!_items.TryRemove(itemId, out var item)) return false; ItemRemoved?.Invoke(item); return true; } /// /// Apply a patch (e.g. from an /// IdentifyObjectResponse) to an existing item. Individual /// keys in the incoming bundle overwrite existing values; keys not /// present are left untouched. /// public bool UpdateProperties(uint itemId, PropertyBundle incoming) { if (!_items.TryGetValue(itemId, out var item)) return false; foreach (var kv in incoming.Ints) item.Properties.Ints[kv.Key] = kv.Value; foreach (var kv in incoming.Int64s) item.Properties.Int64s[kv.Key] = kv.Value; foreach (var kv in incoming.Bools) item.Properties.Bools[kv.Key] = kv.Value; foreach (var kv in incoming.Floats) item.Properties.Floats[kv.Key] = kv.Value; foreach (var kv in incoming.Strings) item.Properties.Strings[kv.Key] = kv.Value; foreach (var kv in incoming.DataIds) item.Properties.DataIds[kv.Key] = kv.Value; foreach (var kv in incoming.InstanceIds) item.Properties.InstanceIds[kv.Key] = kv.Value; ItemPropertiesUpdated?.Invoke(item); return true; } /// /// Flush the repository — typically called on logoff or teleport /// that drops the session's item state. /// public void Clear() { _items.Clear(); _containers.Clear(); } }