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 gestureTriggerCache = new Dictionary(); private static readonly Dictionary gestureReplaceCache = new Dictionary(); 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>(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(); 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 allGestures = new List(); UUID gestureFolder = GetClient().Inventory.FindFolderForType(AssetType.Gesture); if (gestureFolder != UUID.Zero) { List 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 allGestures = new List(); // 1. Gather all gestures from default Gestures folder UUID gestureFolder = GetClient().Inventory.FindFolderForType(AssetType.Gesture); if (gestureFolder != UUID.Zero) { List 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 animations = [.. GetClient().Self.SignaledAnimations.Copy().Keys]; foreach (UUID anim in animations) { GetClient().Self.AnimationStop(anim, true); } return BasicReply("Accepted - "+animations.Count.ToString()+" stopped animations"); } } }