Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 71 additions & 22 deletions LibreMetaverse/Appearance/CurrentOutfitFolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,53 @@ await client.Inventory.CreateLinkAsync(COF.UUID, item.UUID, item.Name,
}
}

/// <summary>
/// Creates multiple COF links in a single request.
/// </summary>
/// <param name="items">Original items to be linked to COF</param>
/// <remarks>
/// Note: On simulators supporting AIS3 it runs in a single request. On simulators not supporting it, it falls back
/// to iterating and calling <see cref="AddLink(InventoryItem, CancellationToken)"/> for each element.
/// </remarks>
public async Task AddLinks(IEnumerable<InventoryItem> items, CancellationToken cancellationToken = default)
{
if (COF == null)
{
return;
}

if (items == null || !items.Any())
{
return;
}

List<InventoryItem> cofLinks = await GetCurrentOutfitLinks(cancellationToken);
IEnumerable<InventoryItem> newLinks = items
.Where(item => cofLinks.Find(itemLink => itemLink.AssetUUID == item.ResolvedAssetID) == null);

if (client.AisClient.IsAvailable)
{
await client.Inventory.CreateLinksAsync(COF.UUID, newLinks, success =>
{
client.Inventory.RequestFolderContents(
COF.UUID,
COF.OwnerID,
fetchFolders: true,
fetchItems: true,
order: InventorySortOrder.ByName,
cancellationToken: cancellationToken
).ConfigureAwait(false);
}, cancellationToken);
Comment on lines +511 to +539
}
else
{
foreach (InventoryItem item in newLinks)
{
await AddLink(item, cancellationToken);
}
}
Comment on lines +527 to +547

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole point of adding AddLinks is to update all the links in a single request, instead of a long sequence request/reply/request/reply which adds tons of latency, about half a second per link created.

}

protected async Task RemoveLinksToByActualId(IEnumerable<UUID> actualItemIdsToRemoveLinksTo, CancellationToken cancellationToken = default)
{
var actualItemIdsSet = actualItemIdsToRemoveLinksTo.ToArray();
Expand Down Expand Up @@ -736,6 +783,7 @@ public async Task<bool> 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(
Expand Down Expand Up @@ -965,35 +1013,39 @@ public async Task<bool> 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<bool>();
Expand Down Expand Up @@ -1214,10 +1266,7 @@ public async Task AddToOutfit(List<InventoryItem> 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 () =>
Expand Down
67 changes: 67 additions & 0 deletions LibreMetaverse/Inventory/InventoryManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,73 @@ public async Task CreateLinkAsync(UUID folderID, UUID itemID, string name, strin
}
}

/// <summary>
/// Creates multiple inventory links to another inventory item or folder at once.
/// </summary>
/// <param name="folderID"></param>
/// <param name="items"></param>
/// <param name="callback"></param>
public async Task CreateLinksAsync(
UUID folderID,
IEnumerable<InventoryBase> items,
Action<bool> 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);
Comment on lines +2007 to +2051

@TsengSR TsengSR May 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it when bots butcher code with non-sense.

@cinderblocks
Would calling client.Inventory.RequestFetchInventoryAsync(newItem.UUID, newItem.OwnerID, cancellationToken); in the client.Inventory.CreateLinksAsync(COF.UUID, newLinks, callback be sufficient? I'm not sure it will fetch folders like RequestFolderContents does.

Though in my personal testing I didn't notice any misssing. The COF Folder and the folder containing the orignal items do update on my Radegast Viewer and show the proper attachment/worn labels

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that would be sufficient. The only issue might be on non-AISv3 regions (none exist in Second Life for about ten years, of course). Not sure how modern the inventory API is in OpenSimulator.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not modern at all. Core doesn't implement AISv3 and has no plans to. I'd like to add it to NGC but still trying to just get the basic architecture compliant with dotnet v8+. So if supporting OpenSim is a goal the code will have to handle cases where AIS isnt present at least for some time.

@TsengSR TsengSR May 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual AddLinks in COF has a fallback to iterate over the list and call AddLink (current code), when AISv3 isn't supported. But the CreateLinksAsync itself doesn't throws if unsupported. I can't even remember where I found the specifications of the endpoint back then.

Tbh I don't know if the older APIs implement batch Link creation or not. I implemented it years ago (2017 or 2018) or so in my local fork, because back then libremetaverise/libopenmetaverse was rather unmaintained and dead.

But until few weeks ago it was still sync, but when rebasing my libremetavers and Radegast i noticing that replacing outfits took forever again (5-10+ seconds) and took me a while to figure out what changed/broke it.

My main movitation back then was to make changing outfits faster, as changing an outfit did freeze the client up to 15-30 seconds (on a Duo Core back in 2017),making it completley unresponsive and slow during this process (also when done via RLV). Today it doesn't freeze the client, but its still slow because of multiple sequential async operations.

}
else
{
throw new InvalidOperationException("Creating of batch links only supported on AIS3");
Comment on lines +2053 to +2055
}
}

#endregion Create

#region Copy
Expand Down
Loading