SecondBot/SecondBotEvents/Commands/Animation.cs
2026-06-13 04:30:13 -06:00

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");
}
}
}