/* * Copyright (c) 2006-2016, openmetaverse.co * Copyright (c) 2021-2025, Sjofn LLC. * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.co nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; namespace OpenMetaverse { /// /// /// Exception class to identify inventory exceptions /// public class InventoryException : Exception { public InventoryException() { } public InventoryException(string message) : base(message) { } public InventoryException(string message, Exception innerException) : base(message, innerException) { } } /// /// Responsible for maintaining inventory structure. Inventory constructs nodes /// and manages node children as is necessary to maintain a coherent hierarchy. /// Other classes should not manipulate or create InventoryNodes explicitly. When /// A node's parent changes (when a folder is moved, for example) simply pass /// Inventory the updated InventoryFolder, and it will make the appropriate changes /// to its internal representation. /// public class Inventory { #region EventHandlers /// The event subscribers, null if no subscribers private EventHandler m_InventoryObjectUpdated; ///Raises the InventoryObjectUpdated Event /// A InventoryObjectUpdatedEventArgs object containing /// the data sent from the simulator protected virtual void OnInventoryObjectUpdated(InventoryObjectUpdatedEventArgs e) { var handler = m_InventoryObjectUpdated; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_InventoryObjectUpdatedLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler InventoryObjectUpdated { add { lock (m_InventoryObjectUpdatedLock) { m_InventoryObjectUpdated += value; } } remove { lock (m_InventoryObjectUpdatedLock) { m_InventoryObjectUpdated -= value; } } } /// The event subscribers, null if no subscribers private EventHandler m_InventoryObjectRemoved; ///Raises the InventoryObjectRemoved Event /// A InventoryObjectRemovedEventArgs object containing /// the data sent from the simulator protected virtual void OnInventoryObjectRemoved(InventoryObjectRemovedEventArgs e) { var handler = m_InventoryObjectRemoved; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_InventoryObjectRemovedLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler InventoryObjectRemoved { add { lock (m_InventoryObjectRemovedLock) { m_InventoryObjectRemoved += value; } } remove { lock (m_InventoryObjectRemovedLock) { m_InventoryObjectRemoved -= value; } } } /// The event subscribers, null if no subscribers private EventHandler m_InventoryObjectAdded; ///Raises the InventoryObjectAdded Event /// A InventoryObjectAddedEventArgs object containing /// the data sent from the simulator protected virtual void OnInventoryObjectAdded(InventoryObjectAddedEventArgs e) { var handler = m_InventoryObjectAdded; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_InventoryObjectAddedLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler InventoryObjectAdded { add { lock (m_InventoryObjectAddedLock) { m_InventoryObjectAdded += value; } } remove { lock (m_InventoryObjectAddedLock) { m_InventoryObjectAdded -= value; } } } #endregion EventHandlers #region Properties /// /// The root folder of this avatars inventory /// public InventoryFolder RootFolder { get => RootNode.Data as InventoryFolder; set { UpdateNodeFor(value); RootNode = Items[value.UUID]; } } /// /// The default shared library folder /// public InventoryFolder LibraryFolder { get => LibraryRootNode.Data as InventoryFolder; set { UpdateNodeFor(value); LibraryRootNode = Items[value.UUID]; } } /// /// The root node of the avatars inventory /// public InventoryNode RootNode { get; private set; } /// /// The root node of the default shared library /// public InventoryNode LibraryRootNode { get; private set; } /// /// Returns owner of Inventory /// public UUID Owner { get; } /// /// Returns number of stored entries /// public int Count => Items.Count; #endregion Properties private readonly GridClient Client; //private InventoryManager Manager; private ConcurrentDictionary Items; public Inventory(GridClient client) : this(client, client.Self.AgentID) { } public Inventory(GridClient client, UUID owner) { Client = client; Owner = owner; if (owner == UUID.Zero) Logger.Log("Inventory owned by nobody!", Helpers.LogLevel.Warning, Client); Items = new ConcurrentDictionary(); } public List GetContents(InventoryFolder folder) { return GetContents(folder.UUID); } /// /// Returns the contents of the specified folder /// /// A folder's UUID /// The contents of the folder corresponding to /// When does not exist in the inventory public List GetContents(UUID folder) { if (!Items.TryGetValue(folder, out var folderNode)) throw new InventoryException("Unknown folder: " + folder); lock (folderNode.Nodes.SyncRoot) { var contents = new List(folderNode.Nodes.Count); contents.AddRange(folderNode.Nodes.Values.Select(node => node.Data)); return contents; } } /// /// Updates the state of the InventoryNode and inventory data structure that /// is responsible for the InventoryObject. If the item was previously not added to inventory, /// it adds the item, and updates structure accordingly. If it was, it updates the /// InventoryNode, changing the parent node if does /// not match . /// /// You can not set the inventory root folder using this method /// /// The InventoryObject to store public void UpdateNodeFor(InventoryBase item) { InventoryObjectUpdatedEventArgs itemUpdatedEventArgs = null; InventoryObjectAddedEventArgs itemAddedEventArgs = null; lock (Items) { InventoryNode itemParent = null; if (item.ParentUUID != UUID.Zero && !Items.TryGetValue(item.ParentUUID, out itemParent)) { // OK, we have no data on the parent, let's create a fake one. var fakeParent = new InventoryFolder(item.ParentUUID) { DescendentCount = 1 // Dear god, please forgive me. }; var fakeItemParent = new InventoryNode(fakeParent); if (Items.TryAdd(item.ParentUUID, fakeItemParent)) { itemParent = fakeItemParent; } else { Items.TryGetValue(item.ParentUUID, out itemParent); } // Unfortunately, this breaks the nice unified tree // while we're waiting for the parent's data to come in. // As soon as we get the parent, the tree repairs itself. //Logger.DebugLog("Attempting to update inventory child of " + // item.ParentUUID.ToString() + " when we have no local reference to that folder", Client); } if (Items.TryGetValue(item.UUID, out var itemNode)) // We're updating. { var oldParent = itemNode.Parent; // Handle parent change if (oldParent == null || itemParent == null || itemParent.Data.UUID != oldParent.Data.UUID) { if (oldParent != null) { lock (oldParent.Nodes.SyncRoot) oldParent.Nodes.Remove(item.UUID); } if (itemParent != null) { lock (itemParent.Nodes.SyncRoot) itemParent.Nodes[item.UUID] = itemNode; } } itemNode.Parent = itemParent; if (m_InventoryObjectUpdated != null) { itemUpdatedEventArgs = new InventoryObjectUpdatedEventArgs(itemNode.Data, item); } itemNode.Data = item; } else // We're adding. { itemNode = new InventoryNode(item, itemParent); bool added = Items.TryAdd(item.UUID, itemNode); if (added && m_InventoryObjectAdded != null) { itemAddedEventArgs = new InventoryObjectAddedEventArgs(item); } } } if(itemUpdatedEventArgs != null) { OnInventoryObjectUpdated(itemUpdatedEventArgs); } if(itemAddedEventArgs != null) { OnInventoryObjectAdded(itemAddedEventArgs); } } public InventoryNode GetNodeFor(UUID uuid) { return Items[uuid]; } public bool TryGetNodeFor(UUID uuid, out InventoryNode node) { return Items.TryGetValue(uuid, out node); } /// /// Removes the InventoryObject and all related node data from Inventory. /// /// The InventoryObject to remove. public void RemoveNodeFor(InventoryBase item) { InventoryObjectRemovedEventArgs itemRemovedEventArgs = null; lock (Items) { if (Items.TryGetValue(item.UUID, out var node)) { if (node.Parent != null) { lock (node.Parent.Nodes.SyncRoot) node.Parent.Nodes.Remove(item.UUID); } bool removed = Items.TryRemove(item.UUID, out node); if (removed && m_InventoryObjectRemoved != null) { itemRemovedEventArgs = new InventoryObjectRemovedEventArgs(item); } } // In case there's a new parent: if (Items.TryGetValue(item.ParentUUID, out var newParent)) { lock (newParent.Nodes.SyncRoot) newParent.Nodes.Remove(item.UUID); } } if(itemRemovedEventArgs != null) { OnInventoryObjectRemoved(itemRemovedEventArgs); } } /// /// Check that Inventory contains the InventoryObject specified by . /// /// The UUID to check. /// true if inventory contains uuid, false otherwise public bool Contains(UUID uuid) { return Items.ContainsKey(uuid); } /// /// Attempts to retrieve an item associated with the specified UUID. /// This method is a specialized overload for retrieving items of type or its derived types. /// /// The unique identifier of the item to retrieve. /// When this method returns true, contains the item if found; otherwise, null. /// true if an item with the specified UUID was found; otherwise, false. /// /// Use this method when you specifically need an item without casting from a generic type. /// For retrieving items of other types, use the generic method. /// public bool TryGetValue(UUID uuid, out InventoryBase item) { item = null; if(TryGetNodeFor(uuid, out var node)) { item = node.Data; } return item != null; } /// /// Attempts to retrieve an item of type associated with the specified UUID. /// This method is generic and can be used for any type stored in the inventory. /// /// The type of the item to retrieve. /// The unique identifier of the item to retrieve. /// When this method returns true, contains the item of type if found and compatible with the requested type; otherwise, the default value of . /// true if an item with the specified UUID was found and is of type ; otherwise, false. /// /// Use this method when you need to retrieve an item of a specific type other than . /// If you are retrieving an item or its derived types, consider using the specialized overload for convenience. /// public bool TryGetValue(UUID uuid, out T item) { if (TryGetNodeFor(uuid, out var node) && node.Data is T requestedItem) { item = requestedItem; return true; } item = default; return false; } /// /// Check that Inventory contains the InventoryObject specified by . /// /// Object to check for /// true if inventory contains object, false otherwise public bool Contains(InventoryBase obj) { return Contains(obj.UUID); } /// /// Clear all entries from Inventory store. /// Useful for regenerating contents. /// public void Clear() { Items.Clear(); } /// /// Saves the current inventory structure to a cache file /// /// Name of the cache file to save to public void SaveToDisk(string filename) { InventoryCache.SaveToDisk(filename, Items); } /// /// Loads in inventory cache file into the inventory structure. Note only valid to call after login has been successful. /// /// Name of the cache file to load /// The number of inventory items successfully reconstructed into the inventory node tree, or -1 on error public int RestoreFromDisk(string filename) { return InventoryCache.RestoreFromDisk(filename, Items); } #region Operators /// /// By using the bracket operator on this class, the program can get the /// InventoryObject designated by the specified uuid. If the value for the corresponding /// UUID is null, the call is equivalent to a call to . /// If the value is non-null, it is equivalent to a call to , /// the uuid parameter is ignored. /// /// The UUID of the InventoryObject to get or set, ignored if set to non-null value. /// The InventoryObject corresponding to . public InventoryBase this[UUID uuid] { get { var node = Items[uuid]; return node.Data; } set { if (value != null) { // Log a warning if there is a UUID mismatch, this will cause problems if (value.UUID != uuid) { Logger.Log($"Inventory[uuid]: uuid {uuid} is not equal to value.UUID {value.UUID}", Helpers.LogLevel.Warning, Client); } UpdateNodeFor(value); } else { if (Items.TryGetValue(uuid, out var node)) { RemoveNodeFor(node.Data); } } } } #endregion Operators } #region EventArgs classes public class InventoryObjectUpdatedEventArgs : EventArgs { public InventoryBase OldObject { get; } public InventoryBase NewObject { get; } public InventoryObjectUpdatedEventArgs(InventoryBase oldObject, InventoryBase newObject) { this.OldObject = oldObject; this.NewObject = newObject; } } public class InventoryObjectRemovedEventArgs : EventArgs { public InventoryBase Obj { get; } public InventoryObjectRemovedEventArgs(InventoryBase obj) { this.Obj = obj; } } public class InventoryObjectAddedEventArgs : EventArgs { public InventoryBase Obj { get; } public InventoryObjectAddedEventArgs(InventoryBase obj) { this.Obj = obj; } } #endregion EventArgs }