/* * Copyright (c) 2006-2016, openmetaverse.co * 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.Generic; using System.Linq; using System.Net; using System.Threading; using System.Runtime.Serialization; using System.Threading.Tasks; using OpenMetaverse.Http; using OpenMetaverse.Messages.Linden; using OpenMetaverse.StructuredData; using OpenMetaverse.Packets; namespace OpenMetaverse { #region Enums [Flags] public enum InventorySortOrder : int { /// Sort by name ByName = 0, /// Sort by date ByDate = 1, /// Sort folders by name, regardless of whether items are /// sorted by name or date FoldersByName = 2, /// Place system folders at the top SystemFoldersToTop = 4 } /// /// Possible destinations for DeRezObject request /// public enum DeRezDestination : byte { /// AgentInventorySave = 0, /// Copy from in-world to agent inventory AgentInventoryCopy = 1, /// Derez to TaskInventory TaskInventory = 2, /// Attachment = 3, /// Take Object AgentInventoryTake = 4, /// God force to inventory ForceToGodInventory = 5, /// Delete Object TrashFolder = 6, /// Put an avatar attachment into agent inventory AttachmentToInventory = 7, /// AttachmentExists = 8, /// Return an object back to the owner's inventory ReturnToOwner = 9, /// Return a deeded object back to the last owner's inventory ReturnToLastOwner = 10 } /// /// Upper half of the Flags field for inventory items /// [Flags] public enum InventoryItemFlags : uint { None = 0, /// Indicates that the NextOwner permission will be set to the /// most restrictive set of permissions found in the object set /// (including linkset items and object inventory items) on next rez ObjectSlamPerm = 0x100, /// Indicates that the object sale information has been /// changed ObjectSlamSale = 0x1000, /// If set, and a slam bit is set, indicates BaseMask will be overwritten on Rez ObjectOverwriteBase = 0x010000, /// If set, and a slam bit is set, indicates OwnerMask will be overwritten on Rez ObjectOverwriteOwner = 0x020000, /// If set, and a slam bit is set, indicates GroupMask will be overwritten on Rez ObjectOverwriteGroup = 0x040000, /// If set, and a slam bit is set, indicates EveryoneMask will be overwritten on Rez ObjectOverwriteEveryone = 0x080000, /// If set, and a slam bit is set, indicates NextOwnerMask will be overwritten on Rez ObjectOverwriteNextOwner = 0x100000, /// Indicates whether this object is composed of multiple /// items or not ObjectHasMultipleItems = 0x200000, /// Indicates that the asset is only referenced by this /// inventory item. If this item is deleted or updated to reference a /// new assetID, the asset can be deleted SharedSingleReference = 0x40000000, } #endregion Enums /// /// Tools for dealing with agents inventory /// [Serializable()] public class InventoryManager { /// Used for converting shadow_id to asset_id public static readonly UUID MAGIC_ID = new UUID("3c115e51-04f4-523c-9fa6-98aff1034730"); protected struct InventorySearch { public UUID Folder; public UUID Owner; public string[] Path; public int Level; } #region Delegates /// /// Callback for inventory item creation finishing /// /// Whether the request to create an inventory /// item succeeded or not /// Inventory item being created. If success is /// false this will be null public delegate void ItemCreatedCallback(bool success, InventoryItem item); /// /// Callback for an inventory item being create from an uploaded asset /// /// true if inventory item creation was successful /// /// /// public delegate void ItemCreatedFromAssetCallback(bool success, string status, UUID itemID, UUID assetID); /// /// /// /// public delegate void ItemCopiedCallback(InventoryBase item); /// The event subscribers, null of no subscribers private EventHandler m_ItemReceived; ///Raises the ItemReceived Event /// A ItemReceivedEventArgs object containing /// the data sent from the simulator protected virtual void OnItemReceived(ItemReceivedEventArgs e) { EventHandler handler = m_ItemReceived; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_ItemReceivedLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler ItemReceived { add { lock (m_ItemReceivedLock) { m_ItemReceived += value; } } remove { lock (m_ItemReceivedLock) { m_ItemReceived -= value; } } } /// The event subscribers, null of no subscribers private EventHandler m_FolderUpdated; ///Raises the FolderUpdated Event /// A FolderUpdatedEventArgs object containing /// the data sent from the simulator protected virtual void OnFolderUpdated(FolderUpdatedEventArgs e) { EventHandler handler = m_FolderUpdated; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_FolderUpdatedLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler FolderUpdated { add { lock (m_FolderUpdatedLock) { m_FolderUpdated += value; } } remove { lock (m_FolderUpdatedLock) { m_FolderUpdated -= value; } } } /// The event subscribers, null of no subscribers private EventHandler m_InventoryObjectOffered; ///Raises the InventoryObjectOffered Event /// A InventoryObjectOfferedEventArgs object containing /// the data sent from the simulator protected virtual void OnInventoryObjectOffered(InventoryObjectOfferedEventArgs e) { EventHandler handler = m_InventoryObjectOffered; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_InventoryObjectOfferedLock = new object(); /// Raised when the simulator sends us data containing /// an inventory object sent by another avatar or primitive public event EventHandler InventoryObjectOffered { add { lock (m_InventoryObjectOfferedLock) { m_InventoryObjectOffered += value; } } remove { lock (m_InventoryObjectOfferedLock) { m_InventoryObjectOffered -= value; } } } /// The event subscribers, null of no subscribers private EventHandler m_TaskItemReceived; ///Raises the TaskItemReceived Event /// A TaskItemReceivedEventArgs object containing /// the data sent from the simulator protected virtual void OnTaskItemReceived(TaskItemReceivedEventArgs e) { EventHandler handler = m_TaskItemReceived; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_TaskItemReceivedLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler TaskItemReceived { add { lock (m_TaskItemReceivedLock) { m_TaskItemReceived += value; } } remove { lock (m_TaskItemReceivedLock) { m_TaskItemReceived -= value; } } } /// The event subscribers, null of no subscribers private EventHandler m_FindObjectByPathReply; ///Raises the FindObjectByPath Event /// A FindObjectByPathEventArgs object containing /// the data sent from the simulator protected virtual void OnFindObjectByPathReply(FindObjectByPathReplyEventArgs e) { EventHandler handler = m_FindObjectByPathReply; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_FindObjectByPathReplyLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler FindObjectByPathReply { add { lock (m_FindObjectByPathReplyLock) { m_FindObjectByPathReply += value; } } remove { lock (m_FindObjectByPathReplyLock) { m_FindObjectByPathReply -= value; } } } /// The event subscribers, null of no subscribers private EventHandler m_TaskInventoryReply; ///Raises the TaskInventoryReply Event /// A TaskInventoryReplyEventArgs object containing /// the data sent from the simulator protected virtual void OnTaskInventoryReply(TaskInventoryReplyEventArgs e) { EventHandler handler = m_TaskInventoryReply; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_TaskInventoryReplyLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler TaskInventoryReply { add { lock (m_TaskInventoryReplyLock) { m_TaskInventoryReply += value; } } remove { lock (m_TaskInventoryReplyLock) { m_TaskInventoryReply -= value; } } } /// /// Reply received when uploading an inventory asset /// /// Has upload been successful /// Error message if upload failed /// Inventory asset UUID /// New asset UUID public delegate void InventoryUploadedAssetCallback(bool success, string status, UUID itemID, UUID assetID); /// The event subscribers, null of no subscribers private EventHandler m_SaveAssetToInventory; ///Raises the SaveAssetToInventory Event /// A SaveAssetToInventoryEventArgs object containing /// the data sent from the simulator protected virtual void OnSaveAssetToInventory(SaveAssetToInventoryEventArgs e) { EventHandler handler = m_SaveAssetToInventory; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_SaveAssetToInventoryLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler SaveAssetToInventory { add { lock (m_SaveAssetToInventoryLock) { m_SaveAssetToInventory += value; } } remove { lock (m_SaveAssetToInventoryLock) { m_SaveAssetToInventory -= value; } } } /// /// Delegate that is invoked when script upload is completed /// /// Has upload succeded (note, there still might be compile errors) /// Upload status message /// Is compilation successful /// If compilation failed, list of error messages, null on compilation success /// Script inventory UUID /// Script's new asset UUID public delegate void ScriptUpdatedCallback(bool uploadSuccess, string uploadStatus, bool compileSuccess, List compileMessages, UUID itemID, UUID assetID); /// The event subscribers, null of no subscribers private EventHandler m_ScriptRunningReply; ///Raises the ScriptRunningReply Event /// A ScriptRunningReplyEventArgs object containing /// the data sent from the simulator protected virtual void OnScriptRunningReply(ScriptRunningReplyEventArgs e) { EventHandler handler = m_ScriptRunningReply; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_ScriptRunningReplyLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler ScriptRunningReply { add { lock (m_ScriptRunningReplyLock) { m_ScriptRunningReply += value; } } remove { lock (m_ScriptRunningReplyLock) { m_ScriptRunningReply -= value; } } } #endregion Delegates #region String Arrays /// Partial mapping of FolderTypes to folder names private static readonly string[] _NewFolderNames = new string[] { "Textures", // 0 "Sounds", // 1 "Calling Cards", // 2 "Landmarks", // 3 String.Empty, // 4 "Clothing", // 5 "Objects", // 6 "Notecards", // 7 "My Inventory", // 8 String.Empty, // 9 "Scripts", // 10 String.Empty, // 11 String.Empty, // 12 "Body Parts", // 13 "Trash", // 14 "Photo Album", // 15 "Lost And Found", // 16 String.Empty, // 17 String.Empty, // 18 String.Empty, // 19 "Animations", // 20 "Gestures", // 21 String.Empty, // 22 "Favorites", // 23 String.Empty, // 24 String.Empty, // 25 "New Folder", // 26 "New Folder", // 27 "New Folder", // 28 "New Folder", // 29 "New Folder", // 30 "New Folder", // 31 "New Folder", // 32 "New Folder", // 33 "New Folder", // 34 "New Folder", // 35 "New Folder", // 36 "New Folder", // 37 "New Folder", // 38 "New Folder", // 39 "New Folder", // 40 "New Folder", // 41 "New Folder", // 42 "New Folder", // 43 "New Folder", // 44 "New Folder", // 45 "Current Outfit", // 46 "New Outfit", // 47 "My Outfits", // 48 "Meshes", // 49 "Received Items", // 50 "Merchant Outbox", // 51 "Basic Root", // 52 "Marketplace Listings", // 53 "New Stock", // 54 }; #endregion String Arrays [NonSerialized] private GridClient Client; [NonSerialized] private Inventory _Store; //private Random _RandNumbers = new Random(); private object _CallbacksLock = new object(); private uint _CallbackPos; private Dictionary _ItemCreatedCallbacks = new Dictionary(); private Dictionary _ItemCopiedCallbacks = new Dictionary(); private Dictionary _ItemInventoryTypeRequest = new Dictionary(); private List _Searches = new List(); #region Properties /// /// Get this agents Inventory data /// public Inventory Store => _Store; #endregion Properties /// /// Default constructor /// /// Reference to the GridClient object public InventoryManager(GridClient client) { Client = client; Client.Network.RegisterCallback(PacketType.UpdateCreateInventoryItem, UpdateCreateInventoryItemHandler); Client.Network.RegisterCallback(PacketType.SaveAssetIntoInventory, SaveAssetIntoInventoryHandler); Client.Network.RegisterCallback(PacketType.BulkUpdateInventory, BulkUpdateInventoryHandler); Client.Network.RegisterEventCallback("BulkUpdateInventory", new Caps.EventQueueCallback(BulkUpdateInventoryCapHandler)); Client.Network.RegisterCallback(PacketType.MoveInventoryItem, MoveInventoryItemHandler); Client.Network.RegisterCallback(PacketType.ReplyTaskInventory, ReplyTaskInventoryHandler); Client.Network.RegisterEventCallback("ScriptRunningReply", new Caps.EventQueueCallback(ScriptRunningReplyMessageHandler)); // Deprecated and removed now in Second Life Client.Network.RegisterCallback(PacketType.InventoryDescendents, InventoryDescendentsHandler); Client.Network.RegisterCallback(PacketType.FetchInventoryReply, FetchInventoryReplyHandler); // Watch for inventory given to us through instant message Client.Self.IM += Self_IM; // Register extra parameters with login and parse the inventory data that comes back Client.Network.RegisterLoginResponseCallback( Network_OnLoginResponse, new string[] { "inventory-root", "inventory-skeleton", "inventory-lib-root", "inventory-lib-owner", "inventory-skel-lib"}); } #region Fetch /// /// Fetch an inventory item from the dataserver /// /// The items /// The item Owners /// a integer representing the number of milliseconds to wait for results /// An object on success, or null if no item was found /// Items will also be sent to the event public InventoryItem FetchItem(UUID itemID, UUID ownerID, int timeoutMS) { AutoResetEvent fetchEvent = new AutoResetEvent(false); InventoryItem fetchedItem = null; EventHandler callback = delegate(object sender, ItemReceivedEventArgs e) { if (e.Item.UUID == itemID) { fetchedItem = e.Item; fetchEvent.Set(); } }; ItemReceived += callback; RequestFetchInventory(itemID, ownerID); fetchEvent.WaitOne(timeoutMS, false); ItemReceived -= callback; return fetchedItem; } /// /// Request A single inventory item /// /// The items /// The item Owners /// public void RequestFetchInventory(UUID itemID, UUID ownerID) { RequestFetchInventory(new List(1) { itemID }, new List(1) { ownerID }); } /// /// Request inventory items /// /// Inventory items to request /// Owners of the inventory items /// public void RequestFetchInventory(List itemIDs, List ownerIDs) { if (itemIDs.Count != ownerIDs.Count) throw new ArgumentException("itemIDs and ownerIDs must contain the same number of entries"); if (Client.Settings.HTTP_INVENTORY && Client.Network.CurrentSim.Caps != null && Client.Network.CurrentSim.Caps.CapabilityURI("FetchInventory2") != null) { RequestFetchInventoryCap(itemIDs, ownerIDs); return; } FetchInventoryPacket fetch = new FetchInventoryPacket { AgentData = new FetchInventoryPacket.AgentDataBlock { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryData = new FetchInventoryPacket.InventoryDataBlock[itemIDs.Count] }; for (int i = 0; i < itemIDs.Count; i++) { fetch.InventoryData[i] = new FetchInventoryPacket.InventoryDataBlock { ItemID = itemIDs[i], OwnerID = ownerIDs[i] }; } Client.Network.SendPacket(fetch); } /// /// Request inventory items via Capabilities /// /// Inventory items to request /// Owners of the inventory items /// private void RequestFetchInventoryCap(IReadOnlyList itemIDs, IReadOnlyList ownerIDs) { if (itemIDs.Count != ownerIDs.Count) throw new ArgumentException("itemIDs and ownerIDs must contain the same number of entries"); if (Client.Settings.HTTP_INVENTORY && Client.Network.CurrentSim.Caps != null && Client.Network.CurrentSim.Caps.CapabilityURI("FetchInventory2") != null) { CapsClient request = Client.Network.CurrentSim.Caps.CreateCapsClient("FetchInventory2"); request.OnComplete += (client, result, error) => { if (error != null) return; try { OSDMap res = (OSDMap)result; OSDArray itemsOSD = (OSDArray)res["items"]; foreach (var it in itemsOSD) { InventoryItem item = InventoryItem.FromOSD(it); _Store[item.UUID] = item; OnItemReceived(new ItemReceivedEventArgs(item)); } } catch (Exception ex) { Logger.Log("Failed getting data from FetchInventory2 capability.", Helpers.LogLevel.Error, Client, ex); } }; OSDMap OSDRequest = new OSDMap {["agent_id"] = Client.Self.AgentID}; OSDArray items = new OSDArray(itemIDs.Count); for (int i = 0; i < itemIDs.Count; i++) { OSDMap item = new OSDMap(2) { ["item_id"] = itemIDs[i], ["owner_id"] = ownerIDs[i] }; items.Add(item); } OSDRequest["items"] = items; request.BeginGetResponse(OSDRequest, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } } /// /// Get contents of a folder /// /// The of the folder to search /// The of the folders owner /// true to retrieve folders /// true to retrieve items /// sort order to return results in /// a integer representing the number of milliseconds to wait for results /// A list of inventory items matching search criteria within folder /// /// InventoryFolder.DescendentCount will only be accurate if both folders and items are /// requested public List FolderContents(UUID folder, UUID owner, bool folders, bool items, InventorySortOrder order, int timeoutMS) { List objects = null; AutoResetEvent fetchEvent = new AutoResetEvent(false); EventHandler callback = delegate(object sender, FolderUpdatedEventArgs e) { if (e.FolderID == folder && _Store[folder] is InventoryFolder) { // InventoryDescendentsHandler only stores DescendendCount if both folders and items are fetched. if (_Store.GetContents(folder).Count >= ((InventoryFolder)_Store[folder]).DescendentCount) { fetchEvent.Set(); } } else { fetchEvent.Set(); } }; FolderUpdated += callback; RequestFolderContents(folder, owner, folders, items, order); if (fetchEvent.WaitOne(timeoutMS, false)) objects = _Store.GetContents(folder); FolderUpdated -= callback; return objects; } /// /// Request the contents of an inventory folder /// /// The folder to search /// The folder owners /// true to return s contained in folder /// true to return s containd in folder /// the sort order to return items in /// public void RequestFolderContents(UUID folder, UUID owner, bool folders, bool items, InventorySortOrder order) { string cap = owner == Client.Self.AgentID ? "FetchInventoryDescendents2" : "FetchLibDescendents2"; if (Client.Settings.HTTP_INVENTORY && Client.Network.CurrentSim.Caps != null && Client.Network.CurrentSim.Caps.CapabilityURI(cap) != null) { RequestFolderContentsCap(folder, owner, folders, items, order); } else // Legacy UDP - REMOVED FROM SECOND LIFE { var fetch = new FetchInventoryDescendentsPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryData = { FetchFolders = folders, FetchItems = items, FolderID = folder, OwnerID = owner, SortOrder = (int) order } }; Client.Network.SendPacket(fetch); } } /// /// Request the contents of an inventory folder using HTTP capabilities /// /// The folder to search /// The folder owners /// true to return s contained in folder /// true to return s containd in folder /// the sort order to return items in /// public void RequestFolderContentsCap(UUID folderID, UUID ownerID, bool fetchFolders, bool fetchItems, InventorySortOrder order) { Uri url = null; string cap = ownerID == Client.Self.AgentID ? "FetchInventoryDescendents2" : "FetchLibDescendents2"; if (Client.Network.CurrentSim.Caps == null || (url = Client.Network.CurrentSim.Caps.CapabilityURI(cap)) == null) { Logger.Log(cap + " capability not available in the current sim", Helpers.LogLevel.Warning, Client); OnFolderUpdated(new FolderUpdatedEventArgs(folderID, false)); } else { InventoryFolder folder = new InventoryFolder(folderID) { OwnerID = ownerID, UUID = folderID }; RequestFolderContentsCap(new List() {folder}, url, fetchFolders, fetchItems, order); } } public void RequestFolderContentsCap(List batch, Uri url, bool fetchFolders, bool fetchItems, InventorySortOrder order) { try { CapsClient request = new CapsClient(url, "ReqFolderContents"); request.OnComplete += (client, result, error) => { try { if (error != null) { throw error; } OSDMap resultMap = ((OSDMap)result); if (resultMap.ContainsKey("folders")) { OSDArray fetchedFolders = (OSDArray)resultMap["folders"]; foreach (var fetchedFolderNr in fetchedFolders) { OSDMap res = (OSDMap)fetchedFolderNr; InventoryFolder fetchedFolder; if (_Store.Contains(res["folder_id"]) && _Store[res["folder_id"]] is InventoryFolder) { fetchedFolder = (InventoryFolder)_Store[res["folder_id"]]; } else { fetchedFolder = new InventoryFolder(res["folder_id"]); _Store[res["folder_id"]] = fetchedFolder; } fetchedFolder.DescendentCount = res["descendents"]; fetchedFolder.Version = res["version"]; fetchedFolder.OwnerID = res["owner_id"]; _Store.GetNodeFor(fetchedFolder.UUID).NeedsUpdate = false; // Do we have any descendants if (fetchedFolder.DescendentCount > 0) { // Fetch descendent folders if (res["categories"] is OSDArray folders) { foreach (OSD cat in folders) { OSDMap descFolder = (OSDMap)cat; InventoryFolder folder; UUID folderID = descFolder.ContainsKey("category_id") ? descFolder["category_id"] : descFolder["folder_id"]; if (!_Store.Contains(folderID)) { folder = new InventoryFolder(folderID) { ParentUUID = descFolder["parent_id"] }; _Store[folderID] = folder; } else { folder = (InventoryFolder)_Store[folderID]; } folder.OwnerID = descFolder["agent_id"]; folder.ParentUUID = descFolder["parent_id"]; folder.Name = descFolder["name"]; folder.Version = descFolder["version"]; folder.PreferredType = (FolderType)(int)descFolder["type_default"]; } // Fetch descendent items OSDArray items = (OSDArray)res["items"]; foreach (OSD it in items) { InventoryItem item = InventoryItem.FromOSD(it); _Store[item.UUID] = item; } } } OnFolderUpdated(new FolderUpdatedEventArgs(res["folder_id"], true)); } } } catch (Exception exc) { Logger.Log($"Failed to fetch inventory descendants: {exc.Message}\n" + $"{exc.StackTrace.ToString()}", Helpers.LogLevel.Warning, Client); foreach (var f in batch) { OnFolderUpdated(new FolderUpdatedEventArgs(f.UUID, false)); } } }; // Construct request OSDArray requestedFolders = new OSDArray(1); foreach (var f in batch) { OSDMap requestedFolder = new OSDMap(1) { ["folder_id"] = f.UUID, ["owner_id"] = f.OwnerID, ["fetch_folders"] = fetchFolders, ["fetch_items"] = fetchItems, ["sort_order"] = (int) order }; requestedFolders.Add(requestedFolder); } OSDMap req = new OSDMap(1) {["folders"] = requestedFolders}; request.BeginGetResponse(req, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } catch (Exception ex) { Logger.Log($"Failed to fetch inventory descendants: {ex.Message}\n" + $"{ex.StackTrace}", Helpers.LogLevel.Warning, Client); foreach (var f in batch) { OnFolderUpdated(new FolderUpdatedEventArgs(f.UUID, false)); } return; } } #endregion Fetch #region Find /// /// Returns the UUID of the folder (category) that defaults to /// containing 'type'. The folder is not necessarily only for that /// type /// /// This will return the root folder if one does not exist /// /// The UUID of the desired folder if found, the UUID of the RootFolder /// if not found, or UUID.Zero on failure public UUID FindFolderForType(AssetType type) { if (_Store == null) { Logger.Log("Inventory is null, FindFolderForType() lookup cannot continue", Helpers.LogLevel.Error, Client); return UUID.Zero; } // Folders go in the root if (type == AssetType.Folder) return _Store.RootFolder.UUID; // Loop through each top-level directory and check if PreferredType // matches the requested type List contents = _Store.GetContents(_Store.RootFolder.UUID); foreach (InventoryBase inv in contents) { if (inv is InventoryFolder folder) { if (folder.PreferredType == (FolderType)type) return folder.UUID; } } // No match found, return Root Folder ID return _Store.RootFolder.UUID; } public UUID FindFolderForType(FolderType type) { if (_Store == null) { Logger.Log("Inventory is null, FindFolderForType() lookup cannot continue", Helpers.LogLevel.Error, Client); return UUID.Zero; } List contents = _Store.GetContents(_Store.RootFolder.UUID); foreach (InventoryBase inv in contents) { InventoryFolder folder = inv as InventoryFolder; if (folder?.PreferredType == type) return folder.UUID; } // No match found, return Root Folder ID return _Store.RootFolder.UUID; } /// /// Find an object in inventory using a specific path to search /// /// The folder to begin the search in /// The object owners /// A string path to search /// milliseconds to wait for a reply /// Found items or if /// timeout occurs or item is not found public UUID FindObjectByPath(UUID baseFolder, UUID inventoryOwner, string path, int timeoutMS) { AutoResetEvent findEvent = new AutoResetEvent(false); UUID foundItem = UUID.Zero; void Callback(object sender, FindObjectByPathReplyEventArgs e) { if (e.Path == path) { foundItem = e.InventoryObjectID; findEvent.Set(); } } FindObjectByPathReply += Callback; RequestFindObjectByPath(baseFolder, inventoryOwner, path); findEvent.WaitOne(timeoutMS, false); FindObjectByPathReply -= Callback; return foundItem; } /// /// Find inventory items by path /// /// The folder to begin the search in /// The object owners /// A string path to search, folders/objects separated by a '/' /// Results are sent to the event public void RequestFindObjectByPath(UUID baseFolder, UUID inventoryOwner, string path) { if (string.IsNullOrEmpty(path)) throw new ArgumentException("Empty path is not supported"); // Store this search InventorySearch search; search.Folder = baseFolder; search.Owner = inventoryOwner; search.Path = path.Split('/'); search.Level = 0; lock (_Searches) _Searches.Add(search); // Start the search RequestFolderContents(baseFolder, inventoryOwner, true, true, InventorySortOrder.ByName); } /// /// Search inventory Store object for an item or folder /// /// The folder to begin the search in /// An array which creates a path to search /// Number of levels below baseFolder to conduct searches /// if True, will stop searching after first match is found /// A list of inventory items found public List LocalFind(UUID baseFolder, string[] path, int level, bool firstOnly) { List objects = new List(); //List folders = new List(); List contents = _Store.GetContents(baseFolder); foreach (InventoryBase inv in contents) { if (string.Compare(inv.Name, path[level], StringComparison.Ordinal) == 0) { if (level == path.Length - 1) { objects.Add(inv); if (firstOnly) return objects; } else if (inv is InventoryFolder) { objects.AddRange(LocalFind(inv.UUID, path, level + 1, firstOnly)); } } } return objects; } #endregion Find #region Move/Rename /// /// Move an inventory item or folder to a new location /// /// The item or folder to move /// The to move item or folder to public void Move(InventoryBase item, InventoryFolder newParent) { if (item is InventoryFolder) MoveFolder(item.UUID, newParent.UUID); else MoveItem(item.UUID, newParent.UUID); } /// /// Move an inventory item or folder to a new location and change its name /// /// The item or folder to move /// The to move item or folder to /// The name to change the item or folder to [Obsolete("Method broken with AIS3. Use Move(item, parent) instead.")] public void Move(InventoryBase item, InventoryFolder newParent, string newName) { if (item is InventoryFolder) MoveFolder(item.UUID, newParent.UUID, newName); else MoveItem(item.UUID, newParent.UUID, newName); } /// /// Move and rename a folder /// /// The source folders /// The destination folders /// The name to change the folder to [Obsolete("Method broken with AIS3. Use MoveFolder(folder, parent) and UpdateFolderProperties(folder, parent, name, type) instead")] public void MoveFolder(UUID folderID, UUID newparentID, string newName) { UpdateFolderProperties(folderID, newparentID, newName, FolderType.None); } /// /// Update folder properties /// /// of the folder to update /// Sets folder's parent to /// Folder name /// Folder type public void UpdateFolderProperties(UUID folderID, UUID parentID, string name, FolderType type) { InventoryFolder inv = null; lock (Store) { if (_Store.Contains(folderID)) { inv = (InventoryFolder)Store[folderID]; inv.Name = name; inv.ParentUUID = parentID; inv.PreferredType = type; } } if (Client.AisClient.IsAvailable) { if (inv != null) { Client.AisClient.UpdateCategory(folderID, inv.GetOSD(), (success) => { if (success) { lock (_Store) { _Store.UpdateNodeFor(inv); } } } ).ConfigureAwait(false); } } else { lock (_Store) { _Store.UpdateNodeFor(inv); } var invFolder = new UpdateInventoryFolderPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, FolderData = new UpdateInventoryFolderPacket.FolderDataBlock[1] }; invFolder.FolderData[0] = new UpdateInventoryFolderPacket.FolderDataBlock { FolderID = folderID, ParentID = parentID, Name = Utils.StringToBytes(name), Type = (sbyte) type }; Client.Network.SendPacket(invFolder); } } /// /// Move a folder /// /// The source folders /// The destination folders public void MoveFolder(UUID folderID, UUID newParentID) { lock (Store) { if (_Store.Contains(folderID)) { InventoryBase inv = Store[folderID]; inv.ParentUUID = newParentID; _Store.UpdateNodeFor(inv); } } var move = new MoveInventoryFolderPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, Stamp = false //FIXME: ?? }, InventoryData = new MoveInventoryFolderPacket.InventoryDataBlock[1] }; move.InventoryData[0] = new MoveInventoryFolderPacket.InventoryDataBlock { FolderID = folderID, ParentID = newParentID }; Client.Network.SendPacket(move); } /// /// Move multiple folders, the keys in the Dictionary parameter, /// to a new parents, the value of that folder's key. /// /// A Dictionary containing the /// of the source as the key, and the /// of the destination as the value public void MoveFolders(Dictionary foldersNewParents) { // FIXME: Use two List to stay consistent lock (Store) { foreach (KeyValuePair entry in foldersNewParents) { if (_Store.Contains(entry.Key)) { InventoryBase inv = _Store[entry.Key]; inv.ParentUUID = entry.Value; _Store.UpdateNodeFor(inv); } } } //TODO: Test if this truly supports multiple-folder move var move = new MoveInventoryFolderPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, Stamp = false //FIXME: ?? }, InventoryData = new MoveInventoryFolderPacket.InventoryDataBlock[foldersNewParents.Count] }; int index = 0; foreach (KeyValuePair folder in foldersNewParents) { var block = new MoveInventoryFolderPacket.InventoryDataBlock { FolderID = folder.Key, ParentID = folder.Value }; move.InventoryData[index++] = block; } Client.Network.SendPacket(move); } /// /// Move an inventory item to a new folder /// /// The of the source item to move /// The of the destination folder public void MoveItem(UUID itemID, UUID folderID) { MoveItem(itemID, folderID, String.Empty); } /// /// Move and rename an inventory item /// /// The of the source item to move /// The of the destination folder /// The name to change the folder to public void MoveItem(UUID itemID, UUID folderID, string newName) { lock (_Store) { if (_Store.Contains(itemID)) { InventoryBase inv = _Store[itemID]; if (!string.IsNullOrEmpty(newName)) { inv.Name = newName; } inv.ParentUUID = folderID; _Store.UpdateNodeFor(inv); } } var move = new MoveInventoryItemPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, Stamp = false //FIXME: ?? }, InventoryData = new MoveInventoryItemPacket.InventoryDataBlock[1] }; move.InventoryData[0] = new MoveInventoryItemPacket.InventoryDataBlock { ItemID = itemID, FolderID = folderID, NewName = Utils.StringToBytes(newName) }; Client.Network.SendPacket(move); } /// /// Move multiple inventory items to new locations /// /// A Dictionary containing the /// of the source item as the key, and the /// of the destination folder as the value public void MoveItems(Dictionary itemsNewParents) { lock (_Store) { foreach (KeyValuePair entry in itemsNewParents) { if (_Store.Contains(entry.Key)) { InventoryBase inv = _Store[entry.Key]; inv.ParentUUID = entry.Value; _Store.UpdateNodeFor(inv); } } } var move = new MoveInventoryItemPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, Stamp = false //FIXME: ?? }, InventoryData = new MoveInventoryItemPacket.InventoryDataBlock[itemsNewParents.Count] }; int index = 0; foreach (KeyValuePair entry in itemsNewParents) { var block = new MoveInventoryItemPacket.InventoryDataBlock { ItemID = entry.Key, FolderID = entry.Value, NewName = Utils.EmptyBytes }; move.InventoryData[index++] = block; } Client.Network.SendPacket(move); } #endregion Move #region Remove private void RemoveLocalUi(bool success, UUID folder) { if (success) { lock (_Store) { if (!_Store.Contains(folder)) return; foreach (InventoryBase obj in _Store.GetContents(folder)) { _Store.RemoveNodeFor(obj); } } } } /// /// Remove descendants of a folder /// /// The of the folder public void RemoveDescendants(UUID folder) { if (Client.AisClient.IsAvailable) { Client.AisClient.PurgeDescendents(folder, RemoveLocalUi).ConfigureAwait(false); } else { var purge = new PurgeInventoryDescendentsPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryData = {FolderID = folder} }; Client.Network.SendPacket(purge); RemoveLocalUi(true, folder); } } /// /// Remove a single item from inventory /// /// The of the inventory item to remove public void RemoveItem(UUID item) { List items = new List(1) {item}; Remove(items, null); } /// /// Remove a folder from inventory /// /// The of the folder to remove public void RemoveFolder(UUID folder) { List folders = new List(1) {folder}; Remove(null, folders); } /// /// Remove multiple items or folders from inventory /// /// A List containing the s of items to remove /// A List containing the s of the folders to remove public void Remove(List items, List folders) { if ((items == null || items.Count == 0) && (folders == null || folders.Count == 0)) return; var rem = new RemoveInventoryObjectsPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID } }; if (items == null || items.Count == 0) { // To indicate that we want no items removed: rem.ItemData = new RemoveInventoryObjectsPacket.ItemDataBlock[1]; rem.ItemData[0] = new RemoveInventoryObjectsPacket.ItemDataBlock {ItemID = UUID.Zero}; } else { lock (_Store) { rem.ItemData = new RemoveInventoryObjectsPacket.ItemDataBlock[items.Count]; for (int i = 0; i < items.Count; i++) { rem.ItemData[i] = new RemoveInventoryObjectsPacket.ItemDataBlock {ItemID = items[i]}; // Update local copy if (_Store.Contains(items[i])) _Store.RemoveNodeFor(Store[items[i]]); } } } if (folders == null || folders.Count == 0) { // To indicate we want no folders removed: rem.FolderData = new RemoveInventoryObjectsPacket.FolderDataBlock[1]; rem.FolderData[0] = new RemoveInventoryObjectsPacket.FolderDataBlock {FolderID = UUID.Zero}; } else { lock (_Store) { rem.FolderData = new RemoveInventoryObjectsPacket.FolderDataBlock[folders.Count]; for (int i = 0; i < folders.Count; i++) { rem.FolderData[i] = new RemoveInventoryObjectsPacket.FolderDataBlock {FolderID = folders[i]}; // Update local copy if (_Store.Contains(folders[i])) _Store.RemoveNodeFor(Store[folders[i]]); } } } Client.Network.SendPacket(rem); } /// /// Empty the Lost and Found folder /// public void EmptyLostAndFound() { EmptySystemFolder(FolderType.LostAndFound); } /// /// Empty the Trash folder /// public void EmptyTrash() { EmptySystemFolder(FolderType.Trash); } private void EmptySystemFolder(FolderType folderType) { UUID folderKey = UUID.Zero; var items = _Store.GetContents(_Store.RootFolder); foreach (var item in items) { InventoryFolder folder = item as InventoryFolder; if (folder?.PreferredType == folderType) { folderKey = folder.UUID; break; } } if (Client.AisClient.IsAvailable) { if (folderKey != UUID.Zero) { Client.AisClient.PurgeDescendents(folderKey, RemoveLocalUi).ConfigureAwait(false); } } else { items = _Store.GetContents(folderKey); List remItems = new List(); List remFolders = new List(); foreach (InventoryBase item in items) { if (item is InventoryFolder) { remFolders.Add(item.UUID); } else { remItems.Add(item.UUID); } } Remove(remItems, remFolders); } } #endregion Remove #region Create /// /// /// /// /// /// /// /// Proper use is to upload the inventory's asset first, then provide the Asset's TransactionID here. /// /// /// public void RequestCreateItem(UUID parentFolder, string name, string description, AssetType type, UUID assetTransactionID, InventoryType invType, PermissionMask nextOwnerMask, ItemCreatedCallback callback) { // Even though WearableType 0 is Shape, in this context it is treated as NOT_WEARABLE RequestCreateItem(parentFolder, name, description, type, assetTransactionID, invType, (WearableType)0, nextOwnerMask, callback); } /// /// /// /// /// /// /// /// Proper use is to upload the inventory's asset first, then provide the Asset's TransactionID here. /// /// /// /// public void RequestCreateItem(UUID parentFolder, string name, string description, AssetType type, UUID assetTransactionID, InventoryType invType, WearableType wearableType, PermissionMask nextOwnerMask, ItemCreatedCallback callback) { var create = new CreateInventoryItemPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryBlock = { CallbackID = RegisterItemCreatedCallback(callback), FolderID = parentFolder, TransactionID = assetTransactionID, NextOwnerMask = (uint) nextOwnerMask, Type = (sbyte) type, InvType = (sbyte) invType, WearableType = (byte) wearableType, Name = Utils.StringToBytes(name), Description = Utils.StringToBytes(description) } }; Client.Network.SendPacket(create); } /// /// Creates a new inventory folder /// /// ID of the folder to put this folder in /// Name of the folder to create /// The UUID of the newly created folder public UUID CreateFolder(UUID parentID, string name) { return CreateFolder(parentID, name, FolderType.None); } /// /// Creates a new inventory folder /// /// ID of the folder to put this folder in /// Name of the folder to create /// Sets this folder as the default folder /// for new assets of the specified type. Use FolderType.None /// to create a normal folder, otherwise it will likely create a /// duplicate of an existing folder type /// The UUID of the newly created folder /// If you specify a preferred type of AsseType.Folder /// it will create a new root folder which may likely cause all sorts /// of strange problems public UUID CreateFolder(UUID parentID, string name, FolderType preferredType) { UUID id = UUID.Random(); // Assign a folder name if one is not already set if (String.IsNullOrEmpty(name)) { if (preferredType >= FolderType.Texture && preferredType <= FolderType.MarkplaceStock) { name = _NewFolderNames[(int)preferredType]; } else { name = "New Folder"; } if (name == string.Empty) { name = "New Folder"; } } // Create the new folder locally InventoryFolder newFolder = new InventoryFolder(id) { Version = 1, DescendentCount = 0, ParentUUID = parentID, PreferredType = preferredType, Name = name, OwnerID = Client.Self.AgentID }; // Update the local store try { _Store[newFolder.UUID] = newFolder; } catch (InventoryException ie) { Logger.Log(ie.Message, Helpers.LogLevel.Warning, Client, ie); } // Create the create folder packet and send it var create = new CreateInventoryFolderPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, FolderData = { FolderID = id, ParentID = parentID, Type = (sbyte) preferredType, Name = Utils.StringToBytes(name) } }; Client.Network.SendPacket(create); return id; } /// /// Create an inventory item and upload asset data /// /// Asset data /// Inventory item name /// Inventory item description /// Asset type /// Inventory type /// Put newly created inventory in this folder /// Delegate that will receive feedback on success or failure public void RequestCreateItemFromAsset(byte[] data, string name, string description, AssetType assetType, InventoryType invType, UUID folderID, ItemCreatedFromAssetCallback callback) { Permissions permissions = new Permissions { EveryoneMask = PermissionMask.None, GroupMask = PermissionMask.None, NextOwnerMask = PermissionMask.All }; RequestCreateItemFromAsset(data, name, description, assetType, invType, folderID, permissions, callback); } /// /// Create an inventory item and upload asset data /// /// Asset data /// Inventory item name /// Inventory item description /// Asset type /// Inventory type /// Put newly created inventory in this folder /// Permission of the newly created item /// (EveryoneMask, GroupMask, and NextOwnerMask of Permissions struct are supported) /// Delegate that will receive feedback on success or failure public void RequestCreateItemFromAsset(byte[] data, string name, string description, AssetType assetType, InventoryType invType, UUID folderID, Permissions permissions, ItemCreatedFromAssetCallback callback) { if (Client.Network.CurrentSim == null || Client.Network.CurrentSim.Caps == null) throw new Exception("NewFileAgentInventory capability is not currently available"); CapsClient request = Client.Network.CurrentSim.Caps.CreateCapsClient("NewFileAgentInventory"); if (request != null) { OSDMap query = new OSDMap { {"folder_id", OSD.FromUUID(folderID)}, {"asset_type", OSD.FromString(Utils.AssetTypeToString(assetType))}, {"inventory_type", OSD.FromString(Utils.InventoryTypeToString(invType))}, {"name", OSD.FromString(name)}, {"description", OSD.FromString(description)}, {"everyone_mask", OSD.FromInteger((int) permissions.EveryoneMask)}, {"group_mask", OSD.FromInteger((int) permissions.GroupMask)}, {"next_owner_mask", OSD.FromInteger((int) permissions.NextOwnerMask)}, {"expected_upload_cost", OSD.FromInteger(Client.Settings.UPLOAD_COST)} }; // Make the request request.OnComplete += CreateItemFromAssetResponse; request.UserData = new object[] { callback, data, Client.Settings.CAPS_TIMEOUT, query }; request.BeginGetResponse(query, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } else { throw new Exception("NewFileAgentInventory capability is not currently available"); } } /// /// Creates inventory link to another inventory item or folder /// /// Put newly created link in folder with this UUID /// Inventory item or folder /// Method to call upon creation of the link public void CreateLink(UUID folderID, InventoryBase bse, ItemCreatedCallback callback) { if (bse is InventoryFolder folder) { CreateLink(folderID, folder, callback); } else if (bse is InventoryItem item) { CreateLink(folderID, item.UUID, item.Name, item.Description, AssetType.Link, item.InventoryType, UUID.Random(), callback); } } /// /// Creates inventory link to another inventory item /// /// Put newly created link in folder with this UUID /// Original inventory item /// Method to call upon creation of the link public void CreateLink(UUID folderID, InventoryItem item, ItemCreatedCallback callback) { CreateLink(folderID, item.UUID, item.Name, item.Description, AssetType.Link, item.InventoryType, UUID.Random(), callback); } /// /// Creates inventory link to another inventory folder /// /// Put newly created link in folder with this UUID /// Original inventory folder /// Method to call upon creation of the link public void CreateLink(UUID folderID, InventoryFolder folder, ItemCreatedCallback callback) { CreateLink(folderID, folder.UUID, folder.Name, "", AssetType.LinkFolder, InventoryType.Folder, UUID.Random(), callback); } /// /// Creates inventory link to another inventory item or folder /// /// Put newly created link in folder with this UUID /// Original item's UUID /// Name /// Description /// Asset Type /// Inventory Type /// Transaction UUID /// Method to call upon creation of the link public void CreateLink(UUID folderID, UUID itemID, string name, string description, AssetType assetType, InventoryType invType, UUID transactionID, ItemCreatedCallback callback) { if (Client.AisClient.IsAvailable) { OSDArray links = new OSDArray(); OSDMap link = new OSDMap { ["linked_id"] = OSD.FromUUID(itemID), ["type"] = OSD.FromInteger((int)assetType), ["inv_type"] = OSD.FromInteger((int)invType), ["name"] = OSD.FromString(name), ["desc"] = OSD.FromString(description) }; links.Add(link); OSDMap newInventory = new OSDMap {{"links", links}}; Client.AisClient.CreateInventory(folderID, newInventory, true, callback) .ConfigureAwait(false); } else { var create = new LinkInventoryItemPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryBlock = {CallbackID = RegisterItemCreatedCallback(callback)} }; lock (_ItemInventoryTypeRequest) { _ItemInventoryTypeRequest[create.InventoryBlock.CallbackID] = invType; } create.InventoryBlock.FolderID = folderID; create.InventoryBlock.TransactionID = transactionID; create.InventoryBlock.OldItemID = itemID; create.InventoryBlock.Type = (sbyte) assetType; create.InventoryBlock.InvType = (sbyte) invType; create.InventoryBlock.Name = Utils.StringToBytes(name); create.InventoryBlock.Description = Utils.StringToBytes(description); Client.Network.SendPacket(create); } } #endregion Create #region Copy /// /// /// /// /// /// /// public void RequestCopyItem(UUID item, UUID newParent, string newName, ItemCopiedCallback callback) { RequestCopyItem(item, newParent, newName, Client.Self.AgentID, callback); } /// /// /// /// /// /// /// /// public void RequestCopyItem(UUID item, UUID newParent, string newName, UUID oldOwnerID, ItemCopiedCallback callback) { List items = new List(1) {item}; List folders = new List(1) {newParent}; List names = new List(1) {newName}; RequestCopyItems(items, folders, names, oldOwnerID, callback); } /// /// /// /// /// /// /// /// public void RequestCopyItems(List items, List targetFolders, List newNames, UUID oldOwnerID, ItemCopiedCallback callback) { if (items.Count != targetFolders.Count || (newNames != null && items.Count != newNames.Count)) throw new ArgumentException("All list arguments must have an equal number of entries"); uint callbackID = RegisterItemsCopiedCallback(callback); var copy = new CopyInventoryItemPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryData = new CopyInventoryItemPacket.InventoryDataBlock[items.Count] }; for (int i = 0; i < items.Count; ++i) { copy.InventoryData[i] = new CopyInventoryItemPacket.InventoryDataBlock { CallbackID = callbackID, NewFolderID = targetFolders[i], OldAgentID = oldOwnerID, OldItemID = items[i], NewName = !String.IsNullOrEmpty(newNames?[i]) ? Utils.StringToBytes(newNames[i]) : Utils.EmptyBytes }; } Client.Network.SendPacket(copy); } /// /// Request a copy of an asset embedded within a notecard /// /// Usually UUID.Zero for copying an asset from a notecard /// UUID of the notecard to request an asset from /// Target folder for asset to go to in your inventory /// UUID of the embedded asset /// callback to run when item is copied to inventory public void RequestCopyItemFromNotecard(UUID objectID, UUID notecardID, UUID folderID, UUID itemID, ItemCopiedCallback callback) { _ItemCopiedCallbacks[0] = callback; //Notecards always use callback ID 0 CapsClient request = Client.Network.CurrentSim.Caps.CreateCapsClient("CopyInventoryFromNotecard"); if (request != null) { var message = new CopyInventoryFromNotecardMessage { CallbackID = 0, FolderID = folderID, ItemID = itemID, NotecardID = notecardID, ObjectID = objectID }; request.BeginGetResponse(message.Serialize(), OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } else { var copy = new CopyInventoryFromNotecardPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, NotecardData = { ObjectID = objectID, NotecardItemID = notecardID }, InventoryData = new CopyInventoryFromNotecardPacket.InventoryDataBlock[1] }; copy.InventoryData[0] = new CopyInventoryFromNotecardPacket.InventoryDataBlock { FolderID = folderID, ItemID = itemID }; Client.Network.SendPacket(copy); } } #endregion Copy #region Update /// /// /// /// public void RequestUpdateItem(InventoryItem item) { List items = new List(1) {item}; RequestUpdateItems(items, UUID.Random()); } /// /// /// /// public void RequestUpdateItems(List items) { RequestUpdateItems(items, UUID.Random()); } /// /// /// /// /// public void RequestUpdateItems(List items, UUID transactionID) { if (Client.AisClient.IsAvailable) { foreach (var item in items) { OSDMap update = (OSDMap)item.GetOSD(); if (update.ContainsKey("asset_id")) { update.Remove("asset_id"); if (item.TransactionID != UUID.Zero) { update["hash_id"] = item.TransactionID; } } if (update.ContainsKey("shadow_id")) { update.Remove("shadow_id"); if (item.TransactionID != UUID.Zero) { update["hash_id"] = item.TransactionID; } } Client.AisClient.UpdateItem(item.UUID, update, null).ConfigureAwait(false); } } else { var update = new UpdateInventoryItemPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, TransactionID = transactionID }, InventoryData = new UpdateInventoryItemPacket.InventoryDataBlock[items.Count] }; for (int i = 0; i < items.Count; i++) { InventoryItem item = items[i]; var block = new UpdateInventoryItemPacket.InventoryDataBlock { BaseMask = (uint) item.Permissions.BaseMask, CRC = ItemCRC(item), CreationDate = (int) Utils.DateTimeToUnixTime(item.CreationDate), CreatorID = item.CreatorID, Description = Utils.StringToBytes(item.Description), EveryoneMask = (uint) item.Permissions.EveryoneMask, Flags = (uint) item.Flags, FolderID = item.ParentUUID, GroupID = item.GroupID, GroupMask = (uint) item.Permissions.GroupMask, GroupOwned = item.GroupOwned, InvType = (sbyte) item.InventoryType, ItemID = item.UUID, Name = Utils.StringToBytes(item.Name), NextOwnerMask = (uint) item.Permissions.NextOwnerMask, OwnerID = item.OwnerID, OwnerMask = (uint) item.Permissions.OwnerMask, SalePrice = item.SalePrice, SaleType = (byte) item.SaleType, TransactionID = item.TransactionID, Type = (sbyte) item.AssetType }; update.InventoryData[i] = block; } Client.Network.SendPacket(update); } } /// /// /// /// /// /// public void RequestUploadNotecardAsset(byte[] data, UUID notecardID, InventoryUploadedAssetCallback callback) { if (Client.Network.CurrentSim == null || Client.Network.CurrentSim.Caps == null) throw new Exception("UpdateNotecardAgentInventory capability is not currently available"); CapsClient request = Client.Network.CurrentSim.Caps.CreateCapsClient("UpdateNotecardAgentInventory"); if (request != null) { OSDMap query = new OSDMap {{"item_id", OSD.FromUUID(notecardID)}}; // Make the request request.OnComplete += UploadInventoryAssetResponse; request.UserData = new object[] { new KeyValuePair(callback, data), notecardID }; request.BeginGetResponse(query, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } else { throw new Exception("UpdateNotecardAgentInventory capability is not currently available"); } } /// /// Save changes to notecard embedded in object contents /// /// Encoded notecard asset data /// Notecard UUID /// Object's UUID /// Called upon finish of the upload with status information public void RequestUpdateNotecardTask(byte[] data, UUID notecardID, UUID taskID, InventoryUploadedAssetCallback callback) { if (Client.Network.CurrentSim == null || Client.Network.CurrentSim.Caps == null) throw new Exception("UpdateNotecardTaskInventory capability is not currently available"); CapsClient request = Client.Network.CurrentSim.Caps.CreateCapsClient("UpdateNotecardTaskInventory"); if (request != null) { OSDMap query = new OSDMap { {"item_id", OSD.FromUUID(notecardID)}, { "task_id", OSD.FromUUID(taskID)} }; // Make the request request.OnComplete += UploadInventoryAssetResponse; request.UserData = new object[] { new KeyValuePair(callback, data), notecardID }; request.BeginGetResponse(query, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } else { throw new Exception("UpdateNotecardTaskInventory capability is not currently available"); } } /// /// Upload new gesture asset for an inventory gesture item /// /// Encoded gesture asset /// Gesture inventory UUID /// Callback whick will be called when upload is complete public void RequestUploadGestureAsset(byte[] data, UUID gestureID, InventoryUploadedAssetCallback callback) { if (Client.Network.CurrentSim == null || Client.Network.CurrentSim.Caps == null) throw new Exception("UpdateGestureAgentInventory capability is not currently available"); CapsClient request = Client.Network.CurrentSim.Caps.CreateCapsClient("UpdateGestureAgentInventory"); if (request != null) { OSDMap query = new OSDMap {{"item_id", OSD.FromUUID(gestureID)}}; // Make the request request.OnComplete += UploadInventoryAssetResponse; request.UserData = new object[] { new KeyValuePair(callback, data), gestureID }; request.BeginGetResponse(query, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } else { throw new Exception("UpdateGestureAgentInventory capability is not currently available"); } } /// /// Update an existing script in an agents Inventory /// /// A byte[] array containing the encoded scripts contents /// the itemID of the script /// if true, sets the script content to run on the mono interpreter /// public void RequestUpdateScriptAgentInventory(byte[] data, UUID itemID, bool mono, ScriptUpdatedCallback callback) { CapsClient request = Client.Network.CurrentSim.Caps.CreateCapsClient("UpdateScriptAgent"); if (request != null) { var msg = new UpdateScriptAgentRequestMessage { ItemID = itemID, Target = mono ? "mono" : "lsl2" }; request.OnComplete += new CapsClient.CompleteCallback(UpdateScriptAgentInventoryResponse); request.UserData = new object[2] { new KeyValuePair(callback, data), itemID }; request.BeginGetResponse(msg.Serialize(), OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } else { throw new Exception("UpdateScriptAgent capability is not currently available"); } } /// /// Update an existing script in an task Inventory /// /// A byte[] array containing the encoded scripts contents /// the itemID of the script /// UUID of the prim containting the script /// if true, sets the script content to run on the mono interpreter /// if true, sets the script to running /// public void RequestUpdateScriptTask(byte[] data, UUID itemID, UUID taskID, bool mono, bool running, ScriptUpdatedCallback callback) { CapsClient request = Client.Network.CurrentSim.Caps.CreateCapsClient("UpdateScriptTask"); if (request != null) { var msg = new UpdateScriptTaskUpdateMessage { ItemID = itemID, TaskID = taskID, ScriptRunning = running, Target = mono ? "mono" : "lsl2" }; request.OnComplete += new CapsClient.CompleteCallback(UpdateScriptAgentInventoryResponse); request.UserData = new object[2] { new KeyValuePair(callback, data), itemID }; request.BeginGetResponse(msg.Serialize(), OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); } else { throw new Exception("UpdateScriptTask capability is not currently available"); } } #endregion Update #region Rez/Give /// /// Rez an object from inventory /// /// Simulator to place object in /// Rotation of the object when rezzed /// Vector of where to place object /// InventoryItem object containing item details public UUID RequestRezFromInventory(Simulator simulator, Quaternion rotation, Vector3 position, InventoryItem item) { return RequestRezFromInventory(simulator, rotation, position, item, Client.Self.ActiveGroup, UUID.Random(), true); } /// /// Rez an object from inventory /// /// Simulator to place object in /// Rotation of the object when rezzed /// Vector of where to place object /// InventoryItem object containing item details /// UUID of group to own the object public UUID RequestRezFromInventory(Simulator simulator, Quaternion rotation, Vector3 position, InventoryItem item, UUID groupOwner) { return RequestRezFromInventory(simulator, rotation, position, item, groupOwner, UUID.Random(), true); } /// /// Rez an object from inventory /// /// Simulator to place object in /// Rotation of the object when rezzed /// Vector of where to place object /// InventoryItem object containing item details /// UUID of group to own the object /// User defined queryID to correlate replies /// If set to true, the CreateSelected flag /// will be set on the rezzed object public UUID RequestRezFromInventory(Simulator simulator, Quaternion rotation, Vector3 position, InventoryItem item, UUID groupOwner, UUID queryID, bool rezSelected) { return RequestRezFromInventory(simulator, UUID.Zero, rotation, position, item, groupOwner, queryID, rezSelected); } /// /// Rez an object from inventory /// /// Simulator to place object in /// TaskID object when rezzed /// Rotation of the object when rezzed /// Vector of where to place object /// InventoryItem object containing item details /// UUID of group to own the object /// User defined queryID to correlate replies /// If set to true, the CreateSelected flag /// will be set on the rezzed object public UUID RequestRezFromInventory(Simulator simulator, UUID taskID, Quaternion rotation, Vector3 position, InventoryItem item, UUID groupOwner, UUID queryID, bool rezSelected) { var add = new RezObjectPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, GroupID = groupOwner }, RezData = { FromTaskID = taskID, BypassRaycast = 1, RayStart = position, RayEnd = position, RayTargetID = UUID.Zero, RayEndIsIntersection = false, RezSelected = rezSelected, RemoveItem = false, ItemFlags = (uint) item.Flags, GroupMask = (uint) item.Permissions.GroupMask, EveryoneMask = (uint) item.Permissions.EveryoneMask, NextOwnerMask = (uint) item.Permissions.NextOwnerMask }, InventoryData = { ItemID = item.UUID, FolderID = item.ParentUUID, CreatorID = item.CreatorID, OwnerID = item.OwnerID, GroupID = item.GroupID, BaseMask = (uint) item.Permissions.BaseMask, OwnerMask = (uint) item.Permissions.OwnerMask, GroupMask = (uint) item.Permissions.GroupMask, EveryoneMask = (uint) item.Permissions.EveryoneMask, NextOwnerMask = (uint) item.Permissions.NextOwnerMask, GroupOwned = item.GroupOwned, TransactionID = queryID, Type = (sbyte) item.InventoryType, InvType = (sbyte) item.InventoryType, Flags = (uint) item.Flags, SaleType = (byte) item.SaleType, SalePrice = item.SalePrice, Name = Utils.StringToBytes(item.Name), Description = Utils.StringToBytes(item.Description), CreationDate = (int) Utils.DateTimeToUnixTime(item.CreationDate) } }; Client.Network.SendPacket(add, simulator); // Remove from store if the item is no copy if (Store.Items.ContainsKey(item.UUID) && Store[item.UUID] is InventoryItem) { InventoryItem invItem = (InventoryItem)Store[item.UUID]; if ((invItem.Permissions.OwnerMask & PermissionMask.Copy) == PermissionMask.None) { Store.RemoveNodeFor(invItem); } } return queryID; } /// /// DeRez an object from the simulator to the agents Objects folder in the agents Inventory /// /// The simulator Local ID of the object /// If objectLocalID is a child primitive in a linkset, the entire linkset will be derezzed public void RequestDeRezToInventory(uint objectLocalID) { RequestDeRezToInventory(objectLocalID, DeRezDestination.AgentInventoryTake, Client.Inventory.FindFolderForType(AssetType.Object), UUID.Random()); } /// /// DeRez an object from the simulator and return to inventory /// /// The simulator Local ID of the object /// The type of destination from the enum /// The destination inventory folders -or- /// if DeRezzing object to a tasks Inventory, the Tasks /// The transaction ID for this request which /// can be used to correlate this request with other packets /// If objectLocalID is a child primitive in a linkset, the entire linkset will be derezzed public void RequestDeRezToInventory(uint objectLocalID, DeRezDestination destType, UUID destFolder, UUID transactionID) { var take = new DeRezObjectPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, AgentBlock = new DeRezObjectPacket.AgentBlockBlock { GroupID = UUID.Zero, Destination = (byte) destType, DestinationID = destFolder, PacketCount = 1, PacketNumber = 1, TransactionID = transactionID }, ObjectData = new DeRezObjectPacket.ObjectDataBlock[1] }; take.ObjectData[0] = new DeRezObjectPacket.ObjectDataBlock {ObjectLocalID = objectLocalID}; Client.Network.SendPacket(take); } /// /// Rez an item from inventory to its previous simulator location /// /// /// /// /// public UUID RequestRestoreRezFromInventory(Simulator simulator, InventoryItem item, UUID queryID) { var add = new RezRestoreToWorldPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryData = { ItemID = item.UUID, FolderID = item.ParentUUID, CreatorID = item.CreatorID, OwnerID = item.OwnerID, GroupID = item.GroupID, BaseMask = (uint) item.Permissions.BaseMask, OwnerMask = (uint) item.Permissions.OwnerMask, GroupMask = (uint) item.Permissions.GroupMask, EveryoneMask = (uint) item.Permissions.EveryoneMask, NextOwnerMask = (uint) item.Permissions.NextOwnerMask, GroupOwned = item.GroupOwned, TransactionID = queryID, Type = (sbyte) item.InventoryType, InvType = (sbyte) item.InventoryType, Flags = (uint) item.Flags, SaleType = (byte) item.SaleType, SalePrice = item.SalePrice, Name = Utils.StringToBytes(item.Name), Description = Utils.StringToBytes(item.Description), CreationDate = (int) Utils.DateTimeToUnixTime(item.CreationDate) } }; Client.Network.SendPacket(add, simulator); return queryID; } /// /// Give an inventory item to another avatar /// /// The of the item to give /// The name of the item /// The type of the item from the enum /// The of the recipient /// true to generate a beameffect during transfer public void GiveItem(UUID itemID, string itemName, AssetType assetType, UUID recipient, bool doEffect) { var bucket = new byte[17]; bucket[0] = (byte)assetType; Buffer.BlockCopy(itemID.GetBytes(), 0, bucket, 1, 16); Client.Self.InstantMessage( Client.Self.Name, recipient, itemName, UUID.Random(), InstantMessageDialog.InventoryOffered, InstantMessageOnline.Online, Client.Self.SimPosition, Client.Network.CurrentSim.ID, bucket); if (doEffect) { Client.Self.BeamEffect(Client.Self.AgentID, recipient, Vector3d.Zero, Client.Settings.DEFAULT_EFFECT_COLOR, 1f, UUID.Random()); } // Remove from store if the item is no copy if (Store.Items.ContainsKey(itemID) && Store[itemID] is InventoryItem) { InventoryItem invItem = (InventoryItem)Store[itemID]; if ((invItem.Permissions.OwnerMask & PermissionMask.Copy) == PermissionMask.None) { Store.RemoveNodeFor(invItem); } } } /// /// Give an inventory Folder with contents to another avatar /// /// The of the Folder to give /// The name of the folder /// The type of the item from the enum /// The of the recipient /// true to generate a beameffect during transfer public void GiveFolder(UUID folderID, string folderName, AssetType assetType, UUID recipient, bool doEffect) { List folderContents = new List(); Client.Inventory.FolderContents(folderID, Client.Self.AgentID, false, true, InventorySortOrder.ByDate, 1000 * 15).ForEach( delegate(InventoryBase ib) { folderContents.Add(Client.Inventory.FetchItem(ib.UUID, Client.Self.AgentID, 1000 * 10)); }); var bucket = new byte[17 * (folderContents.Count + 1)]; //Add parent folder (first item in bucket) bucket[0] = (byte)assetType; Buffer.BlockCopy(folderID.GetBytes(), 0, bucket, 1, 16); //Add contents to bucket after folder for (int i = 1; i <= folderContents.Count; ++i) { bucket[i * 17] = (byte)folderContents[i - 1].AssetType; Buffer.BlockCopy(folderContents[i - 1].UUID.GetBytes(), 0, bucket, i * 17 + 1, 16); } Client.Self.InstantMessage( Client.Self.Name, recipient, folderName, UUID.Random(), InstantMessageDialog.InventoryOffered, InstantMessageOnline.Online, Client.Self.SimPosition, Client.Network.CurrentSim.ID, bucket); if (doEffect) { Client.Self.BeamEffect(Client.Self.AgentID, recipient, Vector3d.Zero, Client.Settings.DEFAULT_EFFECT_COLOR, 1f, UUID.Random()); } // Remove from store if items were no copy foreach (InventoryItem item in folderContents) { if (Store.Items.ContainsKey(item.UUID) && Store[item.UUID] is InventoryItem) { InventoryItem invItem = (InventoryItem)Store[item.UUID]; if ((invItem.Permissions.OwnerMask & PermissionMask.Copy) == PermissionMask.None) { Store.RemoveNodeFor(invItem); } } } } #endregion Rez/Give #region Task /// /// Copy or move an from agent inventory to a task (primitive) inventory /// /// The target object /// The item to copy or move from inventory /// /// For items with copy permissions a copy of the item is placed in the tasks inventory, /// for no-copy items the object is moved to the tasks inventory // DocTODO: what does the return UUID correlate to if anything? public UUID UpdateTaskInventory(uint objectLocalID, InventoryItem item) { UUID transactionID = UUID.Random(); var update = new UpdateTaskInventoryPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, UpdateData = { Key = 0, LocalID = objectLocalID }, InventoryData = { ItemID = item.UUID, FolderID = item.ParentUUID, CreatorID = item.CreatorID, OwnerID = item.OwnerID, GroupID = item.GroupID, BaseMask = (uint) item.Permissions.BaseMask, OwnerMask = (uint) item.Permissions.OwnerMask, GroupMask = (uint) item.Permissions.GroupMask, EveryoneMask = (uint) item.Permissions.EveryoneMask, NextOwnerMask = (uint) item.Permissions.NextOwnerMask, GroupOwned = item.GroupOwned, TransactionID = transactionID, Type = (sbyte) item.AssetType, InvType = (sbyte) item.InventoryType, Flags = (uint) item.Flags, SaleType = (byte) item.SaleType, SalePrice = item.SalePrice, Name = Utils.StringToBytes(item.Name), Description = Utils.StringToBytes(item.Description), CreationDate = (int) Utils.DateTimeToUnixTime(item.CreationDate), CRC = ItemCRC(item) } }; Client.Network.SendPacket(update); return transactionID; } /// /// Retrieve a listing of the items contained in a task (Primitive) /// /// The tasks /// The tasks simulator local ID /// milliseconds to wait for reply from simulator /// A list containing the inventory items inside the task or null /// if a timeout occurs /// This request blocks until the response from the simulator arrives /// or timeoutMS is exceeded public List GetTaskInventory(UUID objectID, UInt32 objectLocalID, Int32 timeoutMS) { String filename = null; AutoResetEvent taskReplyEvent = new AutoResetEvent(false); EventHandler callback = delegate(object sender, TaskInventoryReplyEventArgs e) { if (e.ItemID == objectID) { filename = e.AssetFilename; taskReplyEvent.Set(); } }; TaskInventoryReply += callback; RequestTaskInventory(objectLocalID); if (taskReplyEvent.WaitOne(timeoutMS, false)) { TaskInventoryReply -= callback; if (!String.IsNullOrEmpty(filename)) { byte[] assetData = null; ulong xferID = 0; AutoResetEvent taskDownloadEvent = new AutoResetEvent(false); EventHandler xferCallback = delegate(object sender, XferReceivedEventArgs e) { if (e.Xfer.XferID == xferID) { assetData = e.Xfer.AssetData; taskDownloadEvent.Set(); } }; Client.Assets.XferReceived += xferCallback; // Start the actual asset xfer xferID = Client.Assets.RequestAssetXfer(filename, true, false, UUID.Zero, AssetType.Unknown, true); if (taskDownloadEvent.WaitOne(timeoutMS, false)) { Client.Assets.XferReceived -= xferCallback; String taskList = Utils.BytesToString(assetData); return ParseTaskInventory(taskList); } else { Logger.Log("Timed out waiting for task inventory download for " + filename, Helpers.LogLevel.Warning, Client); Client.Assets.XferReceived -= xferCallback; return null; } } else { Logger.DebugLog("Task is empty for " + objectLocalID, Client); return new List(0); } } else { Logger.Log("Timed out waiting for task inventory reply for " + objectLocalID, Helpers.LogLevel.Warning, Client); TaskInventoryReply -= callback; return null; } } /// /// Request the contents of a tasks (primitives) inventory from the /// current simulator /// /// The LocalID of the object /// public void RequestTaskInventory(uint objectLocalID) { RequestTaskInventory(objectLocalID, Client.Network.CurrentSim); } /// /// Request the contents of a tasks (primitives) inventory /// /// The simulator Local ID of the object /// A reference to the simulator object that contains the object /// public void RequestTaskInventory(uint objectLocalID, Simulator simulator) { var request = new RequestTaskInventoryPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryData = {LocalID = objectLocalID} }; Client.Network.SendPacket(request, simulator); } /// /// Move an item from a tasks (Primitive) inventory to the specified folder in the avatars inventory /// /// LocalID of the object in the simulator /// UUID of the task item to move /// The ID of the destination folder in this agents inventory /// Simulator Object /// Raises the event public void MoveTaskInventory(uint objectLocalID, UUID taskItemID, UUID inventoryFolderID, Simulator simulator) { var request = new MoveTaskInventoryPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, FolderID = inventoryFolderID }, InventoryData = { ItemID = taskItemID, LocalID = objectLocalID } }; Client.Network.SendPacket(request, simulator); } /// /// Remove an item from an objects (Prim) Inventory /// /// LocalID of the object in the simulator /// UUID of the task item to remove /// Simulator Object /// You can confirm the removal by comparing the tasks inventory serial before and after the /// request with the request combined with /// the event public void RemoveTaskInventory(uint objectLocalID, UUID taskItemID, Simulator simulator) { var remove = new RemoveTaskInventoryPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, InventoryData = { ItemID = taskItemID, LocalID = objectLocalID } }; Client.Network.SendPacket(remove, simulator); } /// /// Copy an InventoryScript item from the Agents Inventory into a primitives task inventory /// /// An unsigned integer representing a primitive being simulated /// An which represents a script object from the agents inventory /// true to set the scripts running state to enabled /// A Unique Transaction ID /// /// The following example shows the basic steps necessary to copy a script from the agents inventory into a tasks inventory /// and assumes the script exists in the agents inventory. /// /// uint primID = 95899503; // Fake prim ID /// UUID scriptID = UUID.Parse("92a7fe8a-e949-dd39-a8d8-1681d8673232"); // Fake Script UUID in Inventory /// /// Client.Inventory.FolderContents(Client.Inventory.FindFolderForType(AssetType.LSLText), Client.Self.AgentID, /// false, true, InventorySortOrder.ByName, 10000); /// /// Client.Inventory.RezScript(primID, (InventoryItem)Client.Inventory.Store[scriptID]); /// /// // DocTODO: what does the return UUID correlate to if anything? public UUID CopyScriptToTask(uint objectLocalID, InventoryItem item, bool enableScript) { UUID transactionID = UUID.Random(); var ScriptPacket = new RezScriptPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, UpdateBlock = { ObjectLocalID = objectLocalID, Enabled = enableScript }, InventoryBlock = { ItemID = item.UUID, FolderID = item.ParentUUID, CreatorID = item.CreatorID, OwnerID = item.OwnerID, GroupID = item.GroupID, BaseMask = (uint) item.Permissions.BaseMask, OwnerMask = (uint) item.Permissions.OwnerMask, GroupMask = (uint) item.Permissions.GroupMask, EveryoneMask = (uint) item.Permissions.EveryoneMask, NextOwnerMask = (uint) item.Permissions.NextOwnerMask, GroupOwned = item.GroupOwned, TransactionID = transactionID, Type = (sbyte) item.AssetType, InvType = (sbyte) item.InventoryType, Flags = (uint) item.Flags, SaleType = (byte) item.SaleType, SalePrice = item.SalePrice, Name = Utils.StringToBytes(item.Name), Description = Utils.StringToBytes(item.Description), CreationDate = (int) Utils.DateTimeToUnixTime(item.CreationDate), CRC = ItemCRC(item) } }; Client.Network.SendPacket(ScriptPacket); return transactionID; } /// /// Request the running status of a script contained in a task (primitive) inventory /// /// The ID of the primitive containing the script /// The ID of the script /// The event can be used to obtain the results of the /// request /// public void RequestGetScriptRunning(UUID objectID, UUID scriptID) { var request = new GetScriptRunningPacket { Script = { ObjectID = objectID, ItemID = scriptID } }; Client.Network.SendPacket(request); } /// /// Send a request to set the running state of a script contained in a task (primitive) inventory /// /// The ID of the primitive containing the script /// The ID of the script /// true to set the script running, false to stop a running script /// To verify the change you can use the method combined /// with the event public void RequestSetScriptRunning(UUID objectID, UUID scriptID, bool running) { SetScriptRunningPacket request = new SetScriptRunningPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, Script = { Running = running, ItemID = scriptID, ObjectID = objectID } }; Client.Network.SendPacket(request); } #endregion Task #region Helper Functions private uint RegisterItemCreatedCallback(ItemCreatedCallback callback) { lock (_CallbacksLock) { if (_CallbackPos == UInt32.MaxValue) _CallbackPos = 0; _CallbackPos++; if (_ItemCreatedCallbacks.ContainsKey(_CallbackPos)) Logger.Log("Overwriting an existing ItemCreatedCallback", Helpers.LogLevel.Warning, Client); _ItemCreatedCallbacks[_CallbackPos] = callback; return _CallbackPos; } } private uint RegisterItemsCopiedCallback(ItemCopiedCallback callback) { lock (_CallbacksLock) { if (_CallbackPos == UInt32.MaxValue) _CallbackPos = 0; _CallbackPos++; if (_ItemCopiedCallbacks.ContainsKey(_CallbackPos)) Logger.Log("Overwriting an existing ItemsCopiedCallback", Helpers.LogLevel.Warning, Client); _ItemCopiedCallbacks[_CallbackPos] = callback; return _CallbackPos; } } /// /// Create a CRC from an InventoryItem /// /// The source InventoryItem /// A uint representing the source InventoryItem as a CRC public static uint ItemCRC(InventoryItem iitem) { uint CRC = 0; // IDs CRC += iitem.AssetUUID.CRC(); // AssetID CRC += iitem.ParentUUID.CRC(); // FolderID CRC += iitem.UUID.CRC(); // ItemID // Permission stuff CRC += iitem.CreatorID.CRC(); // CreatorID CRC += iitem.OwnerID.CRC(); // OwnerID CRC += iitem.GroupID.CRC(); // GroupID // CRC += another 4 words which always seem to be zero -- unclear if this is a UUID or what CRC += (uint)iitem.Permissions.OwnerMask; //owner_mask; // Either owner_mask or next_owner_mask may need to be CRC += (uint)iitem.Permissions.NextOwnerMask; //next_owner_mask; // switched with base_mask -- 2 values go here and in my CRC += (uint)iitem.Permissions.EveryoneMask; //everyone_mask; // study item, the three were identical. CRC += (uint)iitem.Permissions.GroupMask; //group_mask; // The rest of the CRC fields CRC += (uint)iitem.Flags; // Flags CRC += (uint)iitem.InventoryType; // InvType CRC += (uint)iitem.AssetType; // Type CRC += (uint)Utils.DateTimeToUnixTime(iitem.CreationDate); // CreationDate CRC += (uint)iitem.SalePrice; // SalePrice CRC += (uint)((uint)iitem.SaleType * 0x07073096); // SaleType return CRC; } /// /// Reverses a cheesy XORing with a fixed UUID to convert a shadow_id to an asset_id /// /// Obfuscated shadow_id value /// Deobfuscated asset_id value public static UUID DecryptShadowID(UUID shadowID) { return shadowID ^ MAGIC_ID; } /// /// Does a cheesy XORing with a fixed UUID to convert an asset_id to a shadow_id /// /// asset_id value to obfuscate /// Obfuscated shadow_id value public static UUID EncryptAssetID(UUID assetID) { return assetID ^ MAGIC_ID; } /// /// Wrapper for creating a new object /// /// The type of item from the enum /// The of the newly created object /// An object with the type and id passed public static InventoryItem CreateInventoryItem(InventoryType type, UUID id) { switch (type) { case InventoryType.Texture: return new InventoryTexture(id); case InventoryType.Sound: return new InventorySound(id); case InventoryType.CallingCard: return new InventoryCallingCard(id); case InventoryType.Landmark: return new InventoryLandmark(id); case InventoryType.Object: return new InventoryObject(id); case InventoryType.Notecard: return new InventoryNotecard(id); case InventoryType.Category: return new InventoryCategory(id); case InventoryType.LSL: return new InventoryLSL(id); case InventoryType.Snapshot: return new InventorySnapshot(id); case InventoryType.Attachment: return new InventoryAttachment(id); case InventoryType.Wearable: return new InventoryWearable(id); case InventoryType.Animation: return new InventoryAnimation(id); case InventoryType.Gesture: return new InventoryGesture(id); default: return new InventoryItem(type, id); } } public InventoryItem SafeCreateInventoryItem(InventoryType InvType, UUID ItemID) { InventoryItem ret = null; if (_Store.Contains(ItemID)) ret = _Store[ItemID] as InventoryItem; return ret ?? (ret = CreateInventoryItem(InvType, ItemID)); } private static bool ParseLine(string line, out string key, out string value) { // Clean up and convert tabs to spaces line = line.Trim(); line = line.Replace('\t', ' '); // Shrink all whitespace down to single spaces while (line.IndexOf(" ", StringComparison.Ordinal) > 0) line = line.Replace(" ", " "); if (line.Length > 2) { int sep = line.IndexOf(' '); if (sep > 0) { key = line.Substring(0, sep); value = line.Substring(sep + 1); return true; } } else if (line.Length == 1) { key = line; value = String.Empty; return true; } key = null; value = null; return false; } /// /// Parse the results of a RequestTaskInventory() response /// /// A string which contains the data from the task reply /// A List containing the items contained within the tasks inventory public static List ParseTaskInventory(string taskData) { List items = new List(); int lineNum = 0; string[] lines = taskData.Replace("\r\n", "\n").Split('\n'); while (lineNum < lines.Length) { string key, value; if (ParseLine(lines[lineNum++], out key, out value)) { if (key == "inv_object") { #region inv_object // In practice this appears to only be used for folders UUID itemID = UUID.Zero; UUID parentID = UUID.Zero; string name = String.Empty; AssetType assetType = AssetType.Unknown; while (lineNum < lines.Length) { if (ParseLine(lines[lineNum++], out key, out value)) { if (key == "{") { continue; } else if (key == "}") { break; } else if (key == "obj_id") { UUID.TryParse(value, out itemID); } else if (key == "parent_id") { UUID.TryParse(value, out parentID); } else if (key == "type") { assetType = Utils.StringToAssetType(value); } else if (key == "name") { name = value.Substring(0, value.IndexOf('|')); } } } if (assetType == AssetType.Folder) { InventoryFolder folder = new InventoryFolder(itemID) { Name = name, ParentUUID = parentID }; items.Add(folder); } else { InventoryItem item = new InventoryItem(itemID) { Name = name, ParentUUID = parentID, AssetType = assetType }; items.Add(item); } #endregion inv_object } else if (key == "inv_item") { #region inv_item // Any inventory item that links to an assetID, has permissions, etc UUID itemID = UUID.Zero; UUID assetID = UUID.Zero; UUID parentID = UUID.Zero; UUID creatorID = UUID.Zero; UUID ownerID = UUID.Zero; UUID lastOwnerID = UUID.Zero; UUID groupID = UUID.Zero; bool groupOwned = false; string name = String.Empty; string desc = String.Empty; AssetType assetType = AssetType.Unknown; InventoryType inventoryType = InventoryType.Unknown; DateTime creationDate = Utils.Epoch; uint flags = 0; Permissions perms = Permissions.NoPermissions; SaleType saleType = SaleType.Not; int salePrice = 0; while (lineNum < lines.Length) { if (ParseLine(lines[lineNum++], out key, out value)) { if (key == "{") { continue; } else if (key == "}") { break; } else if (key == "item_id") { UUID.TryParse(value, out itemID); } else if (key == "parent_id") { UUID.TryParse(value, out parentID); } else if (key == "permissions") { #region permissions while (lineNum < lines.Length) { if (ParseLine(lines[lineNum++], out key, out value)) { if (key == "{") { continue; } else if (key == "}") { break; } else if (key == "creator_mask") { // Deprecated uint val; if (Utils.TryParseHex(value, out val)) perms.BaseMask = (PermissionMask)val; } else if (key == "base_mask") { uint val; if (Utils.TryParseHex(value, out val)) perms.BaseMask = (PermissionMask)val; } else if (key == "owner_mask") { uint val; if (Utils.TryParseHex(value, out val)) perms.OwnerMask = (PermissionMask)val; } else if (key == "group_mask") { uint val; if (Utils.TryParseHex(value, out val)) perms.GroupMask = (PermissionMask)val; } else if (key == "everyone_mask") { uint val; if (Utils.TryParseHex(value, out val)) perms.EveryoneMask = (PermissionMask)val; } else if (key == "next_owner_mask") { uint val; if (Utils.TryParseHex(value, out val)) perms.NextOwnerMask = (PermissionMask)val; } else if (key == "creator_id") { UUID.TryParse(value, out creatorID); } else if (key == "owner_id") { UUID.TryParse(value, out ownerID); } else if (key == "last_owner_id") { UUID.TryParse(value, out lastOwnerID); } else if (key == "group_id") { UUID.TryParse(value, out groupID); } else if (key == "group_owned") { uint val; if (UInt32.TryParse(value, out val)) groupOwned = (val != 0); } } } #endregion permissions } else if (key == "sale_info") { #region sale_info while (lineNum < lines.Length) { if (ParseLine(lines[lineNum++], out key, out value)) { if (key == "{") { continue; } else if (key == "}") { break; } else if (key == "sale_type") { saleType = Utils.StringToSaleType(value); } else if (key == "sale_price") { Int32.TryParse(value, out salePrice); } } } #endregion sale_info } else if (key == "shadow_id") { UUID shadowID; if (UUID.TryParse(value, out shadowID)) assetID = DecryptShadowID(shadowID); } else if (key == "asset_id") { UUID.TryParse(value, out assetID); } else if (key == "type") { assetType = Utils.StringToAssetType(value); } else if (key == "inv_type") { inventoryType = Utils.StringToInventoryType(value); } else if (key == "flags") { UInt32.TryParse(value, out flags); } else if (key == "name") { name = value.Substring(0, value.IndexOf('|')); } else if (key == "desc") { desc = value.Substring(0, value.IndexOf('|')); } else if (key == "creation_date") { uint timestamp; if (UInt32.TryParse(value, out timestamp)) creationDate = Utils.UnixTimeToDateTime(timestamp); else Logger.Log("Failed to parse creation_date " + value, Helpers.LogLevel.Warning); } } } InventoryItem item = CreateInventoryItem(inventoryType, itemID); item.AssetUUID = assetID; item.AssetType = assetType; item.CreationDate = creationDate; item.CreatorID = creatorID; item.Description = desc; item.Flags = flags; item.GroupID = groupID; item.GroupOwned = groupOwned; item.Name = name; item.OwnerID = ownerID; item.LastOwnerID = lastOwnerID; item.ParentUUID = parentID; item.Permissions = perms; item.SalePrice = salePrice; item.SaleType = saleType; items.Add(item); #endregion inv_item } else { Logger.Log("Unrecognized token " + key + " in: " + Environment.NewLine + taskData, Helpers.LogLevel.Error); } } } return items; } #endregion Helper Functions #region Internal Callbacks void Self_IM(object sender, InstantMessageEventArgs e) { // TODO: MainAvatar.InstantMessageDialog.GroupNotice can also be an inventory offer, should we // handle it here? if (m_InventoryObjectOffered != null && (e.IM.Dialog == InstantMessageDialog.InventoryOffered || e.IM.Dialog == InstantMessageDialog.TaskInventoryOffered)) { AssetType type = AssetType.Unknown; UUID objectID = UUID.Zero; bool fromTask = false; if (e.IM.Dialog == InstantMessageDialog.InventoryOffered) { if (e.IM.BinaryBucket.Length == 17) { type = (AssetType)e.IM.BinaryBucket[0]; objectID = new UUID(e.IM.BinaryBucket, 1); fromTask = false; } else { Logger.Log("Malformed inventory offer from agent", Helpers.LogLevel.Warning, Client); return; } } else if (e.IM.Dialog == InstantMessageDialog.TaskInventoryOffered) { if (e.IM.BinaryBucket.Length == 1) { type = (AssetType)e.IM.BinaryBucket[0]; fromTask = true; } else { Logger.Log("Malformed inventory offer from object", Helpers.LogLevel.Warning, Client); return; } } // Find the folder where this is going to go UUID destinationFolderID = FindFolderForType(type); // Fire the callback try { var imp = new ImprovedInstantMessagePacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, MessageBlock = { FromGroup = false, ToAgentID = e.IM.FromAgentID, Offline = 0, ID = e.IM.IMSessionID, Timestamp = 0, FromAgentName = Utils.StringToBytes(Client.Self.Name), Message = Utils.EmptyBytes, ParentEstateID = 0, RegionID = UUID.Zero, Position = Client.Self.SimPosition } }; InventoryObjectOfferedEventArgs args = new InventoryObjectOfferedEventArgs(e.IM, type, objectID, fromTask, destinationFolderID); OnInventoryObjectOffered(args); if (args.Accept) { // Accept the inventory offer switch (e.IM.Dialog) { case InstantMessageDialog.InventoryOffered: imp.MessageBlock.Dialog = (byte)InstantMessageDialog.InventoryAccepted; break; case InstantMessageDialog.TaskInventoryOffered: imp.MessageBlock.Dialog = (byte)InstantMessageDialog.TaskInventoryAccepted; break; case InstantMessageDialog.GroupNotice: imp.MessageBlock.Dialog = (byte)InstantMessageDialog.GroupNoticeInventoryAccepted; break; } imp.MessageBlock.BinaryBucket = args.FolderID.GetBytes(); } else { // Decline the inventory offer switch (e.IM.Dialog) { case InstantMessageDialog.InventoryOffered: imp.MessageBlock.Dialog = (byte)InstantMessageDialog.InventoryDeclined; break; case InstantMessageDialog.TaskInventoryOffered: imp.MessageBlock.Dialog = (byte)InstantMessageDialog.TaskInventoryDeclined; break; case InstantMessageDialog.GroupNotice: imp.MessageBlock.Dialog = (byte)InstantMessageDialog.GroupNoticeInventoryDeclined; break; } imp.MessageBlock.BinaryBucket = Utils.EmptyBytes; } Client.Network.SendPacket(imp, e.Simulator); } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, Client, ex); } } } private void CreateItemFromAssetResponse(CapsClient client, OSD result, Exception error) { object[] args = (object[])client.UserData; ItemCreatedFromAssetCallback callback = (ItemCreatedFromAssetCallback)args[0]; byte[] itemData = (byte[])args[1]; int millisecondsTimeout = (int)args[2]; OSDMap request = (OSDMap)args[3]; if (result == null) { try { callback(false, error.Message, UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } return; } if (result.Type == OSDType.Unknown) { try { callback(false, "Failed to parse asset and item UUIDs", UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } OSDMap contents = (OSDMap)result; string status = contents["state"].AsString().ToLower(); if (status == "upload") { string uploadURL = contents["uploader"].AsString(); Logger.DebugLog("CreateItemFromAsset: uploading to " + uploadURL); // This makes the assumption that all uploads go to CurrentSim, to avoid // the problem of HttpRequestState not knowing anything about simulators CapsClient upload = new CapsClient(new Uri(uploadURL), "CreateItemFromAsset"); upload.OnComplete += CreateItemFromAssetResponse; upload.UserData = new object[] { callback, itemData, millisecondsTimeout, request }; upload.BeginGetResponse(itemData, "application/octet-stream", millisecondsTimeout); } else if (status == "complete") { Logger.DebugLog("CreateItemFromAsset: completed"); if (contents.ContainsKey("new_inventory_item") && contents.ContainsKey("new_asset")) { // Request full update on the item in order to update the local store RequestFetchInventory(contents["new_inventory_item"].AsUUID(), Client.Self.AgentID); try { callback(true, String.Empty, contents["new_inventory_item"].AsUUID(), contents["new_asset"].AsUUID()); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } else { try { callback(false, "Failed to parse asset and item UUIDs", UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } else { // Failure try { callback(false, status, UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } private void Network_OnLoginResponse(bool loginSuccess, bool redirect, string message, string reason, LoginResponseData replyData) { if (!loginSuccess) return; // Initialize the store here so we know who owns it: _Store = new Inventory(Client, this, Client.Self.AgentID); Logger.DebugLog("Setting InventoryRoot to " + replyData.InventoryRoot.ToString(), Client); InventoryFolder rootFolder = new InventoryFolder(replyData.InventoryRoot) { Name = String.Empty, ParentUUID = UUID.Zero }; _Store.RootFolder = rootFolder; foreach (InventoryFolder folder in replyData.InventorySkeleton) _Store.UpdateNodeFor(folder); InventoryFolder libraryRootFolder = new InventoryFolder(replyData.LibraryRoot) { Name = string.Empty, ParentUUID = UUID.Zero }; _Store.LibraryFolder = libraryRootFolder; foreach (InventoryFolder folder in replyData.LibrarySkeleton) _Store.UpdateNodeFor(folder); } private void UploadInventoryAssetResponse(CapsClient client, OSD result, Exception error) { OSDMap contents = result as OSDMap; KeyValuePair kvp = (KeyValuePair)(((object[])client.UserData)[0]); InventoryUploadedAssetCallback callback = kvp.Key; byte[] itemData = (byte[])kvp.Value; if (error == null && contents != null) { string status = contents["state"].AsString(); if (status == "upload") { Uri uploadURL = contents["uploader"].AsUri(); if (uploadURL != null) { // This makes the assumption that all uploads go to CurrentSim, to avoid // the problem of HttpRequestState not knowing anything about simulators CapsClient upload = new CapsClient(uploadURL, "UploadItemResponse"); upload.OnComplete += UploadInventoryAssetResponse; upload.UserData = new object[2] { kvp, (UUID)(((object[])client.UserData)[1]) }; upload.BeginGetResponse(itemData, "application/octet-stream", Client.Settings.CAPS_TIMEOUT); } else { try { callback(false, "Missing uploader URL", UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } else if (status == "complete") { if (contents.ContainsKey("new_asset")) { // Request full item update so we keep store in sync RequestFetchInventory((UUID)(((object[])client.UserData)[1]), contents["new_asset"].AsUUID()); try { callback(true, String.Empty, (UUID)(((object[])client.UserData)[1]), contents["new_asset"].AsUUID()); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } else { try { callback(false, "Failed to parse asset and item UUIDs", UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } else { try { callback(false, status, UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } else { string message = "Unrecognized or empty response"; if (error != null) { if (error is WebException exception) message = ((HttpWebResponse)exception.Response).StatusDescription; if (message == null || message == "None") message = error.Message; } try { callback(false, message, UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } private void UpdateScriptAgentInventoryResponse(CapsClient client, OSD result, Exception error) { KeyValuePair kvp = (KeyValuePair)(((object[])client.UserData)[0]); ScriptUpdatedCallback callback = kvp.Key; byte[] itemData = (byte[])kvp.Value; if (result == null) { try { callback(false, error.Message, false, null, UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } return; } OSDMap contents = (OSDMap)result; string status = contents["state"].AsString(); if (status == "upload") { string uploadURL = contents["uploader"].AsString(); CapsClient upload = new CapsClient(new Uri(uploadURL), "ScriptAgentInventoryResponse"); upload.OnComplete += UpdateScriptAgentInventoryResponse; upload.UserData = new object[2] { kvp, (UUID)(((object[])client.UserData)[1]) }; upload.BeginGetResponse(itemData, "application/octet-stream", Client.Settings.CAPS_TIMEOUT); } else if (status == "complete" && callback != null) { if (contents.ContainsKey("new_asset")) { // Request full item update so we keep store in sync RequestFetchInventory((UUID)(((object[])client.UserData)[1]), contents["new_asset"].AsUUID()); try { List compileErrors = null; if (contents.ContainsKey("errors")) { OSDArray errors = (OSDArray)contents["errors"]; compileErrors = new List(errors.Count); compileErrors.AddRange(errors.Select(t => t.AsString())); } callback(true, status, contents["compiled"].AsBoolean(), compileErrors, (UUID)(((object[])client.UserData)[1]), contents["new_asset"].AsUUID()); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } else { try { callback(false, "Failed to parse asset UUID", false, null, UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } else if (callback != null) { try { callback(false, status, false, null, UUID.Zero, UUID.Zero); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } #endregion Internal Handlers #region Packet Handlers /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void SaveAssetIntoInventoryHandler(object sender, PacketReceivedEventArgs e) { if (m_SaveAssetToInventory != null) { Packet packet = e.Packet; SaveAssetIntoInventoryPacket save = (SaveAssetIntoInventoryPacket)packet; OnSaveAssetToInventory(new SaveAssetToInventoryEventArgs(save.InventoryData.ItemID, save.InventoryData.NewAssetID)); } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void InventoryDescendentsHandler(object sender, PacketReceivedEventArgs e) { Packet packet = e.Packet; InventoryDescendentsPacket reply = (InventoryDescendentsPacket)packet; if (reply.AgentData.Descendents > 0) { // InventoryDescendantsReply sends a null folder if the parent doesnt contain any folders if (reply.FolderData[0].FolderID != UUID.Zero) { // Iterate folders in this packet foreach (InventoryDescendentsPacket.FolderDataBlock data in reply.FolderData) { // If folder already exists then ignore, we assume the version cache // logic is working and if the folder is stale then it should not be present. if (!_Store.Contains(data.FolderID)) { InventoryFolder folder = new InventoryFolder(data.FolderID) { ParentUUID = data.ParentID, Name = Utils.BytesToString(data.Name), PreferredType = (FolderType) data.Type, OwnerID = reply.AgentData.OwnerID }; _Store[folder.UUID] = folder; } } } // InventoryDescendantsReply sends a null item if the parent doesnt contain any items. if (reply.ItemData[0].ItemID != UUID.Zero) { // Iterate items in this packet foreach (InventoryDescendentsPacket.ItemDataBlock data in reply.ItemData) { if (data.ItemID != UUID.Zero) { InventoryItem item; /* * Objects that have been attached in-world prior to being stored on the * asset server are stored with the InventoryType of 0 (Texture) * instead of 17 (Attachment) * * This corrects that behavior by forcing Object Asset types that have an * invalid InventoryType with the proper InventoryType of Attachment. */ if ((AssetType)data.Type == AssetType.Object && (InventoryType)data.InvType == InventoryType.Texture) { item = CreateInventoryItem(InventoryType.Attachment, data.ItemID); item.InventoryType = InventoryType.Attachment; } else { item = CreateInventoryItem((InventoryType)data.InvType, data.ItemID); item.InventoryType = (InventoryType)data.InvType; } item.ParentUUID = data.FolderID; item.CreatorID = data.CreatorID; item.AssetType = (AssetType)data.Type; item.AssetUUID = data.AssetID; item.CreationDate = Utils.UnixTimeToDateTime((uint)data.CreationDate); item.Description = Utils.BytesToString(data.Description); item.Flags = data.Flags; item.Name = Utils.BytesToString(data.Name); item.GroupID = data.GroupID; item.GroupOwned = data.GroupOwned; item.Permissions = new Permissions( data.BaseMask, data.EveryoneMask, data.GroupMask, data.NextOwnerMask, data.OwnerMask); item.SalePrice = data.SalePrice; item.SaleType = (SaleType)data.SaleType; item.OwnerID = reply.AgentData.OwnerID; _Store[item.UUID] = item; } } } } InventoryFolder parentFolder = null; if (_Store.Contains(reply.AgentData.FolderID) && _Store[reply.AgentData.FolderID] is InventoryFolder) { parentFolder = (InventoryFolder) _Store[reply.AgentData.FolderID]; } else { Logger.Log("Don't have a reference to FolderID " + reply.AgentData.FolderID.ToString() + " or it is not a folder", Helpers.LogLevel.Error, Client); return; } if (reply.AgentData.Version < parentFolder.Version) { Logger.Log("Got an outdated InventoryDescendents packet for folder " + parentFolder.Name + ", this version = " + reply.AgentData.Version + ", latest version = " + parentFolder.Version, Helpers.LogLevel.Warning, Client); return; } parentFolder.Version = reply.AgentData.Version; // FIXME: reply.AgentData.Descendants is not parentFolder.DescendentCount if we didn't // request items and folders parentFolder.DescendentCount = reply.AgentData.Descendents; _Store.GetNodeFor(reply.AgentData.FolderID).NeedsUpdate = false; #region FindObjectsByPath Handling lock (_Searches) { if (_Searches.Count > 0) { StartSearch: // Iterate over all of the outstanding searches for (int i = 0; i < _Searches.Count; i++) { InventorySearch search = _Searches[i]; List folderContents = _Store.GetContents(search.Folder); // Iterate over all of the inventory objects in the base search folder for (int j = 0; j < folderContents.Count; j++) { // Check if this inventory object matches the current path node if (folderContents[j].Name == search.Path[search.Level]) { if (search.Level == search.Path.Length - 1) { Logger.DebugLog("Finished path search of " + String.Join("/", search.Path), Client); // This is the last node in the path, fire the callback and clean up if (m_FindObjectByPathReply != null) { OnFindObjectByPathReply(new FindObjectByPathReplyEventArgs(String.Join("/", search.Path), folderContents[j].UUID)); } // Remove this entry and restart the loop since we are changing the collection size _Searches.RemoveAt(i); goto StartSearch; } else { // We found a match but it is not the end of the path, request the next level Logger.DebugLog( $"Matched level {search.Level}/{search.Path.Length - 1} in a path search of {String.Join("/", search.Path)}", Client); search.Folder = folderContents[j].UUID; search.Level++; _Searches[i] = search; RequestFolderContents(search.Folder, search.Owner, true, true, InventorySortOrder.ByName); } } } } } } #endregion FindObjectsByPath Handling // Callback for inventory folder contents being updated OnFolderUpdated(new FolderUpdatedEventArgs(parentFolder.UUID, true)); } /// /// UpdateCreateInventoryItem packets are received when a new inventory item /// is created. This may occur when an object that's rezzed in world is /// taken into inventory, when an item is created using the CreateInventoryItem /// packet, or when an object is purchased /// /// The sender /// The EventArgs object containing the packet data protected void UpdateCreateInventoryItemHandler(object sender, PacketReceivedEventArgs e) { Packet packet = e.Packet; if (!(packet is UpdateCreateInventoryItemPacket reply)) return; foreach (UpdateCreateInventoryItemPacket.InventoryDataBlock dataBlock in reply.InventoryData) { if (dataBlock.InvType == (sbyte) InventoryType.Folder) { Logger.Log( "Received InventoryFolder in an UpdateCreateInventoryItem packet, this should not happen!", Helpers.LogLevel.Error, Client); continue; } InventoryItem item = CreateInventoryItem((InventoryType) dataBlock.InvType, dataBlock.ItemID); item.AssetType = (AssetType) dataBlock.Type; item.AssetUUID = dataBlock.AssetID; item.CreationDate = Utils.UnixTimeToDateTime(dataBlock.CreationDate); item.CreatorID = dataBlock.CreatorID; item.Description = Utils.BytesToString(dataBlock.Description); item.Flags = dataBlock.Flags; item.GroupID = dataBlock.GroupID; item.GroupOwned = dataBlock.GroupOwned; item.Name = Utils.BytesToString(dataBlock.Name); item.OwnerID = dataBlock.OwnerID; item.ParentUUID = dataBlock.FolderID; item.Permissions = new Permissions( dataBlock.BaseMask, dataBlock.EveryoneMask, dataBlock.GroupMask, dataBlock.NextOwnerMask, dataBlock.OwnerMask); item.SalePrice = dataBlock.SalePrice; item.SaleType = (SaleType) dataBlock.SaleType; /* * When attaching new objects, an UpdateCreateInventoryItem packet will be * returned by the server that has a FolderID/ParentUUID of zero. It is up * to the client to make sure that the item gets a good folder, otherwise * it will end up inaccesible in inventory. */ if (item.ParentUUID == UUID.Zero) { // assign default folder for type item.ParentUUID = FindFolderForType(item.AssetType); Logger.Log( "Received an item through UpdateCreateInventoryItem with no parent folder, assigning to folder " + item.ParentUUID, Helpers.LogLevel.Info); // send update to the sim RequestUpdateItem(item); } // Update the local copy _Store[item.UUID] = item; // Look for an "item created" callback ItemCreatedCallback createdCallback; if (_ItemCreatedCallbacks.TryGetValue(dataBlock.CallbackID, out createdCallback)) { _ItemCreatedCallbacks.Remove(dataBlock.CallbackID); try { createdCallback(true, item); } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, Client, ex); } } // TODO: Is this callback even triggered when items are copied? // Look for an "item copied" callback ItemCopiedCallback copyCallback; if (_ItemCopiedCallbacks.TryGetValue(dataBlock.CallbackID, out copyCallback)) { _ItemCopiedCallbacks.Remove(dataBlock.CallbackID); try { copyCallback(item); } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, Client, ex); } } //This is triggered when an item is received from a task if (m_TaskItemReceived != null) { OnTaskItemReceived(new TaskItemReceivedEventArgs(item.UUID, dataBlock.FolderID, item.CreatorID, item.AssetUUID, item.InventoryType)); } } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void MoveInventoryItemHandler(object sender, PacketReceivedEventArgs e) { Packet packet = e.Packet; var move = (MoveInventoryItemPacket)packet; foreach (MoveInventoryItemPacket.InventoryDataBlock data in move.InventoryData) { // FIXME: Do something here string newName = Utils.BytesToString(data.NewName); Logger.Log( $"MoveInventoryItemHandler: Item {data.ItemID.ToString()} is moving to Folder {data.FolderID.ToString()} with new name \"{newName}\"." + " Someone write this function!", Helpers.LogLevel.Warning, Client); } } protected void BulkUpdateInventoryCapHandler(string capsKey, Interfaces.IMessage message, Simulator simulator) { var msg = (BulkUpdateInventoryMessage)message; foreach (BulkUpdateInventoryMessage.FolderDataInfo newFolder in msg.FolderData) { if (newFolder.FolderID == UUID.Zero) continue; InventoryFolder folder; if (!_Store.Contains(newFolder.FolderID)) { folder = new InventoryFolder(newFolder.FolderID); } else { folder = (InventoryFolder)_Store[newFolder.FolderID]; } folder.Name = newFolder.Name; folder.ParentUUID = newFolder.ParentID; folder.PreferredType = newFolder.Type; _Store[folder.UUID] = folder; } foreach (BulkUpdateInventoryMessage.ItemDataInfo newItem in msg.ItemData) { if (newItem.ItemID == UUID.Zero) continue; InventoryType invType = newItem.InvType; lock (_ItemInventoryTypeRequest) { InventoryType storedType = 0; if (_ItemInventoryTypeRequest.TryGetValue(newItem.CallbackID, out storedType)) { _ItemInventoryTypeRequest.Remove(newItem.CallbackID); invType = storedType; } } InventoryItem item = SafeCreateInventoryItem(invType, newItem.ItemID); item.AssetType = newItem.Type; item.AssetUUID = newItem.AssetID; item.CreationDate = newItem.CreationDate; item.CreatorID = newItem.CreatorID; item.Description = newItem.Description; item.Flags = newItem.Flags; item.GroupID = newItem.GroupID; item.GroupOwned = newItem.GroupOwned; item.Name = newItem.Name; item.OwnerID = newItem.OwnerID; item.ParentUUID = newItem.FolderID; item.Permissions.BaseMask = newItem.BaseMask; item.Permissions.EveryoneMask = newItem.EveryoneMask; item.Permissions.GroupMask = newItem.GroupMask; item.Permissions.NextOwnerMask = newItem.NextOwnerMask; item.Permissions.OwnerMask = newItem.OwnerMask; item.SalePrice = newItem.SalePrice; item.SaleType = newItem.SaleType; _Store[item.UUID] = item; // Look for an "item created" callback ItemCreatedCallback callback; if (_ItemCreatedCallbacks.TryGetValue(newItem.CallbackID, out callback)) { _ItemCreatedCallbacks.Remove(newItem.CallbackID); try { callback(true, item); } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, Client, ex); } } // Look for an "item copied" callback ItemCopiedCallback copyCallback; if (_ItemCopiedCallbacks.TryGetValue(newItem.CallbackID, out copyCallback)) { _ItemCopiedCallbacks.Remove(newItem.CallbackID); try { copyCallback(item); } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, Client, ex); } } } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void BulkUpdateInventoryHandler(object sender, PacketReceivedEventArgs e) { Packet packet = e.Packet; if (!(packet is BulkUpdateInventoryPacket update)) return; if (update.FolderData.Length > 0 && update.FolderData[0].FolderID != UUID.Zero) { foreach (BulkUpdateInventoryPacket.FolderDataBlock dataBlock in update.FolderData) { InventoryFolder folder; if (!_Store.Contains(dataBlock.FolderID)) { folder = new InventoryFolder(dataBlock.FolderID); } else { folder = (InventoryFolder) _Store[dataBlock.FolderID]; } if (dataBlock.Name != null) { folder.Name = Utils.BytesToString(dataBlock.Name); } folder.OwnerID = update.AgentData.AgentID; folder.ParentUUID = dataBlock.ParentID; _Store[folder.UUID] = folder; } } if (update.ItemData.Length > 0 && update.ItemData[0].ItemID != UUID.Zero) { foreach (BulkUpdateInventoryPacket.ItemDataBlock dataBlock in update.ItemData) { InventoryItem item = SafeCreateInventoryItem((InventoryType) dataBlock.InvType, dataBlock.ItemID); item.AssetType = (AssetType) dataBlock.Type; if (dataBlock.AssetID != UUID.Zero) item.AssetUUID = dataBlock.AssetID; item.CreationDate = Utils.UnixTimeToDateTime(dataBlock.CreationDate); item.CreatorID = dataBlock.CreatorID; item.Description = Utils.BytesToString(dataBlock.Description); item.Flags = dataBlock.Flags; item.GroupID = dataBlock.GroupID; item.GroupOwned = dataBlock.GroupOwned; item.Name = Utils.BytesToString(dataBlock.Name); item.OwnerID = dataBlock.OwnerID; item.ParentUUID = dataBlock.FolderID; item.Permissions = new Permissions( dataBlock.BaseMask, dataBlock.EveryoneMask, dataBlock.GroupMask, dataBlock.NextOwnerMask, dataBlock.OwnerMask); item.SalePrice = dataBlock.SalePrice; item.SaleType = (SaleType) dataBlock.SaleType; _Store[item.UUID] = item; // Look for an "item created" callback ItemCreatedCallback callback; if (_ItemCreatedCallbacks.TryGetValue(dataBlock.CallbackID, out callback)) { _ItemCreatedCallbacks.Remove(dataBlock.CallbackID); try { callback(true, item); } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, Client, ex); } } // Look for an "item copied" callback ItemCopiedCallback copyCallback; if (_ItemCopiedCallbacks.TryGetValue(dataBlock.CallbackID, out copyCallback)) { _ItemCopiedCallbacks.Remove(dataBlock.CallbackID); try { copyCallback(item); } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, Client, ex); } } } } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void FetchInventoryReplyHandler(object sender, PacketReceivedEventArgs e) { Packet packet = e.Packet; if (!(packet is FetchInventoryReplyPacket reply)) return; foreach (FetchInventoryReplyPacket.InventoryDataBlock dataBlock in reply.InventoryData) { if (dataBlock.InvType == (sbyte) InventoryType.Folder) { Logger.Log("Received FetchInventoryReply for an inventory folder, this should not happen!", Helpers.LogLevel.Error, Client); continue; } InventoryItem item = CreateInventoryItem((InventoryType) dataBlock.InvType, dataBlock.ItemID); item.AssetType = (AssetType) dataBlock.Type; item.AssetUUID = dataBlock.AssetID; item.CreationDate = Utils.UnixTimeToDateTime(dataBlock.CreationDate); item.CreatorID = dataBlock.CreatorID; item.Description = Utils.BytesToString(dataBlock.Description); item.Flags = dataBlock.Flags; item.GroupID = dataBlock.GroupID; item.GroupOwned = dataBlock.GroupOwned; item.InventoryType = (InventoryType) dataBlock.InvType; item.Name = Utils.BytesToString(dataBlock.Name); item.OwnerID = dataBlock.OwnerID; item.ParentUUID = dataBlock.FolderID; item.Permissions = new Permissions( dataBlock.BaseMask, dataBlock.EveryoneMask, dataBlock.GroupMask, dataBlock.NextOwnerMask, dataBlock.OwnerMask); item.SalePrice = dataBlock.SalePrice; item.SaleType = (SaleType) dataBlock.SaleType; item.UUID = dataBlock.ItemID; _Store[item.UUID] = item; // Fire the callback for an item being fetched OnItemReceived(new ItemReceivedEventArgs(item)); } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void ReplyTaskInventoryHandler(object sender, PacketReceivedEventArgs e) { if (m_TaskInventoryReply != null) { Packet packet = e.Packet; ReplyTaskInventoryPacket reply = (ReplyTaskInventoryPacket)packet; OnTaskInventoryReply(new TaskInventoryReplyEventArgs(reply.InventoryData.TaskID, reply.InventoryData.Serial, Utils.BytesToString(reply.InventoryData.Filename))); } } protected void ScriptRunningReplyMessageHandler(string capsKey, Interfaces.IMessage message, Simulator simulator) { if (m_ScriptRunningReply != null) { ScriptRunningReplyMessage msg = (ScriptRunningReplyMessage)message; OnScriptRunningReply(new ScriptRunningReplyEventArgs(msg.ObjectID, msg.ItemID, msg.Mono, msg.Running)); } } #endregion Packet Handlers } #region EventArgs public class InventoryObjectOfferedEventArgs : EventArgs { /// Set to true to accept offer, false to decline it public bool Accept { get; set; } /// The folder to accept the inventory into, if null default folder for will be used public UUID FolderID { get; set; } public InstantMessage Offer { get; } public AssetType AssetType { get; } public UUID ObjectID { get; } public bool FromTask { get; } public InventoryObjectOfferedEventArgs(InstantMessage offerDetails, AssetType type, UUID objectID, bool fromTask, UUID folderID) { this.Accept = false; this.FolderID = folderID; this.Offer = offerDetails; this.AssetType = type; this.ObjectID = objectID; this.FromTask = fromTask; } } public class FolderUpdatedEventArgs : EventArgs { public UUID FolderID { get; } public bool Success { get; } public FolderUpdatedEventArgs(UUID folderID, bool success) { this.FolderID = folderID; this.Success = success; } } public class ItemReceivedEventArgs : EventArgs { public InventoryItem Item { get; } public ItemReceivedEventArgs(InventoryItem item) { this.Item = item; } } public class FindObjectByPathReplyEventArgs : EventArgs { public String Path { get; } public UUID InventoryObjectID { get; } public FindObjectByPathReplyEventArgs(string path, UUID inventoryObjectID) { this.Path = path; this.InventoryObjectID = inventoryObjectID; } } /// /// Callback when an inventory object is accepted and received from a /// task inventory. This is the callback in which you actually get /// the ItemID, as in ObjectOfferedCallback it is null when received /// from a task. /// public class TaskItemReceivedEventArgs : EventArgs { public UUID ItemID { get; } public UUID FolderID { get; } public UUID CreatorID { get; } public UUID AssetID { get; } public InventoryType Type { get; } public TaskItemReceivedEventArgs(UUID itemID, UUID folderID, UUID creatorID, UUID assetID, InventoryType type) { this.ItemID = itemID; this.FolderID = folderID; this.CreatorID = creatorID; this.AssetID = assetID; this.Type = type; } } public class TaskInventoryReplyEventArgs : EventArgs { public UUID ItemID { get; } public Int16 Serial { get; } public String AssetFilename { get; } public TaskInventoryReplyEventArgs(UUID itemID, short serial, string assetFilename) { this.ItemID = itemID; this.Serial = serial; this.AssetFilename = assetFilename; } } public class SaveAssetToInventoryEventArgs : EventArgs { public UUID ItemID { get; } public UUID NewAssetID { get; } public SaveAssetToInventoryEventArgs(UUID itemID, UUID newAssetID) { this.ItemID = itemID; this.NewAssetID = newAssetID; } } public class ScriptRunningReplyEventArgs : EventArgs { public UUID ObjectID { get; } public UUID ScriptID { get; } public bool IsMono { get; } public bool IsRunning { get; } public ScriptRunningReplyEventArgs(UUID objectID, UUID sctriptID, bool isMono, bool isRunning) { this.ObjectID = objectID; this.ScriptID = sctriptID; this.IsMono = isMono; this.IsRunning = isRunning; } } #endregion }