870 lines
35 KiB
C#
870 lines
35 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LibreMetaverse.RLV
|
|
{
|
|
public class RlvCommandProcessor
|
|
{
|
|
private readonly ImmutableDictionary<string, Func<RlvMessage, CancellationToken, Task<bool>>> _rlvActionHandlers;
|
|
|
|
// TODO: Swap manager out with an interface once it's been solidified into only useful stuff
|
|
private readonly RlvPermissionsService _manager;
|
|
private readonly IRlvQueryCallbacks _queryCallbacks;
|
|
private readonly IRlvActionCallbacks _actionCallbacks;
|
|
|
|
internal RlvCommandProcessor(RlvPermissionsService manager, IRlvQueryCallbacks callbacks, IRlvActionCallbacks actionCallbacks)
|
|
{
|
|
_manager = manager;
|
|
_queryCallbacks = callbacks;
|
|
_actionCallbacks = actionCallbacks;
|
|
|
|
_rlvActionHandlers = new Dictionary<string, Func<RlvMessage, CancellationToken, Task<bool>>>()
|
|
{
|
|
{ "setrot", HandleSetRot },
|
|
{ "adjustheight", HandleAdjustHeight},
|
|
{ "setcam_fov", HandleSetCamFOV},
|
|
{ "tpto", HandleTpTo},
|
|
{ "sit", HandleSit},
|
|
{ "unsit", HandleUnsit},
|
|
{ "sitground", HandleSitGround},
|
|
{ "remoutfit", HandleRemOutfit},
|
|
{ "detachme", HandleDetachMe},
|
|
{ "remattach", HandleRemAttach},
|
|
{ "detach", HandleRemAttach},
|
|
{ "detachall", HandleDetachAll},
|
|
{ "detachthis", (command, cancellationToken) => HandleDetachThis(command, false, cancellationToken)},
|
|
{ "detachallthis", (command, cancellationToken) => HandleDetachThis(command, true, cancellationToken)},
|
|
{ "setgroup", HandleSetGroup},
|
|
{ "setdebug_", HandleSetDebug},
|
|
{ "setenv_", HandleSetEnv},
|
|
|
|
{ "attach", (command, cancellationToken) => HandleAttach(command, true, false, cancellationToken)},
|
|
{ "attachall", (command, cancellationToken) => HandleAttach(command, true, true, cancellationToken)},
|
|
{ "attachover", (command, cancellationToken) => HandleAttach(command, false, false, cancellationToken)},
|
|
{ "attachallover", (command, cancellationToken) => HandleAttach(command, false, true, cancellationToken)},
|
|
{ "attachthis", (command, cancellationToken) => HandleAttachThis(command, true, false, cancellationToken)},
|
|
{ "attachallthis", (command, cancellationToken) => HandleAttachThis(command, true, true, cancellationToken)},
|
|
{ "attachthisover", (command, cancellationToken) => HandleAttachThis(command, false, false, cancellationToken)},
|
|
{ "attachallthisover", (command, cancellationToken) => HandleAttachThis(command, false, true, cancellationToken)},
|
|
|
|
// addoutfit* -> attach* (These are all aliases of their corresponding attach command)
|
|
{ "addoutfit", (command, cancellationToken) => HandleAttach(command, true, false, cancellationToken)},
|
|
{ "addoutfitall", (command, cancellationToken) => HandleAttach(command, true, true, cancellationToken)},
|
|
{ "addoutfitover", (command, cancellationToken) => HandleAttach(command, false, false, cancellationToken)},
|
|
{ "addoutfitallover", (command, cancellationToken) => HandleAttach(command, false, true, cancellationToken)},
|
|
{ "addoutfitthis", (command, cancellationToken) => HandleAttachThis(command, true, false, cancellationToken)},
|
|
{ "addoutfitallthis", (command, cancellationToken) => HandleAttachThis(command, true, true, cancellationToken)},
|
|
{ "addoutfitthisover", (command, cancellationToken) => HandleAttachThis(command, false, false, cancellationToken)},
|
|
{ "addoutfitallthisover", (command, cancellationToken) => HandleAttachThis(command, false, true, cancellationToken)},
|
|
|
|
// *overorreplace -> * (These are all aliases of their corresponding attach command)
|
|
{ "attachoverorreplace", (command, cancellationToken) => HandleAttach(command, true, false, cancellationToken)},
|
|
{ "attachalloverorreplace", (command, cancellationToken) => HandleAttach(command, true, true, cancellationToken)},
|
|
{ "attachthisoverorreplace", (command, cancellationToken) => HandleAttachThis(command, true, false, cancellationToken)},
|
|
{ "attachallthisoverorreplace", (command, cancellationToken) => HandleAttachThis(command, true, true, cancellationToken)},
|
|
}.ToImmutableDictionary();
|
|
}
|
|
|
|
internal async Task<bool> ProcessActionCommand(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
if (_rlvActionHandlers.TryGetValue(command.Behavior, out var func))
|
|
{
|
|
return await func(command, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
else if (command.Behavior.StartsWith("setdebug_", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return await _rlvActionHandlers["setdebug_"](command, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
else if (command.Behavior.StartsWith("setenv_", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return await _rlvActionHandlers["setenv_"](command, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task<bool> HandleSetDebug(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var separatorIndex = command.Behavior.IndexOf('_');
|
|
if (separatorIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var settingName = command.Behavior.Substring(separatorIndex + 1);
|
|
if (settingName.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
await _actionCallbacks.SetDebugAsync(settingName, command.Option, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleSetEnv(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var separatorIndex = command.Behavior.IndexOf('_');
|
|
if (separatorIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var settingName = command.Behavior.Substring(separatorIndex + 1);
|
|
if (settingName.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
await _actionCallbacks.SetEnvAsync(settingName, command.Option, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleSetGroup(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var argParts = command.Option.Split([';'], StringSplitOptions.RemoveEmptyEntries);
|
|
if (argParts.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string? groupRole = null;
|
|
if (argParts.Length > 1)
|
|
{
|
|
groupRole = argParts[1];
|
|
}
|
|
|
|
if (Guid.TryParse(argParts[0], out var groupId))
|
|
{
|
|
await _actionCallbacks.SetGroupAsync(groupId, groupRole, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
await _actionCallbacks.SetGroupAsync(argParts[0], groupRole, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool CanRemAttachItem(RlvInventoryItem item, bool enforceNostrip, bool enforceRestrictions)
|
|
{
|
|
if (item.WornOn == null && item.AttachedTo == null && item.GestureState != RlvGestureState.Active)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (enforceNostrip && item.Name.ToLowerInvariant().Contains("nostrip"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Special exception: If a folder with (nostrip) contains inventory links to other items, those linked items can still
|
|
// be removed. Only the objects actual parent folder or the actual item itself counts.
|
|
if (enforceNostrip && !item.IsLink && item.Folder != null && item.Folder.Name.ToLowerInvariant().Contains("nostrip"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (item.WornOn is RlvWearableType.Skin or RlvWearableType.Shape or RlvWearableType.Eyes or RlvWearableType.Hair)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (enforceRestrictions && !_manager.CanDetach(item))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void CollectItemsToAttach(RlvSharedFolder folder, InventoryMap inventoryMap, bool replaceExistingAttachments, bool recursive, bool skipIfPrivateFolder, Dictionary<Guid, AttachmentRequest> attachableItemMap)
|
|
{
|
|
if (skipIfPrivateFolder && folder.Name.StartsWith(".", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (folder.Name.StartsWith("+", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
replaceExistingAttachments = false;
|
|
}
|
|
|
|
RlvAttachmentPoint? folderAttachmentPoint = null;
|
|
if (RlvCommon.TryGetAttachmentPointFromItemName(folder.Name, out var attachmentPointTemp))
|
|
{
|
|
folderAttachmentPoint = attachmentPointTemp;
|
|
}
|
|
|
|
foreach (var item in folder.Items)
|
|
{
|
|
if (item.AttachedTo != null || item.WornOn != null || item.GestureState == RlvGestureState.Active)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (RlvCommon.TryGetAttachmentPointFromItemName(item.Name, out var attachmentPoint))
|
|
{
|
|
if (!attachableItemMap.ContainsKey(item.Id))
|
|
{
|
|
attachableItemMap[item.Id] = new AttachmentRequest(item.Id, attachmentPoint.Value, replaceExistingAttachments);
|
|
}
|
|
}
|
|
else if (folderAttachmentPoint != null)
|
|
{
|
|
if (!attachableItemMap.ContainsKey(item.Id))
|
|
{
|
|
attachableItemMap[item.Id] = new AttachmentRequest(item.Id, folderAttachmentPoint.Value, replaceExistingAttachments);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!attachableItemMap.ContainsKey(item.Id))
|
|
{
|
|
attachableItemMap[item.Id] = new AttachmentRequest(item.Id, RlvAttachmentPoint.Default, replaceExistingAttachments);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (recursive)
|
|
{
|
|
foreach (var child in folder.Children)
|
|
{
|
|
if (child.Name.StartsWith(".", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
CollectItemsToAttach(child, inventoryMap, replaceExistingAttachments, recursive, true, attachableItemMap);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CollectItemsToDetach(RlvSharedFolder folder, InventoryMap inventoryMap, bool recursive, bool skipIfPrivateFolder, bool enforceNoStrip, bool enforceRestrictions, Dictionary<Guid, bool> detachableItemMap)
|
|
{
|
|
if (skipIfPrivateFolder && folder.Name.StartsWith(".", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdateDetachableItemsMap(folder.Items, enforceNoStrip, enforceRestrictions, detachableItemMap);
|
|
|
|
if (recursive)
|
|
{
|
|
foreach (var child in folder.Children)
|
|
{
|
|
CollectItemsToDetach(child, inventoryMap, recursive, true, enforceNoStrip, enforceRestrictions, detachableItemMap);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateDetachableItemsMap(IEnumerable<RlvInventoryItem> items, bool enforceNostrip, bool enforceRestrictions, Dictionary<Guid, bool> detachableItemMap)
|
|
{
|
|
foreach (var item in items)
|
|
{
|
|
if (item.AttachedTo == null && item.WornOn == null && item.GestureState != RlvGestureState.Active)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (detachableItemMap.TryGetValue(item.Id, out var canDetach) && canDetach == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
canDetach = CanRemAttachItem(item, enforceNostrip, enforceRestrictions);
|
|
detachableItemMap[item.Id] = canDetach;
|
|
}
|
|
}
|
|
|
|
// @attach:[folder]=force
|
|
private async Task<bool> HandleAttach(RlvMessage command, bool replaceExistingAttachments, bool recursive, CancellationToken cancellationToken)
|
|
{
|
|
var (hasInventoryMap, inventoryMap) = await _queryCallbacks.TryGetInventoryMapAsync(cancellationToken).ConfigureAwait(false);
|
|
if (!hasInventoryMap || inventoryMap == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!inventoryMap.TryGetFolderFromPath(command.Option, false, out var folder))
|
|
{
|
|
await _actionCallbacks.AttachAsync(Array.Empty<AttachmentRequest>(), cancellationToken).ConfigureAwait(false);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
var attachableItemMap = new Dictionary<Guid, AttachmentRequest>();
|
|
CollectItemsToAttach(folder, inventoryMap, replaceExistingAttachments, recursive, false, attachableItemMap);
|
|
|
|
var itemsToAttach = attachableItemMap
|
|
.Values
|
|
.ToList();
|
|
|
|
await _actionCallbacks.AttachAsync(itemsToAttach, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// @attachthis[:<attachableType>|<wearableType>|<attachedPrimId>]=force
|
|
//
|
|
// - @attachthis
|
|
// * Attach all items in the folders where the sender of this command exists
|
|
//
|
|
// - @attachthis:attachedPrimId
|
|
// * Attach all items in the folders where the attached prim id exists
|
|
//
|
|
// - @attachthis:wearableType
|
|
// * Attach all items in folders containing worn wearables of the specified wearable type
|
|
//
|
|
// - @attachthis:attachableType
|
|
// * Attach all items in folders containing attached attachables of the specified attachable type
|
|
//
|
|
//
|
|
// * BUG: Ignores locked folders restrictions (@attach[all]this=n)
|
|
// * Ignores .private folders
|
|
//
|
|
private async Task<bool> HandleAttachThis(RlvMessage command, bool replaceExistingAttachments, bool recursive, CancellationToken cancellationToken)
|
|
{
|
|
var (hasInventoryMap, inventoryMap) = await _queryCallbacks.TryGetInventoryMapAsync(cancellationToken).ConfigureAwait(false);
|
|
if (!hasInventoryMap || inventoryMap == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var skipHiddenFolders = true;
|
|
var foldersToAttachMap = new Dictionary<Guid, RlvSharedFolder>();
|
|
|
|
if (command.Option.Length == 0)
|
|
{
|
|
var folders = inventoryMap.FindFoldersContaining(false, command.Sender, null, null);
|
|
foreach (var folder in folders)
|
|
{
|
|
foldersToAttachMap[folder.Id] = folder;
|
|
}
|
|
|
|
skipHiddenFolders = false;
|
|
}
|
|
else if (Guid.TryParse(command.Option, out var attachedPrimId))
|
|
{
|
|
var items = inventoryMap.GetItemsByPrimId(attachedPrimId);
|
|
foreach (var item in items)
|
|
{
|
|
if (item.Folder != null)
|
|
{
|
|
foldersToAttachMap[item.Folder.Id] = item.Folder;
|
|
}
|
|
}
|
|
}
|
|
else if (RlvCommon.RlvWearableTypeMap.TryGetValue(command.Option, out var wearableType))
|
|
{
|
|
var folders = inventoryMap.FindFoldersContaining(false, null, null, wearableType);
|
|
foreach (var folder in folders)
|
|
{
|
|
foldersToAttachMap[folder.Id] = folder;
|
|
}
|
|
}
|
|
else if (RlvCommon.RlvAttachmentPointMap.TryGetValue(command.Option, out var attachmentPoint))
|
|
{
|
|
var folders = inventoryMap.FindFoldersContaining(false, null, attachmentPoint, null);
|
|
foreach (var folder in folders)
|
|
{
|
|
foldersToAttachMap[folder.Id] = folder;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var attachableItemMap = new Dictionary<Guid, AttachmentRequest>();
|
|
foreach (var folder in foldersToAttachMap)
|
|
{
|
|
CollectItemsToAttach(folder.Value, inventoryMap, replaceExistingAttachments, recursive, skipHiddenFolders, attachableItemMap);
|
|
}
|
|
|
|
var itemsToAttach = attachableItemMap
|
|
.Values
|
|
.ToList();
|
|
|
|
await _actionCallbacks.AttachAsync(itemsToAttach, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
// @remattach[:<folder|attachpt|attachedPrimId>]=force
|
|
//
|
|
// - @remattach
|
|
// * Detach all attached items
|
|
//
|
|
// - @remattach:folder
|
|
// * Detach and unwear and deactivate everything possible in the folder
|
|
//
|
|
// - @remattach:attachpt -
|
|
// * Detach all attachables of the specified type, as long as they are not restricted.
|
|
// Having a link to an object in multiple folder, and one of those folders is locked,
|
|
// the command will fail because one of the links is in a locked folder
|
|
//
|
|
// - @remattach:attachedPrimId -
|
|
// * Detach the item that has the specified prim id (this is the id you get when
|
|
// you click 'copy keys' when editing the item attached to your avatar.
|
|
//
|
|
// * Items with (nostrip) will be ignored
|
|
// * If any links to the targeted item exists in a locked folder (restricted detach), then
|
|
// the item will be ignored and not removed.
|
|
//
|
|
private async Task<bool> HandleRemAttach(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var (hasInventoryMap, inventoryMap) = await _queryCallbacks.TryGetInventoryMapAsync(cancellationToken).ConfigureAwait(false);
|
|
if (!hasInventoryMap || inventoryMap == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var detachableItemMap = new Dictionary<Guid, bool>();
|
|
|
|
if (command.Option.Length == 0)
|
|
{
|
|
var currentOutfit = inventoryMap.GetCurrentOutfit()
|
|
.Where(n => n.AttachedTo != null);
|
|
|
|
UpdateDetachableItemsMap(currentOutfit, true, true, detachableItemMap);
|
|
}
|
|
else if (Guid.TryParse(command.Option, out var attachedPrimId))
|
|
{
|
|
var items = inventoryMap.GetItemsByPrimId(attachedPrimId);
|
|
UpdateDetachableItemsMap(items, true, true, detachableItemMap);
|
|
}
|
|
else if (inventoryMap.TryGetFolderFromPath(command.Option, false, out var folder))
|
|
{
|
|
UpdateDetachableItemsMap(folder.Items, true, true, detachableItemMap);
|
|
}
|
|
else if (RlvCommon.RlvAttachmentPointMap.TryGetValue(command.Option, out var attachmentPoint))
|
|
{
|
|
var items = inventoryMap.GetItemsByAttachmentPoint(attachmentPoint);
|
|
UpdateDetachableItemsMap(items, true, true, detachableItemMap);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var itemIdsToDetach = detachableItemMap
|
|
.Where(n => n.Value == true)
|
|
.Select(n => n.Key)
|
|
.ToList();
|
|
|
|
await _actionCallbacks.DetachAsync(itemIdsToDetach, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
|
|
// @detachall:<folder>=force
|
|
//
|
|
// - @detachall
|
|
// * Detach all attached items, unwear all worn items, and deactivate all gestures in the specified folder recursively
|
|
//
|
|
// * Private folder (. prefix) will be ignored unless it's the target folder
|
|
// * Items with (nostrip) will be ignored
|
|
// * If any links to the targeted item exists in a locked folder (restricted detach), then
|
|
// the item will be ignored and not removed.
|
|
//
|
|
private async Task<bool> HandleDetachAll(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var (hasInventoryMap, inventoryMap) = await _queryCallbacks.TryGetInventoryMapAsync(cancellationToken).ConfigureAwait(false);
|
|
if (!hasInventoryMap || inventoryMap == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!inventoryMap.TryGetFolderFromPath(command.Option, false, out var folder))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var detachableItemMap = new Dictionary<Guid, bool>();
|
|
|
|
CollectItemsToDetach(folder, inventoryMap, true, false, true, true, detachableItemMap);
|
|
|
|
var itemIdsToDetach = detachableItemMap
|
|
.Where(n => n.Value == true)
|
|
.Select(n => n.Key)
|
|
.ToList();
|
|
|
|
await _actionCallbacks.DetachAsync(itemIdsToDetach, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
// @detachthis[:<wearableType>|<attachableType>|<uuid>]=force
|
|
//
|
|
// - @detachthis
|
|
// * Find all links and objects with the sender prim id and detach/unwear/deactivate everything from those folders
|
|
//
|
|
// - @detachthis:wearableType
|
|
// * Find all links and objects worn as the specified wearable type and detach/unwear/deactivate everything from those folders
|
|
//
|
|
// - @detachthis:attachableType
|
|
// * Find all links and objects attached to the specified location and detach/unwear/deactivate everything from those folders
|
|
//
|
|
// - @detachthis:uuid
|
|
// * Find all links and objects with the given prim ID and detach/unwear/deactivate everything from those folders
|
|
//
|
|
// * BUG: Takes into account rlv restrictions (@attachthis ignores restrictions, this one enforces restrictions. one of these is a bug in firestorm)
|
|
// * Private folders (. prefix) will be ignored
|
|
// * Items with (nostrip) will be ignored
|
|
// * If any links to the targeted item exists in a locked folder (restricted detach), then
|
|
// the item will be ignored and not removed.
|
|
//
|
|
private async Task<bool> HandleDetachThis(RlvMessage command, bool recursive, CancellationToken cancellationToken)
|
|
{
|
|
var (hasInventoryMap, inventoryMap) = await _queryCallbacks.TryGetInventoryMapAsync(cancellationToken).ConfigureAwait(false);
|
|
if (!hasInventoryMap || inventoryMap == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var folderPaths = new Dictionary<Guid, RlvSharedFolder>();
|
|
var ignoreHiddenFolders = true;
|
|
|
|
if (command.Option.Length == 0)
|
|
{
|
|
var parts = inventoryMap.FindFoldersContaining(false, command.Sender, null, null);
|
|
foreach (var item in parts)
|
|
{
|
|
folderPaths[item.Id] = item;
|
|
}
|
|
|
|
ignoreHiddenFolders = false;
|
|
}
|
|
else if (Guid.TryParse(command.Option, out var attachedPrimId))
|
|
{
|
|
var items = inventoryMap.GetItemsByPrimId(attachedPrimId);
|
|
foreach (var item in items)
|
|
{
|
|
if (item.Folder != null)
|
|
{
|
|
folderPaths[item.Id] = item.Folder;
|
|
}
|
|
}
|
|
}
|
|
else if (RlvCommon.RlvWearableTypeMap.TryGetValue(command.Option, out var wearableType))
|
|
{
|
|
var parts = inventoryMap.FindFoldersContaining(false, null, null, wearableType);
|
|
foreach (var item in parts)
|
|
{
|
|
folderPaths[item.Id] = item;
|
|
}
|
|
}
|
|
else if (RlvCommon.RlvAttachmentPointMap.TryGetValue(command.Option, out var attachmentPoint))
|
|
{
|
|
var parts = inventoryMap.FindFoldersContaining(false, null, attachmentPoint, null);
|
|
foreach (var item in parts)
|
|
{
|
|
folderPaths[item.Id] = item;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var detachableItemMap = new Dictionary<Guid, bool>();
|
|
foreach (var item in folderPaths)
|
|
{
|
|
CollectItemsToDetach(item.Value, inventoryMap, recursive, ignoreHiddenFolders, true, true, detachableItemMap);
|
|
}
|
|
|
|
var itemIdsToDetach = detachableItemMap
|
|
.Where(n => n.Value == true)
|
|
.Select(n => n.Key)
|
|
.ToList();
|
|
|
|
await _actionCallbacks.DetachAsync(itemIdsToDetach, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
// @detachme=force
|
|
// Detach the item calling this command, unless it's inside of a locked folder
|
|
//
|
|
// * Detaches the item even if the name or folder contains (nostrip)
|
|
//
|
|
// * Does not detach the item if any links to it exist in a locked folder (restricted detach)
|
|
private async Task<bool> HandleDetachMe(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var (hasInventoryMap, inventoryMap) = await _queryCallbacks.TryGetInventoryMapAsync(cancellationToken).ConfigureAwait(false);
|
|
if (!hasInventoryMap || inventoryMap == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var senderItems = inventoryMap.GetItemsByPrimId(command.Sender);
|
|
if (senderItems.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var detachableItemMap = new Dictionary<Guid, bool>();
|
|
|
|
// NOTE: @detachme ignores nostrip tags
|
|
UpdateDetachableItemsMap(senderItems, false, true, detachableItemMap);
|
|
|
|
var itemIdsToDetach = detachableItemMap
|
|
.Where(n => n.Value == true)
|
|
.Select(n => n.Key)
|
|
.ToList();
|
|
|
|
await _actionCallbacks.DetachAsync(itemIdsToDetach, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
// @remoutfit[:<folder|wearableType>]=force
|
|
//
|
|
// - @remoutfit
|
|
// * Find all worn items and unwear them
|
|
// * This will search .private folders
|
|
//
|
|
// - @remoutfit:wearableType
|
|
// * Find all links and objects worn as the specified wearable type and unwear them.
|
|
// * This will search .private folders
|
|
//
|
|
// - @remoutfit:folder
|
|
// * Find all links and objects worn as the specified wearable type and detach/unwear/deactivate everything from those folders
|
|
//
|
|
// * Items with (nostrip) will be ignored
|
|
// * If any links to the targeted item exists in a locked folder (restricted detach), then
|
|
// the item will be ignored and not removed.
|
|
//
|
|
// TODO: Add support for Attachment groups (RLVa)
|
|
private async Task<bool> HandleRemOutfit(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var (hasInventoryMap, inventoryMap) = await _queryCallbacks.TryGetInventoryMapAsync(cancellationToken).ConfigureAwait(false);
|
|
if (!hasInventoryMap || inventoryMap == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var detachableItemMap = new Dictionary<Guid, bool>();
|
|
|
|
if (command.Option.Length == 0)
|
|
{
|
|
var currentlyWornItems = inventoryMap
|
|
.GetCurrentOutfit()
|
|
.Where(n => n.WornOn != null);
|
|
|
|
UpdateDetachableItemsMap(currentlyWornItems, true, true, detachableItemMap);
|
|
}
|
|
else if (RlvCommon.RlvWearableTypeMap.TryGetValue(command.Option, out var wearableType))
|
|
{
|
|
var wearableItems = inventoryMap.GetItemsByWearableType(wearableType);
|
|
UpdateDetachableItemsMap(wearableItems, true, true, detachableItemMap);
|
|
}
|
|
else if (inventoryMap.TryGetFolderFromPath(command.Option, false, out var folder))
|
|
{
|
|
CollectItemsToDetach(folder, inventoryMap, false, false, true, true, detachableItemMap);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var itemIdsToDetach = detachableItemMap
|
|
.Where(n => n.Value == true)
|
|
.Select(n => n.Key)
|
|
.ToList();
|
|
|
|
await _actionCallbacks.RemOutfitAsync(itemIdsToDetach, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleUnsit(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
if (!_manager.CanUnsit())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
await _actionCallbacks.UnsitAsync(cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleSitGround(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
if (!_manager.CanSit())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
await _actionCallbacks.SitGroundAsync(cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleSetRot(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
if (!float.TryParse(command.Option, out var angleInRadians))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
await _actionCallbacks.SetRotAsync(angleInRadians, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleAdjustHeight(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var args = command.Option.Split([';'], StringSplitOptions.RemoveEmptyEntries);
|
|
if (args.Length < 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!float.TryParse(args[0], out var distance))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var factor = 1.0f;
|
|
var deltaInMeters = 0.0f;
|
|
|
|
if (args.Length > 1 && !float.TryParse(args[1], out factor))
|
|
{
|
|
factor = 1;
|
|
}
|
|
|
|
if (args.Length > 2 && !float.TryParse(args[2], out deltaInMeters))
|
|
{
|
|
deltaInMeters = 0;
|
|
}
|
|
|
|
await _actionCallbacks.AdjustHeightAsync(distance, factor, deltaInMeters, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleSetCamFOV(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
var cameraRestrictions = _manager.GetCameraRestrictions();
|
|
if (cameraRestrictions.IsLocked)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!float.TryParse(command.Option, out var fov))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
await _actionCallbacks.SetCamFOVAsync(fov, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleSit(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
if (!Guid.TryParse(command.Option, out var sitTarget))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_manager.CanSit())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var objectExists = await _queryCallbacks.ObjectExistsAsync(sitTarget, cancellationToken).ConfigureAwait(false);
|
|
if (!objectExists)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var isCurrentlySitting = await _queryCallbacks.IsSittingAsync(cancellationToken).ConfigureAwait(false);
|
|
if (isCurrentlySitting)
|
|
{
|
|
if (!_manager.CanUnsit())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_manager.CanStandTp())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
await _actionCallbacks.SitAsync(sitTarget, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> HandleTpTo(RlvMessage command, CancellationToken cancellationToken)
|
|
{
|
|
// @tpto is inhibited by @tploc=n, by @unsit too.
|
|
if (!_manager.CanTpLoc())
|
|
{
|
|
return false;
|
|
}
|
|
if (!_manager.CanUnsit())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var commandArgs = command.Option.Split([';'], StringSplitOptions.RemoveEmptyEntries);
|
|
var locationArgs = commandArgs[0].Split('/');
|
|
|
|
if (locationArgs.Length is < 3 or > 4)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
float? lookat = null;
|
|
if (commandArgs.Length > 1)
|
|
{
|
|
if (!float.TryParse(commandArgs[1], out var val))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
lookat = val;
|
|
}
|
|
|
|
if (locationArgs.Length == 3)
|
|
{
|
|
if (!float.TryParse(locationArgs[0], out var x))
|
|
{
|
|
return false;
|
|
}
|
|
if (!float.TryParse(locationArgs[1], out var y))
|
|
{
|
|
return false;
|
|
}
|
|
if (!float.TryParse(locationArgs[2], out var z))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
await _actionCallbacks.TpToAsync(x, y, z, null, lookat, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
else if (locationArgs.Length == 4)
|
|
{
|
|
var regionName = locationArgs[0];
|
|
|
|
if (!float.TryParse(locationArgs[1], out var x))
|
|
{
|
|
return false;
|
|
}
|
|
if (!float.TryParse(locationArgs[2], out var y))
|
|
{
|
|
return false;
|
|
}
|
|
if (!float.TryParse(locationArgs[3], out var z))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
await _actionCallbacks.TpToAsync(x, y, z, regionName, lookat, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|