From 6a21d070597acb68297c02eb485d73cf49ef29fe Mon Sep 17 00:00:00 2001 From: TsengSR Date: Mon, 4 May 2026 00:48:53 +0200 Subject: [PATCH 1/3] Added CreateLinksAsync and AddLinks which allows to create multiple Links in a single request when AIS3 is supported which drastically reduces the competition time when adding 5+ links, i.e. for adding/replacing outfits Fixes a minor bug in ReplaceOutfit that will create a folder link for folders that are not in children of "Outfits", where its not desired to do a folder link (folder Links are only used as indicator for the currently active appearance in SL Viewers Appearance tab) --- .../Appearance/CurrentOutfitFolder.cs | 84 +++++++++++++++---- LibreMetaverse/Inventory/InventoryManager.cs | 67 +++++++++++++++ 2 files changed, 133 insertions(+), 18 deletions(-) diff --git a/LibreMetaverse/Appearance/CurrentOutfitFolder.cs b/LibreMetaverse/Appearance/CurrentOutfitFolder.cs index 625af162..1fe0c406 100644 --- a/LibreMetaverse/Appearance/CurrentOutfitFolder.cs +++ b/LibreMetaverse/Appearance/CurrentOutfitFolder.cs @@ -500,6 +500,49 @@ await client.Inventory.CreateLinkAsync(COF.UUID, item.UUID, item.Name, } } + /// + /// Creates multiple COF links in a single request. + /// + /// Original items to be linked to COF + /// + /// Note: On simulators supporting AIS3 it runs in a single request. On simulators not supporting it, it falls back + /// to iterating and calling for each element. + /// + public async Task AddLinks(IEnumerable items, CancellationToken cancellationToken = default) + { + if (COF == null) + { + return; + } + + if (items == null || !items.Any()) + { + return; + } + + if (client.AisClient.IsAvailable) + { + await client.Inventory.CreateLinksAsync(COF.UUID, items, success => + { + client.Inventory.RequestFolderContents( + COF.UUID, + COF.OwnerID, + fetchFolders: true, + fetchItems: true, + order: InventorySortOrder.ByName, + cancellationToken: cancellationToken + ).ConfigureAwait(false); + }, cancellationToken); + } + else + { + foreach (InventoryItem item in items) + { + await AddLink(item, cancellationToken); + } + } + } + protected async Task RemoveLinksToByActualId(IEnumerable actualItemIdsToRemoveLinksTo, CancellationToken cancellationToken = default) { var actualItemIdsSet = actualItemIdsToRemoveLinksTo.ToArray(); @@ -736,6 +779,7 @@ public async Task ReplaceOutfit(UUID newOutfitFolderId, CancellationToken } var trashFolderId = client.Inventory.FindFolderForType(FolderType.Trash); + var outfitsFolderId = client.Inventory.FindFolderForType(FolderType.Outfit); var rootFolderId = client.Inventory.Store.RootFolder.UUID; var newOutfit = await client.Inventory.RequestFolderContents( @@ -965,35 +1009,39 @@ public async Task ReplaceOutfit(UUID newOutfitFolderId, CancellationToken var toRemoveIds = linksToRemove .Select(n => n.UUID) .Distinct(); - await client.Inventory.RemoveItemsAsync(toRemoveIds, cancellationToken); + if (toRemoveIds.Any()) + { + await client.Inventory.RemoveItemsAsync(toRemoveIds, cancellationToken); + } // Add body parts from current outfit to new outfit if it's lacking those essential body parts foreach (var item in bodypartsToWear) { itemsBeingAdded.Add(item.Value.UUID, item.Value); } - foreach (var item in itemsBeingAdded) - { - await AddLink(item.Value, cancellationToken); - } + await AddLinks(itemsBeingAdded.Values, cancellationToken); - // Add link to outfit folder we're putting on - await client.Inventory.CreateLinkAsync( - currentOutfitFolder.UUID, - newOutfitFolderNode.Data.UUID, - newOutfitFolderNode.Data.Name, - "", - InventoryType.Folder, - UUID.Random(), - (success, newItem) => - { + bool isOutfitFolder = await IsObjectDescendentOf(newOutfitFolderNode.Data, outfitsFolderId, cancellationToken); + // Add link to outfit folder we're putting on, but only if its a child of "Outfits" + if (isOutfitFolder) + { + await client.Inventory.CreateLinkAsync( + currentOutfitFolder.UUID, + newOutfitFolderNode.Data.UUID, + newOutfitFolderNode.Data.Name, + "", + InventoryType.Folder, + UUID.Random(), + (success, newItem) => + { if (success && newItem != null) { _ = client.Inventory.RequestFetchInventoryAsync(newItem.UUID, newItem.OwnerID); } - }, - cancellationToken - ).ConfigureAwait(false); + }, + cancellationToken + ).ConfigureAwait(false); + } // Wear new outfit var tcs = new TaskCompletionSource(); diff --git a/LibreMetaverse/Inventory/InventoryManager.cs b/LibreMetaverse/Inventory/InventoryManager.cs index 6a3b639d..9730af09 100644 --- a/LibreMetaverse/Inventory/InventoryManager.cs +++ b/LibreMetaverse/Inventory/InventoryManager.cs @@ -1989,6 +1989,73 @@ public async Task CreateLinkAsync(UUID folderID, UUID itemID, string name, strin } } + /// + /// Creates multiple inventory links to another inventory item or folder atonce + /// + /// + /// + /// + public async Task CreateLinksAsync( + UUID folderID, + IEnumerable items, + Action callback, + CancellationToken cancellationToken = default + ) + { + if (Client.AisClient.IsAvailable) + { + OSDArray links = new OSDArray(); + foreach (InventoryBase baseItem in items) + { + switch (baseItem) + { + case InventoryItem item: + { + OSDMap link = new OSDMap + { + ["linked_id"] = OSD.FromUUID(item.UUID), + ["type"] = OSD.FromInteger((int)AssetType.Link), + ["inv_type"] = OSD.FromInteger((int)item.InventoryType), + ["name"] = OSD.FromString(item.Name), + ["desc"] = OSD.FromString(item.Description) + }; + + links.Add(link); + break; + } + case InventoryFolder folder: + { + OSDMap link = new OSDMap + { + ["linked_id"] = OSD.FromUUID(folder.UUID), + ["type"] = OSD.FromInteger((int)AssetType.LinkFolder), + ["inv_type"] = OSD.FromInteger((int)InventoryType.Folder), + ["name"] = OSD.FromString(folder.Name), + ["desc"] = OSD.FromString(string.Empty) + }; + + links.Add(link); + } + break; + } + } + + OSDMap newInventory = new OSDMap { { "links", links } }; + + await Client.AisClient.CreateInventory( + folderID, + newInventory, + true, + (success, reply) => callback?.Invoke(success), + cancellationToken + ).ConfigureAwait(false); + } + else + { + throw new InvalidOperationException("Creating of batch links only supported on AIS3"); + } + } + #endregion Create #region Copy From 088811d3187a776353cf445f05b676a4e6d04eda Mon Sep 17 00:00:00 2001 From: TsengSR Date: Mon, 4 May 2026 00:58:57 +0200 Subject: [PATCH 2/3] Also optimized AddToOutfit --- LibreMetaverse/Appearance/CurrentOutfitFolder.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/LibreMetaverse/Appearance/CurrentOutfitFolder.cs b/LibreMetaverse/Appearance/CurrentOutfitFolder.cs index 1fe0c406..95f8a8ed 100644 --- a/LibreMetaverse/Appearance/CurrentOutfitFolder.cs +++ b/LibreMetaverse/Appearance/CurrentOutfitFolder.cs @@ -1262,10 +1262,7 @@ public async Task AddToOutfit(List requestedItemsToAdd, bool repl } // Add links to new items - foreach (var item in itemsToAdd) - { - await AddLink(item, cancellationToken); - } + await AddLinks(itemsToAdd, cancellationToken); client.Appearance.AddToOutfit(itemsToAdd, replace); _ = Task.Run(async () => From 0f97651067b70124c63bcc31b2c77574498c71e2 Mon Sep 17 00:00:00 2001 From: TsengSR Date: Mon, 4 May 2026 23:27:23 +0200 Subject: [PATCH 3/3] Only create links for items which don't already exist in the Current Outfit folder when calling AddLinks --- LibreMetaverse/Appearance/CurrentOutfitFolder.cs | 8 ++++++-- LibreMetaverse/Inventory/InventoryManager.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/LibreMetaverse/Appearance/CurrentOutfitFolder.cs b/LibreMetaverse/Appearance/CurrentOutfitFolder.cs index 95f8a8ed..fce60f31 100644 --- a/LibreMetaverse/Appearance/CurrentOutfitFolder.cs +++ b/LibreMetaverse/Appearance/CurrentOutfitFolder.cs @@ -520,9 +520,13 @@ public async Task AddLinks(IEnumerable items, CancellationToken c return; } + List cofLinks = await GetCurrentOutfitLinks(cancellationToken); + IEnumerable newLinks = items + .Where(item => cofLinks.Find(itemLink => itemLink.AssetUUID == item.ResolvedAssetID) == null); + if (client.AisClient.IsAvailable) { - await client.Inventory.CreateLinksAsync(COF.UUID, items, success => + await client.Inventory.CreateLinksAsync(COF.UUID, newLinks, success => { client.Inventory.RequestFolderContents( COF.UUID, @@ -536,7 +540,7 @@ await client.Inventory.CreateLinksAsync(COF.UUID, items, success => } else { - foreach (InventoryItem item in items) + foreach (InventoryItem item in newLinks) { await AddLink(item, cancellationToken); } diff --git a/LibreMetaverse/Inventory/InventoryManager.cs b/LibreMetaverse/Inventory/InventoryManager.cs index 9730af09..baa4331e 100644 --- a/LibreMetaverse/Inventory/InventoryManager.cs +++ b/LibreMetaverse/Inventory/InventoryManager.cs @@ -1990,7 +1990,7 @@ public async Task CreateLinkAsync(UUID folderID, UUID itemID, string name, strin } /// - /// Creates multiple inventory links to another inventory item or folder atonce + /// Creates multiple inventory links to another inventory item or folder at once. /// /// ///