496 lines
20 KiB
C#
496 lines
20 KiB
C#
using OpenMetaverse;
|
|
using OpenMetaverse.Assets;
|
|
using SecondBotEvents.Services;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace SecondBotEvents.Commands
|
|
{
|
|
[ClassInfo("Animations and Gestures")]
|
|
public class AnimationCommands(EventsSecondBot setmaster) : CommandsAPI(setmaster)
|
|
{
|
|
private static readonly Dictionary<UUID, string> gestureTriggerCache = new Dictionary<UUID, string>();
|
|
private static readonly Dictionary<UUID, string> gestureReplaceCache = new Dictionary<UUID, string>();
|
|
private static bool isPreCachingStarted = false;
|
|
private static bool isPreCachingActive = false;
|
|
private static readonly string cacheFilePath = "data/gesture_cache.json";
|
|
private static bool isCacheLoaded = false;
|
|
private static readonly object cacheLock = new object();
|
|
|
|
public class GestureCacheEntry
|
|
{
|
|
public string Trigger { get; set; }
|
|
public string ReplaceWith { get; set; }
|
|
}
|
|
|
|
private static void LoadGestureCache()
|
|
{
|
|
lock (cacheLock)
|
|
{
|
|
if (isCacheLoaded) return;
|
|
isCacheLoaded = true;
|
|
|
|
try
|
|
{
|
|
System.IO.Directory.CreateDirectory("data");
|
|
if (System.IO.File.Exists(cacheFilePath))
|
|
{
|
|
string json = System.IO.File.ReadAllText(cacheFilePath);
|
|
var loaded = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, GestureCacheEntry>>(json);
|
|
if (loaded != null)
|
|
{
|
|
lock (gestureTriggerCache)
|
|
{
|
|
foreach (var kvp in loaded)
|
|
{
|
|
if (UUID.TryParse(kvp.Key, out UUID uuid))
|
|
{
|
|
gestureTriggerCache[uuid] = kvp.Value.Trigger;
|
|
gestureReplaceCache[uuid] = kvp.Value.ReplaceWith;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogFormater.Warn("Error loading gesture cache: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void SaveGestureCache()
|
|
{
|
|
lock (cacheLock)
|
|
{
|
|
try
|
|
{
|
|
System.IO.Directory.CreateDirectory("data");
|
|
var dump = new Dictionary<string, GestureCacheEntry>();
|
|
lock (gestureTriggerCache)
|
|
{
|
|
foreach (var kvp in gestureTriggerCache)
|
|
{
|
|
string replaceWith = "";
|
|
gestureReplaceCache.TryGetValue(kvp.Key, out replaceWith);
|
|
dump[kvp.Key.ToString()] = new GestureCacheEntry
|
|
{
|
|
Trigger = kvp.Value,
|
|
ReplaceWith = replaceWith ?? ""
|
|
};
|
|
}
|
|
}
|
|
string json = System.Text.Json.JsonSerializer.Serialize(dump);
|
|
System.IO.File.WriteAllText(cacheFilePath, json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogFormater.Warn("Error saving gesture cache: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
[About("Toggles if animation requests from this avatar (used for remote poseballs) are accepted")]
|
|
[ReturnHints("Granted perm animation")]
|
|
[ReturnHints("Removed perm animation")]
|
|
[ReturnHintsFailure("avatar lookup")]
|
|
[ArgHints("avatar", "Who to accept requests from", "AVATAR")]
|
|
[CmdTypeSet()]
|
|
public object AddToAllowAnimations(string avatar)
|
|
{
|
|
ProcessAvatar(avatar);
|
|
if (avataruuid == UUID.Zero)
|
|
{
|
|
return BasicReply("avatar lookup", [avatar]);
|
|
}
|
|
// @todo accept storage
|
|
return Failure("@todo");
|
|
}
|
|
|
|
[About("Attempts to play a gesture \n using the inventory uuid")]
|
|
[ReturnHintsFailure("Error with gesture inventory uuid")]
|
|
[ReturnHints("Accepted")]
|
|
[ArgHints("gesture", "inventory uuid for the gesture to trigger", "UUID")]
|
|
[ReturnHintsFailure("unable to get gesture from inventory")]
|
|
[CmdTypeDo()]
|
|
public object PlayGesture(string gesture)
|
|
{
|
|
if (UUID.TryParse(gesture, out UUID gestureUUID) == false)
|
|
{
|
|
return BasicReply("Error with gesture inventory uuid", [gesture]);
|
|
}
|
|
InventoryItem itm = GetClient().Inventory.FetchItem(gestureUUID, GetClient().Self.AgentID, TimeSpan.FromSeconds(15));
|
|
if (itm == null)
|
|
{
|
|
return BasicReply("unable to get gesture from inventory", [gesture]);
|
|
}
|
|
LogFormater.Info($"Playing gesture: {itm.Name} ({itm.AssetUUID})");
|
|
GetClient().Self.ActivateGesture(itm.UUID, itm.AssetUUID);
|
|
GetClient().Self.PlayGesture(itm.AssetUUID);
|
|
return BasicReply("Accepted", [gesture]);
|
|
}
|
|
[About("Attempts to play a gesture \n using the real asset uuid not the inventory uuid")]
|
|
[ReturnHintsFailure("Error with gesture asset uuid")]
|
|
[ReturnHints("Accepted")]
|
|
[ArgHints("gesture", "asset uuid for the gesture to trigger", "UUID")]
|
|
[CmdTypeDo()]
|
|
public object PlayGestureDirect(string gesture)
|
|
{
|
|
if (UUID.TryParse(gesture, out UUID gestureUUID) == false)
|
|
{
|
|
return BasicReply("Error with gesture", [gesture]);
|
|
}
|
|
GetClient().Self.PlayGesture(gestureUUID);
|
|
return BasicReply("Accepted", [gesture]);
|
|
}
|
|
|
|
[About("Plays a built-in animation by UUID or name")]
|
|
[ArgHints("animation", "UUID or name of the animation", "STRING")]
|
|
[CmdTypeDo()]
|
|
public object PlayAnimation(string animation)
|
|
{
|
|
UUID animID;
|
|
if (!UUID.TryParse(animation, out animID))
|
|
{
|
|
var field = typeof(Animations).GetField(animation.ToUpper(), System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
|
if (field != null)
|
|
{
|
|
animID = (UUID)field.GetValue(null);
|
|
}
|
|
else
|
|
{
|
|
return Failure("Invalid animation name or UUID");
|
|
}
|
|
}
|
|
GetClient().Self.AnimationStart(animID, true);
|
|
return BasicReply("Accepted", [animation]);
|
|
}
|
|
|
|
private string GetGestureTrigger(InventoryItem itm)
|
|
{
|
|
LoadGestureCache();
|
|
lock (gestureTriggerCache)
|
|
{
|
|
if (gestureTriggerCache.TryGetValue(itm.AssetUUID, out string cachedTrigger))
|
|
{
|
|
return cachedTrigger;
|
|
}
|
|
}
|
|
|
|
AssetGesture gesture = null;
|
|
System.Threading.AutoResetEvent gotAsset = new System.Threading.AutoResetEvent(false);
|
|
|
|
GetClient().Assets.RequestAsset(itm.AssetUUID, AssetType.Gesture, true,
|
|
delegate(AssetDownload transfer, Asset asset)
|
|
{
|
|
if (transfer.Success)
|
|
{
|
|
gesture = (AssetGesture)asset;
|
|
}
|
|
gotAsset.Set();
|
|
}
|
|
);
|
|
|
|
// Wait 500ms max (gestures are tiny and take < 50ms usually)
|
|
gotAsset.WaitOne(TimeSpan.FromMilliseconds(500), false);
|
|
|
|
if (gesture != null && gesture.Decode())
|
|
{
|
|
string trigger = gesture.Trigger.Trim().ToLowerInvariant();
|
|
string replaceWith = gesture.ReplaceWith ?? "";
|
|
lock (gestureTriggerCache)
|
|
{
|
|
gestureTriggerCache[itm.AssetUUID] = trigger;
|
|
gestureReplaceCache[itm.AssetUUID] = replaceWith;
|
|
}
|
|
if (!isPreCachingActive)
|
|
{
|
|
SaveGestureCache();
|
|
}
|
|
return trigger;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void StartPreCachingGestures()
|
|
{
|
|
LoadGestureCache();
|
|
if (isPreCachingStarted) return;
|
|
isPreCachingStarted = true;
|
|
|
|
System.Threading.ThreadPool.QueueUserWorkItem(delegate
|
|
{
|
|
try
|
|
{
|
|
List<InventoryItem> allGestures = new List<InventoryItem>();
|
|
UUID gestureFolder = GetClient().Inventory.FindFolderForType(AssetType.Gesture);
|
|
if (gestureFolder != UUID.Zero)
|
|
{
|
|
List<InventoryBase> contents = GetClient().Inventory.FolderContents(gestureFolder, GetClient().Self.AgentID, false, true, InventorySortOrder.ByName, TimeSpan.FromSeconds(15));
|
|
if (contents != null)
|
|
{
|
|
foreach (InventoryBase item in contents)
|
|
{
|
|
if (item is InventoryItem invItem && invItem.InventoryType == InventoryType.Gesture)
|
|
{
|
|
allGestures.Add(invItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Inventory store = GetClient().Inventory.GetStore;
|
|
lock (store.Items)
|
|
{
|
|
foreach (var node in store.Items.Values)
|
|
{
|
|
if (node.Data is InventoryItem item && item.InventoryType == InventoryType.Gesture)
|
|
{
|
|
if (!allGestures.Any(g => g.UUID == item.UUID))
|
|
{
|
|
allGestures.Add(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
isPreCachingActive = true;
|
|
foreach (var item in allGestures)
|
|
{
|
|
GetGestureTrigger(item);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogFormater.Warn("Error pre-caching gestures: " + ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
isPreCachingActive = false;
|
|
SaveGestureCache();
|
|
}
|
|
});
|
|
}
|
|
|
|
private InventoryItem FindGestureInInventory(string search)
|
|
{
|
|
StartPreCachingGestures();
|
|
|
|
search = search.Trim().ToLowerInvariant();
|
|
string searchWithSlash = search.StartsWith("/") ? search : "/" + search;
|
|
string searchWithoutSlash = search.StartsWith("/") ? search.Substring(1) : search;
|
|
|
|
List<InventoryItem> allGestures = new List<InventoryItem>();
|
|
|
|
// 1. Gather all gestures from default Gestures folder
|
|
UUID gestureFolder = GetClient().Inventory.FindFolderForType(AssetType.Gesture);
|
|
if (gestureFolder != UUID.Zero)
|
|
{
|
|
List<InventoryBase> contents = GetClient().Inventory.FolderContents(gestureFolder, GetClient().Self.AgentID, false, true, InventorySortOrder.ByName, TimeSpan.FromSeconds(15));
|
|
if (contents != null)
|
|
{
|
|
foreach (InventoryBase item in contents)
|
|
{
|
|
if (item is InventoryItem invItem && invItem.InventoryType == InventoryType.Gesture)
|
|
{
|
|
allGestures.Add(invItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Gather from general inventory store cache
|
|
Inventory store = GetClient().Inventory.GetStore;
|
|
lock (store.Items)
|
|
{
|
|
foreach (var node in store.Items.Values)
|
|
{
|
|
if (node.Data is InventoryItem item && item.InventoryType == InventoryType.Gesture)
|
|
{
|
|
if (!allGestures.Any(g => g.UUID == item.UUID))
|
|
{
|
|
allGestures.Add(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Step A: Search for exact name match first (super fast, no asset download)
|
|
foreach (var item in allGestures)
|
|
{
|
|
string itemName = item.Name.Trim().ToLowerInvariant();
|
|
if (itemName == searchWithSlash || itemName == searchWithoutSlash)
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
|
|
// 4. Step B: Search for trigger match (downloads/decodes asset, then checks cache)
|
|
InventoryItem bestMatch = null;
|
|
int bestScore = -9999;
|
|
|
|
string botFirstName = GetClient().Self.FirstName.ToLowerInvariant();
|
|
bool isFemaleBot = botFirstName.Contains("girl") || botFirstName.Contains("female") || botFirstName.Contains("lady") || botFirstName.Contains("miss") || botFirstName.Contains("woman") || botFirstName.Contains("queen") || botFirstName.Contains("princess") || botFirstName.Contains("she");
|
|
bool isMaleBot = botFirstName.Contains("boy") || botFirstName.Contains("male") || botFirstName.Contains("guy") || botFirstName.Contains("man") || botFirstName.Contains("king") || botFirstName.Contains("prince") || botFirstName.Contains("he");
|
|
|
|
foreach (var item in allGestures)
|
|
{
|
|
string trigger = GetGestureTrigger(item);
|
|
if (trigger != null)
|
|
{
|
|
string triggerWithSlash = trigger.StartsWith("/") ? trigger : "/" + trigger;
|
|
string triggerWithoutSlash = trigger.StartsWith("/") ? trigger.Substring(1) : trigger;
|
|
if (trigger == search || triggerWithSlash == search || triggerWithoutSlash == search ||
|
|
trigger == searchWithSlash || trigger == searchWithoutSlash)
|
|
{
|
|
int score = 0;
|
|
string itemName = item.Name.ToLowerInvariant();
|
|
|
|
if (isFemaleBot)
|
|
{
|
|
if (itemName.Contains("female") || itemName.Contains("girl") || itemName.Contains("woman") || itemName.Contains("lady") || itemName.Contains("she"))
|
|
score += 10;
|
|
if (itemName.Contains("male") || itemName.Contains("boy") || itemName.Contains("man") || itemName.Contains("guy") || itemName.Contains("he"))
|
|
score -= 10;
|
|
}
|
|
else if (isMaleBot)
|
|
{
|
|
if (itemName.Contains("male") || itemName.Contains("boy") || itemName.Contains("man") || itemName.Contains("guy") || itemName.Contains("he"))
|
|
score += 10;
|
|
if (itemName.Contains("female") || itemName.Contains("girl") || itemName.Contains("woman") || itemName.Contains("lady") || itemName.Contains("she"))
|
|
score -= 10;
|
|
}
|
|
|
|
if (score > bestScore)
|
|
{
|
|
bestScore = score;
|
|
bestMatch = item;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestMatch != null)
|
|
{
|
|
return bestMatch;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
[About("Plays a gesture by name from the inventory")]
|
|
[ArgHints("gesture", "name of the gesture to play", "STRING")]
|
|
[CmdTypeDo()]
|
|
public object PlayGestureByName(string gesture)
|
|
{
|
|
string search = gesture.Trim().ToLowerInvariant();
|
|
|
|
// First try to match the exact string (like "clap" or ":-D")
|
|
InventoryItem foundGesture = FindGestureInInventory(search);
|
|
|
|
// If no exact match, try splitting into words (natural speak matching, like "i am a sad :-(")
|
|
if (foundGesture == null)
|
|
{
|
|
string[] words = search.Split(new[] { ' ', ',', '.', '!', '?' }, StringSplitOptions.RemoveEmptyEntries);
|
|
foreach (string word in words)
|
|
{
|
|
foundGesture = FindGestureInInventory(word);
|
|
if (foundGesture != null)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundGesture == null)
|
|
{
|
|
return Failure("Gesture not found in inventory");
|
|
}
|
|
|
|
GetClient().Self.ActivateGesture(foundGesture.UUID, foundGesture.AssetUUID);
|
|
GetClient().Self.PlayGesture(foundGesture.AssetUUID);
|
|
|
|
string replaceWith = "";
|
|
lock (gestureTriggerCache)
|
|
{
|
|
gestureReplaceCache.TryGetValue(foundGesture.AssetUUID, out replaceWith);
|
|
}
|
|
|
|
master.DataStoreService.AddCommandToHistory(true, "PlayGestureByName", [gesture]);
|
|
return new
|
|
{
|
|
command = "PlayGestureByName",
|
|
args = new[] { gesture },
|
|
reply = "Accepted",
|
|
status = true,
|
|
replace_with = replaceWith ?? "",
|
|
matched_trigger = GetGestureTrigger(foundGesture) ?? ""
|
|
};
|
|
}
|
|
|
|
|
|
[About("Activates a gesture in the bot's inventory")]
|
|
[ArgHints("gesture", "inventory uuid of the gesture", "UUID")]
|
|
[CmdTypeDo()]
|
|
public object ActivateGesture(string gesture)
|
|
{
|
|
if (UUID.TryParse(gesture, out UUID gestureUUID) == false) return Failure("Invaild UUID");
|
|
InventoryItem itm = GetClient().Inventory.FetchItem(gestureUUID, GetClient().Self.AgentID, TimeSpan.FromSeconds(15));
|
|
if (itm == null) return Failure("unable to get gesture from inventory");
|
|
GetClient().Self.ActivateGesture(itm.UUID, itm.AssetUUID);
|
|
return BasicReply("Ok");
|
|
}
|
|
|
|
[About("Deactivates a gesture in the bot's inventory")]
|
|
[ArgHints("gesture", "inventory uuid of the gesture", "UUID")]
|
|
[CmdTypeDo()]
|
|
public object DeactivateGesture(string gesture)
|
|
{
|
|
if (UUID.TryParse(gesture, out UUID gestureUUID) == false) return Failure("Invaild UUID");
|
|
GetClient().Self.DeactivateGesture(gestureUUID);
|
|
return BasicReply("Ok");
|
|
}
|
|
|
|
[About("Clears the saved gesture cache and runs pre-caching again to pick up new or changed gestures")]
|
|
[CmdTypeDo()]
|
|
public object RecacheGestures()
|
|
{
|
|
lock (cacheLock)
|
|
{
|
|
try
|
|
{
|
|
if (System.IO.File.Exists(cacheFilePath))
|
|
{
|
|
System.IO.File.Delete(cacheFilePath);
|
|
}
|
|
}
|
|
catch {}
|
|
|
|
lock (gestureTriggerCache)
|
|
{
|
|
gestureTriggerCache.Clear();
|
|
gestureReplaceCache.Clear();
|
|
}
|
|
isCacheLoaded = false;
|
|
isPreCachingStarted = false;
|
|
}
|
|
StartPreCachingGestures();
|
|
return BasicReply("Recaching started");
|
|
}
|
|
|
|
[About("Resets the animation stack for the bot")]
|
|
[ReturnHints("Accepted - X stopped animations")]
|
|
[CmdTypeDo()]
|
|
public object ResetAnimations()
|
|
{
|
|
List<UUID> animations = [.. GetClient().Self.SignaledAnimations.Copy().Keys];
|
|
foreach (UUID anim in animations)
|
|
{
|
|
GetClient().Self.AnimationStop(anim, true);
|
|
}
|
|
return BasicReply("Accepted - "+animations.Count.ToString()+" stopped animations");
|
|
}
|
|
|
|
}
|
|
}
|