feat: persist pending group invites and resolve names, restore custom avatar methods
Some checks are pending
autobuild / docker (push) Waiting to run
Some checks are pending
autobuild / docker (push) Waiting to run
This commit is contained in:
parent
ad2aab419f
commit
f9094f6fca
24 changed files with 1350 additions and 67 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
|
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["NuGet.Config", "."]
|
COPY ["NuGet.Config", "."]
|
||||||
COPY ["SecondBotEvents/SecondBotEvents.csproj", "SecondBotEvents/"]
|
COPY ["SecondBotEvents/SecondBotEvents.csproj", "SecondBotEvents/"]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>LibreMetaverse.StructuredData</AssemblyName>
|
<AssemblyName>LibreMetaverse.StructuredData</AssemblyName>
|
||||||
<Description>LibreMetaverse structured data library</Description>
|
<Description>LibreMetaverse structured data library</Description>
|
||||||
<PackageId>LibreMetaverse.StructuredData</PackageId>
|
<PackageId>LibreMetaverse.StructuredData</PackageId>
|
||||||
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<RootNamespace>LibreMetaverse.StructuredData</RootNamespace>
|
<RootNamespace>LibreMetaverse.StructuredData</RootNamespace>
|
||||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>LibreMetaverse.Types</AssemblyName>
|
<AssemblyName>LibreMetaverse.Types</AssemblyName>
|
||||||
<PackageId>LibreMetaverse.Types</PackageId>
|
<PackageId>LibreMetaverse.Types</PackageId>
|
||||||
<Description>LibreMetaverse type library</Description>
|
<Description>LibreMetaverse type library</Description>
|
||||||
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<RootNamespace>LibreMetaverse</RootNamespace>
|
<RootNamespace>LibreMetaverse</RootNamespace>
|
||||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||||
|
|
|
||||||
|
|
@ -375,11 +375,11 @@ namespace OpenMetaverse
|
||||||
public ulong TimeOfDay { get; private set; }
|
public ulong TimeOfDay { get; private set; }
|
||||||
|
|
||||||
/// <summary>A dictionary of all the regions, indexed by region name</summary>
|
/// <summary>A dictionary of all the regions, indexed by region name</summary>
|
||||||
internal Dictionary<string, GridRegion> Regions = new Dictionary<string, GridRegion>();
|
public Dictionary<string, GridRegion> Regions = new Dictionary<string, GridRegion>();
|
||||||
/// <summary>A dictionary of all the regions, indexed by region handle</summary>
|
/// <summary>A dictionary of all the regions, indexed by region handle</summary>
|
||||||
internal Dictionary<ulong, GridRegion> RegionsByHandle = new Dictionary<ulong, GridRegion>();
|
public Dictionary<ulong, GridRegion> RegionsByHandle = new Dictionary<ulong, GridRegion>();
|
||||||
/// <summary>A dictionary of regions by region handle</summary>
|
/// <summary>A dictionary of regions by region handle</summary>
|
||||||
internal Dictionary<UUID, ulong> RegionsByUUID = new Dictionary<UUID, ulong>();
|
public Dictionary<UUID, ulong> RegionsByUUID = new Dictionary<UUID, ulong>();
|
||||||
|
|
||||||
private readonly GridClient Client;
|
private readonly GridClient Client;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>LibreMetaverse</AssemblyName>
|
<AssemblyName>LibreMetaverse</AssemblyName>
|
||||||
<PackageId>LibreMetaverse</PackageId>
|
<PackageId>LibreMetaverse</PackageId>
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
<NoWarn>$(NoWarn);CS0419;CS8981;CS1591;CS1574</NoWarn>
|
<NoWarn>$(NoWarn);CS0419;CS8981;CS1591;CS1574</NoWarn>
|
||||||
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
<OutputPath>..\bin\</OutputPath>
|
<OutputPath>..\bin\</OutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
|
using OpenMetaverse.Assets;
|
||||||
using SecondBotEvents.Services;
|
using SecondBotEvents.Services;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -7,8 +8,90 @@ using System.Linq;
|
||||||
namespace SecondBotEvents.Commands
|
namespace SecondBotEvents.Commands
|
||||||
{
|
{
|
||||||
[ClassInfo("Animations and Gestures")]
|
[ClassInfo("Animations and Gestures")]
|
||||||
internal class AnimationCommands(EventsSecondBot setmaster) : CommandsAPI(setmaster)
|
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")]
|
[About("Toggles if animation requests from this avatar (used for remote poseballs) are accepted")]
|
||||||
[ReturnHints("Granted perm animation")]
|
[ReturnHints("Granted perm animation")]
|
||||||
[ReturnHints("Removed perm animation")]
|
[ReturnHints("Removed perm animation")]
|
||||||
|
|
@ -41,8 +124,10 @@ namespace SecondBotEvents.Commands
|
||||||
InventoryItem itm = GetClient().Inventory.FetchItem(gestureUUID, GetClient().Self.AgentID, TimeSpan.FromSeconds(15));
|
InventoryItem itm = GetClient().Inventory.FetchItem(gestureUUID, GetClient().Self.AgentID, TimeSpan.FromSeconds(15));
|
||||||
if (itm == null)
|
if (itm == null)
|
||||||
{
|
{
|
||||||
return BasicReply("unable to get getsture from inventory", [gesture]);
|
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);
|
GetClient().Self.PlayGesture(itm.AssetUUID);
|
||||||
return BasicReply("Accepted", [gesture]);
|
return BasicReply("Accepted", [gesture]);
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +146,339 @@ namespace SecondBotEvents.Commands
|
||||||
return BasicReply("Accepted", [gesture]);
|
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")]
|
[About("Resets the animation stack for the bot")]
|
||||||
[ReturnHints("Accepted - X stopped animations")]
|
[ReturnHints("Accepted - X stopped animations")]
|
||||||
[CmdTypeDo()]
|
[CmdTypeDo()]
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,7 @@ namespace SecondBotEvents.Commands
|
||||||
Avatar.AvatarProperties props = reply.Value;
|
Avatar.AvatarProperties props = reply.Value;
|
||||||
props.ProfileImage = textureUUID;
|
props.ProfileImage = textureUUID;
|
||||||
GetClient().Self.UpdateProfile(props);
|
GetClient().Self.UpdateProfile(props);
|
||||||
|
master.DataStoreService.UpdateAvatarProperties(GetClient().Self.AgentID, props);
|
||||||
return BasicReply("ok");
|
return BasicReply("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +231,7 @@ namespace SecondBotEvents.Commands
|
||||||
{
|
{
|
||||||
return Failure("Invaild avatar uuid", [avatar]);
|
return Failure("Invaild avatar uuid", [avatar]);
|
||||||
}
|
}
|
||||||
KeyValuePair<bool, Dictionary<UUID, string>> reply = master.DataStoreService.GetAvatarPicks(avataruuid);
|
KeyValuePair<bool, Dictionary<UUID, ProfilePick>> reply = master.DataStoreService.GetAvatarPicksDetailed(avataruuid);
|
||||||
if (reply.Key == false)
|
if (reply.Key == false)
|
||||||
{
|
{
|
||||||
return Failure("Requesting avatar details [Retry later]", [avatar]);
|
return Failure("Requesting avatar details [Retry later]", [avatar]);
|
||||||
|
|
@ -259,7 +260,7 @@ namespace SecondBotEvents.Commands
|
||||||
[ReturnHints("Requesting avatar details [Retry later]")]
|
[ReturnHints("Requesting avatar details [Retry later]")]
|
||||||
[ReturnHintsFailure("about text to short")]
|
[ReturnHintsFailure("about text to short")]
|
||||||
[ReturnHintsFailure("about text to long")]
|
[ReturnHintsFailure("about text to long")]
|
||||||
[ArgHints("abouttext", "text to use in the about me area, length 10 to 300", "Text", "Hello world!")]
|
[ArgHints("abouttext", "text to use in the about me area, length 10 to 510 bytes", "Text", "Hello world!")]
|
||||||
[CmdTypeSet()]
|
[CmdTypeSet()]
|
||||||
public object SetProfileAbout(string abouttext)
|
public object SetProfileAbout(string abouttext)
|
||||||
{
|
{
|
||||||
|
|
@ -267,7 +268,7 @@ namespace SecondBotEvents.Commands
|
||||||
{
|
{
|
||||||
return Failure("About text to short", [abouttext]);
|
return Failure("About text to short", [abouttext]);
|
||||||
}
|
}
|
||||||
else if (abouttext.Length > 300)
|
else if (System.Text.Encoding.UTF8.GetByteCount(abouttext) > 510)
|
||||||
{
|
{
|
||||||
return Failure("About text to long", [abouttext]);
|
return Failure("About text to long", [abouttext]);
|
||||||
}
|
}
|
||||||
|
|
@ -279,6 +280,7 @@ namespace SecondBotEvents.Commands
|
||||||
Avatar.AvatarProperties props = reply.Value;
|
Avatar.AvatarProperties props = reply.Value;
|
||||||
props.AboutText = abouttext;
|
props.AboutText = abouttext;
|
||||||
GetClient().Self.UpdateProfile(props);
|
GetClient().Self.UpdateProfile(props);
|
||||||
|
master.DataStoreService.UpdateAvatarProperties(GetClient().Self.AgentID, props);
|
||||||
return BasicReply("ok");
|
return BasicReply("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,5 +337,187 @@ namespace SecondBotEvents.Commands
|
||||||
}
|
}
|
||||||
return BasicReply(master.DataStoreService.GetAvatarUUID(name));
|
return BasicReply(master.DataStoreService.GetAvatarUUID(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[About("Sets the bot display name")]
|
||||||
|
[CmdTypeSet()]
|
||||||
|
public object SetDisplayName(string newname)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(newname))
|
||||||
|
{
|
||||||
|
return Failure("Display name cannot be empty");
|
||||||
|
}
|
||||||
|
GetClient().Self.SetDisplayName(GetClient().Self.Name, newname);
|
||||||
|
return BasicReply("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Sets the bot first life profile text and image")]
|
||||||
|
[CmdTypeSet()]
|
||||||
|
public object SetFirstLife(string text, string image)
|
||||||
|
{
|
||||||
|
if (UUID.TryParse(image, out UUID imageUUID) == false)
|
||||||
|
{
|
||||||
|
return Failure("Invalid image uuid");
|
||||||
|
}
|
||||||
|
KeyValuePair<bool, Avatar.AvatarProperties> reply = master.DataStoreService.GetAvatarAvatarProperties(GetClient().Self.AgentID);
|
||||||
|
if (reply.Key == false)
|
||||||
|
{
|
||||||
|
return Failure("Requesting avatar details [Retry later]");
|
||||||
|
}
|
||||||
|
Avatar.AvatarProperties props = reply.Value;
|
||||||
|
props.FirstLifeText = text;
|
||||||
|
props.FirstLifeImage = imageUUID;
|
||||||
|
GetClient().Self.UpdateProfile(props);
|
||||||
|
master.DataStoreService.UpdateAvatarProperties(GetClient().Self.AgentID, props);
|
||||||
|
return BasicReply("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Creates or updates a profile pick")]
|
||||||
|
[CmdTypeSet()]
|
||||||
|
public object SetAvatarPick(string id, string name, string desc, string snapshot, string sim, string pos)
|
||||||
|
{
|
||||||
|
if (UUID.TryParse(id, out UUID pickUUID) == false)
|
||||||
|
{
|
||||||
|
return Failure("Invalid pick uuid");
|
||||||
|
}
|
||||||
|
if (UUID.TryParse(snapshot, out UUID snapshotUUID) == false)
|
||||||
|
{
|
||||||
|
snapshotUUID = UUID.Zero;
|
||||||
|
}
|
||||||
|
Vector3 position = Vector3.Zero;
|
||||||
|
if (Vector3.TryParse(pos, out Vector3 parsedPos))
|
||||||
|
{
|
||||||
|
position = parsedPos;
|
||||||
|
}
|
||||||
|
Vector3d globalPos = GetClient().Self.GlobalPosition;
|
||||||
|
if (!string.IsNullOrWhiteSpace(sim) && sim == GetClient().Network.CurrentSim.Name)
|
||||||
|
{
|
||||||
|
Utils.LongToUInts(GetClient().Network.CurrentSim.Handle, out uint x, out uint y);
|
||||||
|
globalPos = new Vector3d(x + position.X, y + position.Y, position.Z);
|
||||||
|
}
|
||||||
|
GetClient().Self.PickInfoUpdate(pickUUID, false, UUID.Zero, name, globalPos, snapshotUUID, desc);
|
||||||
|
master.DataStoreService.ClearAvatarPicksCache(GetClient().Self.AgentID);
|
||||||
|
return BasicReply("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Deletes a profile pick")]
|
||||||
|
[CmdTypeSet()]
|
||||||
|
public object DeleteAvatarPick(string id)
|
||||||
|
{
|
||||||
|
if (UUID.TryParse(id, out UUID pickUUID) == false)
|
||||||
|
{
|
||||||
|
return Failure("Invalid pick uuid");
|
||||||
|
}
|
||||||
|
GetClient().Self.PickDelete(pickUUID);
|
||||||
|
master.DataStoreService.ClearAvatarPicksCache(GetClient().Self.AgentID);
|
||||||
|
return BasicReply("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Creates or updates a classified ad")]
|
||||||
|
[CmdTypeSet()]
|
||||||
|
public object SetAvatarClassified(string id, string name, string desc, string snapshot, string category, string price, string autorenew)
|
||||||
|
{
|
||||||
|
if (UUID.TryParse(id, out UUID classifiedUUID) == false)
|
||||||
|
{
|
||||||
|
return Failure("Invalid classified uuid");
|
||||||
|
}
|
||||||
|
if (UUID.TryParse(snapshot, out UUID snapshotUUID) == false)
|
||||||
|
{
|
||||||
|
snapshotUUID = UUID.Zero;
|
||||||
|
}
|
||||||
|
int adPrice = 50;
|
||||||
|
int.TryParse(price, out adPrice);
|
||||||
|
bool renew = autorenew.ToLower() == "true";
|
||||||
|
|
||||||
|
DirectoryManager.ClassifiedCategories cat = DirectoryManager.ClassifiedCategories.Shopping;
|
||||||
|
if (Enum.TryParse(category, out DirectoryManager.ClassifiedCategories parsedCat))
|
||||||
|
{
|
||||||
|
cat = parsedCat;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetClient().Self.UpdateClassifiedInfo(classifiedUUID, cat, snapshotUUID, adPrice, name, desc, renew);
|
||||||
|
master.DataStoreService.ClearAvatarClassifiedsCache(GetClient().Self.AgentID);
|
||||||
|
return BasicReply("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Deletes a classified ad")]
|
||||||
|
[CmdTypeSet()]
|
||||||
|
public object DeleteAvatarClassified(string id)
|
||||||
|
{
|
||||||
|
if (UUID.TryParse(id, out UUID classifiedUUID) == false)
|
||||||
|
{
|
||||||
|
return Failure("Invalid classified uuid");
|
||||||
|
}
|
||||||
|
GetClient().Self.DeleteClassified(classifiedUUID);
|
||||||
|
master.DataStoreService.ClearAvatarClassifiedsCache(GetClient().Self.AgentID);
|
||||||
|
return BasicReply("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Requests the given avatars profile classifieds")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetAvatarClassifieds(string avatar)
|
||||||
|
{
|
||||||
|
ProcessAvatar(avatar);
|
||||||
|
if (avataruuid == UUID.Zero)
|
||||||
|
{
|
||||||
|
return Failure("Invaild avatar uuid", [avatar]);
|
||||||
|
}
|
||||||
|
KeyValuePair<bool, Dictionary<UUID, ClassifiedAd>> reply = master.DataStoreService.GetAvatarClassifiedsDetailed(avataruuid);
|
||||||
|
if (reply.Key == false)
|
||||||
|
{
|
||||||
|
return Failure("Requesting avatar details [Retry later]", [avatar]);
|
||||||
|
}
|
||||||
|
return BasicReply(JsonSerializer.Serialize(reply.Value, JsonOptions.UnsafeRelaxed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Requests the given avatars first life profile text")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetFirstLifeText(string avatar)
|
||||||
|
{
|
||||||
|
ProcessAvatar(avatar);
|
||||||
|
if (avataruuid == UUID.Zero)
|
||||||
|
{
|
||||||
|
return Failure("Invaild avatar uuid", [avatar]);
|
||||||
|
}
|
||||||
|
KeyValuePair<bool, Avatar.AvatarProperties> reply = master.DataStoreService.GetAvatarAvatarProperties(avataruuid);
|
||||||
|
if (reply.Key == false)
|
||||||
|
{
|
||||||
|
return Failure("Requesting avatar details [Retry later]", [avatar]);
|
||||||
|
}
|
||||||
|
return BasicReply(reply.Value.FirstLifeText.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Requests the given avatars first life profile image")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetFirstLifeImage(string avatar)
|
||||||
|
{
|
||||||
|
ProcessAvatar(avatar);
|
||||||
|
if (avataruuid == UUID.Zero)
|
||||||
|
{
|
||||||
|
return Failure("Invaild avatar uuid", [avatar]);
|
||||||
|
}
|
||||||
|
KeyValuePair<bool, Avatar.AvatarProperties> reply = master.DataStoreService.GetAvatarAvatarProperties(avataruuid);
|
||||||
|
if (reply.Key == false)
|
||||||
|
{
|
||||||
|
return Failure("Requesting avatar details [Retry later]", [avatar]);
|
||||||
|
}
|
||||||
|
return BasicReply(reply.Value.FirstLifeImage.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Requests the given avatars profile groups")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetAvatarGroups(string avatar)
|
||||||
|
{
|
||||||
|
ProcessAvatar(avatar);
|
||||||
|
if (avataruuid == UUID.Zero)
|
||||||
|
{
|
||||||
|
return Failure("Invaild avatar uuid", [avatar]);
|
||||||
|
}
|
||||||
|
KeyValuePair<bool, List<AvatarGroup>> reply = master.DataStoreService.GetAvatarGroups(avataruuid);
|
||||||
|
if (reply.Key == false)
|
||||||
|
{
|
||||||
|
return Failure("Requesting avatar details [Retry later]", [avatar]);
|
||||||
|
}
|
||||||
|
return BasicReply(JsonSerializer.Serialize(reply.Value, JsonOptions.UnsafeRelaxed));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@ namespace SecondBotEvents.Commands
|
||||||
{
|
{
|
||||||
return BasicReply(GetClient().Self.Name);
|
return BasicReply(GetClient().Self.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[About("Returns the bot's UUID")]
|
||||||
|
[ReturnHints("Bot UUID")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetBotUUID()
|
||||||
|
{
|
||||||
|
return BasicReply(GetClient().Self.AgentID.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CommandLibCall
|
public class CommandLibCall
|
||||||
|
|
|
||||||
|
|
@ -44,21 +44,48 @@ namespace SecondBotEvents.Commands
|
||||||
{
|
{
|
||||||
Dictionary<UUID, FriendInfo> FriendListCopy = GetClient().Friends.FriendList.ToDictionary(k => k.Key, v => v.Value);
|
Dictionary<UUID, FriendInfo> FriendListCopy = GetClient().Friends.FriendList.ToDictionary(k => k.Key, v => v.Value);
|
||||||
List< FriendListEntry > CleanedFriendsList = [];
|
List< FriendListEntry > CleanedFriendsList = [];
|
||||||
int index = 0;
|
|
||||||
foreach (FriendInfo A in FriendListCopy.Values)
|
foreach (FriendInfo A in FriendListCopy.Values)
|
||||||
{
|
{
|
||||||
FriendListEntry entry = new()
|
FriendListEntry entry = new()
|
||||||
{
|
{
|
||||||
id = A.UUID.ToString(),
|
id = A.UUID.ToString(),
|
||||||
name = A.Name,
|
name = A.Name,
|
||||||
online = A.IsOnline
|
online = A.IsOnline,
|
||||||
|
canSeeMeOnline = A.CanSeeMeOnline,
|
||||||
|
canSeeMeOnMap = A.CanSeeMeOnMap,
|
||||||
|
canModifyMyObjects = A.CanModifyMyObjects,
|
||||||
|
canSeeThemOnline = A.CanSeeThemOnline,
|
||||||
|
canSeeThemOnMap = A.CanSeeThemOnMap,
|
||||||
|
canModifyTheirObjects = A.CanModifyTheirObjects
|
||||||
};
|
};
|
||||||
CleanedFriendsList.Add(entry);
|
CleanedFriendsList.Add(entry);
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
return BasicReply(JsonSerializer.Serialize(CleanedFriendsList, JsonOptions.UnsafeRelaxed));
|
return BasicReply(JsonSerializer.Serialize(CleanedFriendsList, JsonOptions.UnsafeRelaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[About("Updates specific rights granted to a friend")]
|
||||||
|
[ArgHints("avatar", "The avatar to change rights for","AVATAR")]
|
||||||
|
[ArgHints("rights", "The exact integer mask of FriendRights to grant","INT")]
|
||||||
|
[CmdTypeSet()]
|
||||||
|
public object FriendSetRights(string avatar, string rights)
|
||||||
|
{
|
||||||
|
ProcessAvatar(avatar);
|
||||||
|
if (avataruuid == UUID.Zero)
|
||||||
|
{
|
||||||
|
return Failure("avatar lookup", [avatar, rights]);
|
||||||
|
}
|
||||||
|
if (int.TryParse(rights, out int mask) == false)
|
||||||
|
{
|
||||||
|
return Failure("invalid rights mask", [avatar, rights]);
|
||||||
|
}
|
||||||
|
if (GetClient().Friends.FriendList.ContainsKey(avataruuid) == false)
|
||||||
|
{
|
||||||
|
return Failure("Not A friend", [avatar, rights]);
|
||||||
|
}
|
||||||
|
GetClient().Friends.GrantRights(avataruuid, (FriendRights)mask);
|
||||||
|
return BasicReply("rights_updated", [avatar, rights]);
|
||||||
|
}
|
||||||
|
|
||||||
[About("Updates the friend perms for avatar avatar to State \n if true grants (Online/Map/Modify) perms")]
|
[About("Updates the friend perms for avatar avatar to State \n if true grants (Online/Map/Modify) perms")]
|
||||||
[ArgHints("avatar", "Who we are talking about","AVATAR")]
|
[ArgHints("avatar", "Who we are talking about","AVATAR")]
|
||||||
[ArgHints("state", "true: Grant perms, false: Remove perms","BOOL")]
|
[ArgHints("state", "true: Grant perms, false: Remove perms","BOOL")]
|
||||||
|
|
@ -186,5 +213,11 @@ namespace SecondBotEvents.Commands
|
||||||
public string name;
|
public string name;
|
||||||
public string id;
|
public string id;
|
||||||
public bool online = false;
|
public bool online = false;
|
||||||
|
public bool canSeeMeOnline = false;
|
||||||
|
public bool canSeeMeOnMap = false;
|
||||||
|
public bool canModifyMyObjects = false;
|
||||||
|
public bool canSeeThemOnline = false;
|
||||||
|
public bool canSeeThemOnMap = false;
|
||||||
|
public bool canModifyTheirObjects = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Text.Json;
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
using SecondBotEvents.Services;
|
using SecondBotEvents.Services;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SecondBotEvents.Commands
|
namespace SecondBotEvents.Commands
|
||||||
{
|
{
|
||||||
|
|
@ -354,24 +355,63 @@ namespace SecondBotEvents.Commands
|
||||||
return BasicReply("Working on it");
|
return BasicReply("Working on it");
|
||||||
}
|
}
|
||||||
|
|
||||||
[About("Requests the roles for the selected group")]
|
[About("Returns a list of roles for a group")]
|
||||||
[ReturnHints("GroupRoleDetails object")]
|
[ReturnHints("dictionary uuid:name")]
|
||||||
[ReturnHintsFailure("Group is not currently known")]
|
|
||||||
[ReturnHintsFailure("Invaild group UUID")]
|
[ReturnHintsFailure("Invaild group UUID")]
|
||||||
[ReturnHintsFailure("Updating")]
|
[ReturnHintsFailure("Updating")]
|
||||||
[ArgHints("group", "the group to get roles from", "UUID")]
|
[ArgHints("group", "the group to get roles from", "UUID")]
|
||||||
|
[ArgHints("botOnly", "only return roles the bot is in", "bool")]
|
||||||
|
[ArgHints("force", "force refresh from server", "bool")]
|
||||||
[CmdTypeGet()]
|
[CmdTypeGet()]
|
||||||
public object GetGroupRoles(string group)
|
public object GetGroupRoles(string group, string botOnly = "false", string force = "false")
|
||||||
{
|
{
|
||||||
if (UUID.TryParse(group, out UUID groupuuid) == false)
|
if (UUID.TryParse(group, out UUID groupuuid) == false)
|
||||||
{
|
{
|
||||||
return Failure("Invaild group UUID", [group]);
|
return Failure("Invaild group UUID", [group]);
|
||||||
}
|
}
|
||||||
if (GetClient().Groups.GroupName2KeyCache.ContainsKey(groupuuid) == false)
|
|
||||||
|
bool doForce = force.ToLower() == "true";
|
||||||
|
Dictionary<UUID, string> allRoles = master.DataStoreService.GetGroupRoles(groupuuid, doForce);
|
||||||
|
|
||||||
|
if (botOnly.ToLower() == "true")
|
||||||
{
|
{
|
||||||
return Failure("Unknown group", [group]);
|
Dictionary<UUID, string> botRoles = [];
|
||||||
|
AutoResetEvent fetchEvent = new(false);
|
||||||
|
void callback(object sender, GroupRolesMembersReplyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GroupID == groupuuid)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<UUID, UUID> pair in e.RolesMembers)
|
||||||
|
{
|
||||||
|
if (pair.Value == GetClient().Self.AgentID)
|
||||||
|
{
|
||||||
|
if (allRoles.ContainsKey(pair.Key))
|
||||||
|
{
|
||||||
|
botRoles[pair.Key] = allRoles[pair.Key];
|
||||||
}
|
}
|
||||||
return BasicReply(JsonSerializer.Serialize(master.DataStoreService.GetGroupRoles(groupuuid), JsonOptions.UnsafeRelaxed));
|
}
|
||||||
|
}
|
||||||
|
fetchEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GetClient().Groups.GroupRoleMembersReply += callback;
|
||||||
|
GetClient().Groups.RequestGroupRolesMembers(groupuuid);
|
||||||
|
fetchEvent.WaitOne(2000, false);
|
||||||
|
GetClient().Groups.GroupRoleMembersReply -= callback;
|
||||||
|
|
||||||
|
// Ensure 'Everyone' role is included if we missed it
|
||||||
|
foreach(var role in allRoles)
|
||||||
|
{
|
||||||
|
if (role.Value.ToLower() == "everyone" || role.Key == UUID.Zero)
|
||||||
|
{
|
||||||
|
if (!botRoles.ContainsKey(role.Key)) botRoles[role.Key] = role.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BasicReply(JsonSerializer.Serialize(botRoles, JsonOptions.UnsafeRelaxed));
|
||||||
|
}
|
||||||
|
|
||||||
|
return BasicReply(JsonSerializer.Serialize(allRoles, JsonOptions.UnsafeRelaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
[About("fetchs a list of all groups with unread messages")]
|
[About("fetchs a list of all groups with unread messages")]
|
||||||
|
|
@ -483,5 +523,12 @@ namespace SecondBotEvents.Commands
|
||||||
master.DataStoreService.RemovePendingGroupInvite(groupuuid);
|
master.DataStoreService.RemovePendingGroupInvite(groupuuid);
|
||||||
return BasicReply("Done");
|
return BasicReply("Done");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[About("Gets the currently active group and title")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetGroupStatus()
|
||||||
|
{
|
||||||
|
return BasicReply(GetClient().Self.ActiveGroup.ToString(), [master.DataStoreService.GetActiveGroupTitle(), master.DataStoreService.GetGroupName(GetClient().Self.ActiveGroup)]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,11 +179,22 @@ namespace SecondBotEvents.Commands
|
||||||
{
|
{
|
||||||
return Failure("Error not in a sim");
|
return Failure("Error not in a sim");
|
||||||
}
|
}
|
||||||
Dictionary<string, int> pos = new()
|
uint sittingOn = GetClient().Self.SittingOn;
|
||||||
|
string sittingOnUUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
if (sittingOn != 0)
|
||||||
|
{
|
||||||
|
if (GetClient().Network.CurrentSim.ObjectsPrimitives.TryGetValue(sittingOn, out Primitive prim))
|
||||||
|
{
|
||||||
|
sittingOnUUID = prim.ID.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, object> pos = new()
|
||||||
{
|
{
|
||||||
{ "x", (int)Math.Round(GetClient().Self.SimPosition.X) },
|
{ "x", (int)Math.Round(GetClient().Self.SimPosition.X) },
|
||||||
{ "y", (int)Math.Round(GetClient().Self.SimPosition.Y) },
|
{ "y", (int)Math.Round(GetClient().Self.SimPosition.Y) },
|
||||||
{ "z", (int)Math.Round(GetClient().Self.SimPosition.Z) }
|
{ "z", (int)Math.Round(GetClient().Self.SimPosition.Z) },
|
||||||
|
{ "sitting_on", sittingOnUUID }
|
||||||
};
|
};
|
||||||
return BasicReply(JsonSerializer.Serialize(pos, JsonOptions.UnsafeRelaxed));
|
return BasicReply(JsonSerializer.Serialize(pos, JsonOptions.UnsafeRelaxed));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
using OpenMetaverse.ImportExport.Collada14;
|
using OpenMetaverse.ImportExport.Collada14;
|
||||||
using SecondBotEvents.Services;
|
using SecondBotEvents.Services;
|
||||||
|
|
@ -86,6 +86,21 @@ namespace SecondBotEvents.Commands
|
||||||
return BasicReply(test.ToString());
|
return BasicReply(test.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[About("Gets the current parcel's snapshot UUID")]
|
||||||
|
[ReturnHints("snapshot uuid")]
|
||||||
|
[ReturnHintsFailure("Error not in a sim")]
|
||||||
|
[ReturnHintsFailure("Parcel data not ready")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetCurrentParcelSnapshot()
|
||||||
|
{
|
||||||
|
KeyValuePair<bool, string> tests = SetupCurrentParcel();
|
||||||
|
if (tests.Key == false)
|
||||||
|
{
|
||||||
|
return Failure(tests.Value);
|
||||||
|
}
|
||||||
|
return BasicReply(targetparcel.SnapshotID.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
[About("Requests the bot update its list of parcels ready for other commands")]
|
[About("Requests the bot update its list of parcels ready for other commands")]
|
||||||
[ReturnHints("ok")]
|
[ReturnHints("ok")]
|
||||||
[CmdTypeDo()]
|
[CmdTypeDo()]
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ namespace SecondBotEvents.Config
|
||||||
settings.Add("DefaultHoverHeight");
|
settings.Add("DefaultHoverHeight");
|
||||||
settings.Add("MAC");
|
settings.Add("MAC");
|
||||||
settings.Add("Id0");
|
settings.Add("Id0");
|
||||||
|
settings.Add("StartLocation");
|
||||||
|
settings.Add("ReconnectDelaySecs");
|
||||||
|
settings.Add("MaxReconnectDelaySecs");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetStartLocation()
|
||||||
|
{
|
||||||
|
return ReadSettingAsString("StartLocation", "last");
|
||||||
}
|
}
|
||||||
|
|
||||||
public double GetDefaultHoverHeight()
|
public double GetDefaultHoverHeight()
|
||||||
|
|
@ -67,5 +75,23 @@ namespace SecondBotEvents.Config
|
||||||
{
|
{
|
||||||
return ReadSettingAsString("Id0", "");
|
return ReadSettingAsString("Id0", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base reconnect delay in seconds. Doubles on each consecutive failure (exponential backoff).
|
||||||
|
/// Default: 120 seconds.
|
||||||
|
/// </summary>
|
||||||
|
public int GetReconnectDelaySecs()
|
||||||
|
{
|
||||||
|
return ReadSettingAsInt("ReconnectDelaySecs", 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum reconnect delay cap in seconds for exponential backoff.
|
||||||
|
/// Default: 600 seconds (10 minutes).
|
||||||
|
/// </summary>
|
||||||
|
public int GetMaxReconnectDelaySecs()
|
||||||
|
{
|
||||||
|
return ReadSettingAsInt("MaxReconnectDelaySecs", 600);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
SecondBotEvents/Config/BotRejoinConfig.cs
Normal file
20
SecondBotEvents/Config/BotRejoinConfig.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SecondBotEvents.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for bot reconnection jitter timings.
|
||||||
|
/// </summary>
|
||||||
|
public class BotRejoinConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum jitter (in milliseconds) before a bot attempts to reconnect after a restart.
|
||||||
|
/// </summary>
|
||||||
|
public int JitterMinMs { get; set; } = 5000; // 5 seconds
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum jitter (in milliseconds) before a bot attempts to reconnect after a restart.
|
||||||
|
/// </summary>
|
||||||
|
public int JitterMaxMs { get; set; } = 30000; // 30 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ namespace SecondBotEvents.Config
|
||||||
{
|
{
|
||||||
filename = "http";
|
filename = "http";
|
||||||
settings.Add("Enabled");
|
settings.Add("Enabled");
|
||||||
|
settings.Add("Port");
|
||||||
settings.Add("HideStatusOutput");
|
settings.Add("HideStatusOutput");
|
||||||
settings.Add("DisableCommandValidation");
|
settings.Add("DisableCommandValidation");
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +24,11 @@ namespace SecondBotEvents.Config
|
||||||
{
|
{
|
||||||
return ReadSettingAsBool("DisableCommandValidation", false);
|
return ReadSettingAsBool("DisableCommandValidation", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int GetPort()
|
||||||
|
{
|
||||||
|
return ReadSettingAsInt("Port", 80);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,16 @@ namespace SecondBotEvents
|
||||||
{
|
{
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// Force initialize OpenMetaverse logger early to avoid log4net repository registration errors
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var _ = OpenMetaverse.Logger.LogInstance;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Startup] Warning: Failed to initialize OpenMetaverse logger: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
#if (DEBUG)
|
#if (DEBUG)
|
||||||
new WikiMake();
|
new WikiMake();
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk;Microsoft.NET.Sdk.Publish">
|
<Project Sdk="Microsoft.NET.Sdk;Microsoft.NET.Sdk.Publish">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<StartupObject>SecondBotEvents.Startup</StartupObject>
|
<StartupObject>SecondBotEvents.Startup</StartupObject>
|
||||||
<MSBuildGitHashCommand>git rev-parse --short HEAD</MSBuildGitHashCommand>
|
<MSBuildGitHashCommand>git rev-parse --short HEAD</MSBuildGitHashCommand>
|
||||||
<MSBuildGitHashValidate>False</MSBuildGitHashValidate>
|
<MSBuildGitHashValidate>False</MSBuildGitHashValidate>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,12 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
public GridClient client = null;
|
public GridClient client = null;
|
||||||
public BasicConfig basicCfg = null;
|
public BasicConfig basicCfg = null;
|
||||||
|
private BotRejoinConfig rejoinCfg;
|
||||||
|
|
||||||
protected bool LogoutExpected = false;
|
protected bool LogoutExpected = false;
|
||||||
|
protected int consecutiveLoginFailures = 0;
|
||||||
|
protected DateTime lastConnectTime = DateTime.MinValue;
|
||||||
|
protected bool isRegionRestartScheduled = false;
|
||||||
public void flagLogoutExpected()
|
public void flagLogoutExpected()
|
||||||
{
|
{
|
||||||
LogoutExpected = true;
|
LogoutExpected = true;
|
||||||
|
|
@ -40,14 +44,19 @@ namespace SecondBotEvents.Services
|
||||||
public BotClientService(EventsSecondBot setMaster) : base(setMaster)
|
public BotClientService(EventsSecondBot setMaster) : base(setMaster)
|
||||||
{
|
{
|
||||||
basicCfg = new BasicConfig(master.fromEnv, master.fromFolder);
|
basicCfg = new BasicConfig(master.fromEnv, master.fromFolder);
|
||||||
|
rejoinCfg = new BotRejoinConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void RestartTimer(object o, ElapsedEventArgs e)
|
protected void RestartTimer(object o, ElapsedEventArgs e)
|
||||||
{
|
{
|
||||||
AutoRestartLoginTimer.Stop();
|
AutoRestartLoginTimer?.Stop();
|
||||||
AutoRestartLoginTimer = null;
|
AutoRestartLoginTimer = null;
|
||||||
LogFormater.Info("Recovery underway - waiting 4 secs then restarting bot");
|
// Ensure logger is initialized early to avoid "logger not registered" errors
|
||||||
Thread.Sleep(4000);
|
var _ = OpenMetaverse.Logger.LogInstance;
|
||||||
|
// Apply jitter before restart
|
||||||
|
int jitter = new Random().Next(rejoinCfg.JitterMinMs, rejoinCfg.JitterMaxMs);
|
||||||
|
LogFormater.Info($"Recovery sequence: Waiting {jitter}ms before restart (jitter applied)...");
|
||||||
|
Thread.Sleep(jitter);
|
||||||
Restart();
|
Restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,10 +99,56 @@ namespace SecondBotEvents.Services
|
||||||
ResetClient();
|
ResetClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void HandleUnexpectedDisconnect(string reason)
|
||||||
|
{
|
||||||
|
if (getLogoutExpected()) return;
|
||||||
|
|
||||||
|
int baseDelay = basicCfg.GetReconnectDelaySecs();
|
||||||
|
int maxDelay = basicCfg.GetMaxReconnectDelaySecs();
|
||||||
|
|
||||||
|
if (lastConnectTime != DateTime.MinValue && (DateTime.UtcNow - lastConnectTime).TotalSeconds > 60)
|
||||||
|
{
|
||||||
|
consecutiveLoginFailures = 0;
|
||||||
|
}
|
||||||
|
lastConnectTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
int delaySecs = (int)Math.Min(baseDelay * Math.Pow(2, consecutiveLoginFailures), maxDelay);
|
||||||
|
if (isRegionRestartScheduled)
|
||||||
|
{
|
||||||
|
// Force a minimum 15 minutes (900 seconds) delay if a region restart is scheduled
|
||||||
|
delaySecs = Math.Max(delaySecs, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
consecutiveLoginFailures++;
|
||||||
|
LogFormater.Warn($"{reason} - retrying in {delaySecs}s (attempt {consecutiveLoginFailures})...");
|
||||||
|
StartRecoveryTimer(delaySecs * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void BotAlertMessage(object o, AlertMessageEventArgs e)
|
||||||
|
{
|
||||||
|
LogFormater.Info("Client service ~ Alert message: " + e.Message);
|
||||||
|
if (e.Message != null && e.Message.IndexOf("restart", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
LogFormater.Warn("Detected upcoming region restart. Initiating graceful logout to prevent limbo state.");
|
||||||
|
isRegionRestartScheduled = true;
|
||||||
|
flagLogoutExpected();
|
||||||
|
if (client != null && client.Network != null)
|
||||||
|
{
|
||||||
|
client.Network.Logout();
|
||||||
|
}
|
||||||
|
// Directly start recovery timer with 15 minutes delay
|
||||||
|
StartRecoveryTimer(900 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void BotLoggedOut(object o, LoggedOutEventArgs e)
|
protected void BotLoggedOut(object o, LoggedOutEventArgs e)
|
||||||
{
|
{
|
||||||
LogFormater.Info("Client service ~ Logged out");
|
LogFormater.Info("Client service ~ Logged out");
|
||||||
master.TriggerBotClientEvent(true,!getLogoutExpected());
|
if (!getLogoutExpected())
|
||||||
|
{
|
||||||
|
HandleUnexpectedDisconnect("Unexpected logout");
|
||||||
|
}
|
||||||
|
master.TriggerBotClientEvent(true, !getLogoutExpected());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void BotSimConnected(object o, SimConnectedEventArgs e)
|
protected void BotSimConnected(object o, SimConnectedEventArgs e)
|
||||||
|
|
@ -103,28 +158,59 @@ namespace SecondBotEvents.Services
|
||||||
AutoRestartLoginTimer.Stop();
|
AutoRestartLoginTimer.Stop();
|
||||||
AutoRestartLoginTimer = null;
|
AutoRestartLoginTimer = null;
|
||||||
}
|
}
|
||||||
|
isRegionRestartScheduled = false;
|
||||||
|
lastConnectTime = DateTime.UtcNow;
|
||||||
goodticks = 0;
|
goodticks = 0;
|
||||||
LogFormater.Info("Client service ~ Connected to sim: "+e.Simulator.Name);
|
LogFormater.Info("Client service ~ Connected to sim: " + e.Simulator.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void BotSimDisconnected(object o, SimDisconnectedEventArgs e)
|
protected void BotSimDisconnected(object o, SimDisconnectedEventArgs e)
|
||||||
{
|
{
|
||||||
goodticks = 0;
|
goodticks = 0;
|
||||||
LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name);
|
LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name);
|
||||||
|
if (!getLogoutExpected())
|
||||||
|
{
|
||||||
|
HandleUnexpectedDisconnect("Unexpected sim disconnect");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void BotDisconnected(object o, DisconnectedEventArgs e)
|
protected void BotDisconnected(object o, DisconnectedEventArgs e)
|
||||||
{
|
{
|
||||||
LogFormater.Info("Client service ~ Network disconnected: " + e.Message);
|
LogFormater.Info("Client service ~ Network disconnected: " + e.Message);
|
||||||
|
if (!getLogoutExpected())
|
||||||
|
{
|
||||||
|
HandleUnexpectedDisconnect("Unexpected network disconnect: " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void StartRecoveryTimer(int intervalMs = 5000)
|
||||||
|
{
|
||||||
|
if (AutoRestartLoginTimer == null)
|
||||||
|
{
|
||||||
|
AutoRestartLoginTimer = new Timer
|
||||||
|
{
|
||||||
|
Interval = intervalMs,
|
||||||
|
AutoReset = false
|
||||||
|
};
|
||||||
|
AutoRestartLoginTimer.Elapsed += RestartTimer;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AutoRestartLoginTimer.Interval = intervalMs;
|
||||||
|
}
|
||||||
|
AutoRestartLoginTimer.Stop();
|
||||||
|
AutoRestartLoginTimer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void BotLoginStatus(object o, LoginProgressEventArgs e)
|
protected void BotLoginStatus(object o, LoginProgressEventArgs e)
|
||||||
{
|
{
|
||||||
if(e.FailReason != "")
|
if (e.Status == LoginStatus.Failed || !string.IsNullOrEmpty(e.FailReason))
|
||||||
{
|
{
|
||||||
LogFormater.Info("Client service ~ {FAILED} Login status: " + e.FailReason.ToString());
|
string reason = string.IsNullOrEmpty(e.FailReason) ? e.Message : e.FailReason;
|
||||||
|
LogFormater.Info("Client service ~ {FAILED} Login status: " + reason);
|
||||||
client.Network.Logout();
|
client.Network.Logout();
|
||||||
ResetClient();
|
ResetClient();
|
||||||
|
HandleUnexpectedDisconnect("Login failed: " + reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LogFormater.Info("Client service ~ Login status: " + e.Status.ToString());
|
LogFormater.Info("Client service ~ Login status: " + e.Status.ToString());
|
||||||
|
|
@ -209,12 +295,18 @@ namespace SecondBotEvents.Services
|
||||||
client.Network.Disconnected += BotDisconnected;
|
client.Network.Disconnected += BotDisconnected;
|
||||||
client.Network.SimDisconnected += BotSimDisconnected;
|
client.Network.SimDisconnected += BotSimDisconnected;
|
||||||
client.Network.LoginProgress += BotLoginStatus;
|
client.Network.LoginProgress += BotLoginStatus;
|
||||||
|
client.Self.AlertMessage += BotAlertMessage;
|
||||||
|
client.Self.AgentDataReply += (s, e) =>
|
||||||
|
{
|
||||||
|
master.DataStoreService.SetActiveGroupTitle(e.GroupTitle);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Login()
|
public void Login()
|
||||||
{
|
{
|
||||||
ResetClient();
|
ResetClient();
|
||||||
goodticks = 0;
|
goodticks = 0;
|
||||||
|
LogoutExpected = false;
|
||||||
master.TriggerBotClientEvent(false, false);
|
master.TriggerBotClientEvent(false, false);
|
||||||
LoginParams loginParams = new(
|
LoginParams loginParams = new(
|
||||||
client,
|
client,
|
||||||
|
|
@ -241,6 +333,12 @@ namespace SecondBotEvents.Services
|
||||||
loginParams.ID0 = customId0;
|
loginParams.ID0 = customId0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string customStart = basicCfg.GetStartLocation();
|
||||||
|
if (!string.IsNullOrEmpty(customStart))
|
||||||
|
{
|
||||||
|
loginParams.Start = customStart;
|
||||||
|
}
|
||||||
|
|
||||||
AutoRestartLoginTimer.Start();
|
AutoRestartLoginTimer.Start();
|
||||||
client.Network.BeginLogin(loginParams);
|
client.Network.BeginLogin(loginParams);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
using OpenMetaverse.ImportExport.Collada14;
|
using OpenMetaverse.ImportExport.Collada14;
|
||||||
using SecondBotEvents.Config;
|
using SecondBotEvents.Config;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ namespace SecondBotEvents.Services
|
||||||
public DataStoreService(EventsSecondBot setMaster) : base(setMaster)
|
public DataStoreService(EventsSecondBot setMaster) : base(setMaster)
|
||||||
{
|
{
|
||||||
myConfig = new DataStoreConfig(master.fromEnv,master.fromFolder);
|
myConfig = new DataStoreConfig(master.fromEnv,master.fromFolder);
|
||||||
|
LoadPendingGroupInvites();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Dictionary<string, UUID> avatarsName2Key = [];
|
protected Dictionary<string, UUID> avatarsName2Key = [];
|
||||||
|
|
@ -456,12 +457,11 @@ namespace SecondBotEvents.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<UUID, string> GetGroupRoles(UUID group)
|
public Dictionary<UUID, string> GetGroupRoles(UUID group, bool force = false)
|
||||||
{
|
{
|
||||||
Dictionary<UUID, string> reply = [];
|
Dictionary<UUID, string> reply = [];
|
||||||
if (groupRoles.ContainsKey(group) == false)
|
if (force || groupRoles.ContainsKey(group) == false)
|
||||||
{
|
{
|
||||||
|
|
||||||
AutoResetEvent fetchEvent = new(false);
|
AutoResetEvent fetchEvent = new(false);
|
||||||
void callback(object sender, GroupRolesDataReplyEventArgs e)
|
void callback(object sender, GroupRolesDataReplyEventArgs e)
|
||||||
{
|
{
|
||||||
|
|
@ -675,6 +675,49 @@ namespace SecondBotEvents.Services
|
||||||
return KnownAvatar(name, UUID.Zero);
|
return KnownAvatar(name, UUID.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string groupInvitesFilePath = "data/pending_group_invites.json";
|
||||||
|
|
||||||
|
private void SavePendingGroupInvites()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string dir = System.IO.Path.GetDirectoryName(groupInvitesFilePath);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir))
|
||||||
|
{
|
||||||
|
System.IO.Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
string json = JsonSerializer.Serialize(PendingGroupInvites, JsonOptions.UnsafeRelaxed);
|
||||||
|
System.IO.File.WriteAllText(groupInvitesFilePath, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogFormater.Crit("Failed to save pending group invites: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadPendingGroupInvites()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(groupInvitesFilePath))
|
||||||
|
{
|
||||||
|
string json = System.IO.File.ReadAllText(groupInvitesFilePath);
|
||||||
|
var loaded = JsonSerializer.Deserialize<List<GroupInvite>>(json);
|
||||||
|
if (loaded != null)
|
||||||
|
{
|
||||||
|
lock (PendingGroupInvites)
|
||||||
|
{
|
||||||
|
PendingGroupInvites = loaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogFormater.Crit("Failed to load pending group invites: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void AddPendingGroupInvite(UUID groupID, UUID agentID, string agentName, string message)
|
public void AddPendingGroupInvite(UUID groupID, UUID agentID, string agentName, string message)
|
||||||
{
|
{
|
||||||
lock (PendingGroupInvites)
|
lock (PendingGroupInvites)
|
||||||
|
|
@ -684,14 +727,50 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string groupName = "Unknown Group";
|
||||||
|
if (GetClient().Groups.GroupName2KeyCache.TryGetValue(groupID, out string resolvedCacheName))
|
||||||
|
{
|
||||||
|
groupName = resolvedCacheName;
|
||||||
|
}
|
||||||
|
else if (groupsKey2Name.TryGetValue(groupID, out string cachedName))
|
||||||
|
{
|
||||||
|
groupName = cachedName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AutoResetEvent fetchEvent = new(false);
|
||||||
|
void callback(object sender, GroupNamesEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GroupNames.TryGetValue(groupID, out string resolvedName))
|
||||||
|
{
|
||||||
|
groupName = resolvedName;
|
||||||
|
fetchEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GetClient().Groups.GroupNamesReply += callback;
|
||||||
|
GetClient().Groups.RequestGroupName(groupID);
|
||||||
|
fetchEvent.WaitOne(1500, false);
|
||||||
|
GetClient().Groups.GroupNamesReply -= callback;
|
||||||
|
|
||||||
|
if (groupName != "Unknown Group")
|
||||||
|
{
|
||||||
|
lock (groupsKey2Name)
|
||||||
|
{
|
||||||
|
groupsKey2Name[groupID] = groupName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PendingGroupInvites.Add(new GroupInvite
|
PendingGroupInvites.Add(new GroupInvite
|
||||||
{
|
{
|
||||||
GroupID = groupID,
|
GroupID = groupID,
|
||||||
GroupName = "Unknown Group", // Will be updated on lookup
|
GroupName = groupName,
|
||||||
AgentID = agentID,
|
AgentID = agentID,
|
||||||
AgentName = agentName,
|
AgentName = agentName,
|
||||||
Message = message
|
Message = message
|
||||||
});
|
});
|
||||||
|
SavePendingGroupInvites();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -708,6 +787,7 @@ namespace SecondBotEvents.Services
|
||||||
lock (PendingGroupInvites)
|
lock (PendingGroupInvites)
|
||||||
{
|
{
|
||||||
PendingGroupInvites.RemoveAll(i => i.GroupID == groupID);
|
PendingGroupInvites.RemoveAll(i => i.GroupID == groupID);
|
||||||
|
SavePendingGroupInvites();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1156,12 +1236,35 @@ namespace SecondBotEvents.Services
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<UUID, Avatar.AvatarProperties> avprops = [];
|
protected string activeGroupTitle = "";
|
||||||
Dictionary<UUID, Dictionary<UUID, string>> avpicks = [];
|
protected Dictionary<UUID, Avatar.AvatarProperties> avprops = [];
|
||||||
|
protected Dictionary<UUID, Dictionary<UUID, string>> avpicks = [];
|
||||||
|
protected Dictionary<UUID, Dictionary<UUID, string>> avclassifieds = [];
|
||||||
|
protected Dictionary<UUID, Dictionary<UUID, ProfilePick>> avpicksDetails = [];
|
||||||
|
protected Dictionary<UUID, Dictionary<UUID, ClassifiedAd>> avclassifiedsDetails = [];
|
||||||
|
protected Dictionary<UUID, List<AvatarGroup>> avgroups = [];
|
||||||
|
|
||||||
|
public void ClearAvatarPicksCache(UUID agentID)
|
||||||
|
{
|
||||||
|
avpicks.Remove(agentID);
|
||||||
|
lock (avpicksDetails)
|
||||||
|
{
|
||||||
|
avpicksDetails.Remove(agentID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAvatarClassifiedsCache(UUID agentID)
|
||||||
|
{
|
||||||
|
avclassifieds.Remove(agentID);
|
||||||
|
lock (avclassifiedsDetails)
|
||||||
|
{
|
||||||
|
avclassifiedsDetails.Remove(agentID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public KeyValuePair<bool, Dictionary<UUID, string>> GetAvatarPicks(UUID agentID)
|
public KeyValuePair<bool, Dictionary<UUID, string>> GetAvatarPicks(UUID agentID)
|
||||||
{
|
{
|
||||||
if (avpicks.ContainsKey(agentID) == false)
|
if (!avpicks.ContainsKey(agentID))
|
||||||
{
|
{
|
||||||
GetClient().Avatars.RequestAvatarPicks(agentID);
|
GetClient().Avatars.RequestAvatarPicks(agentID);
|
||||||
return new KeyValuePair<bool, Dictionary<UUID, string>>(false, []);
|
return new KeyValuePair<bool, Dictionary<UUID, string>>(false, []);
|
||||||
|
|
@ -1169,21 +1272,259 @@ namespace SecondBotEvents.Services
|
||||||
return new KeyValuePair<bool, Dictionary<UUID, string>>(true, avpicks[agentID]);
|
return new KeyValuePair<bool, Dictionary<UUID, string>>(true, avpicks[agentID]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public KeyValuePair<bool, Dictionary<UUID, ProfilePick>> GetAvatarPicksDetailed(UUID agentID)
|
||||||
|
{
|
||||||
|
if (!avpicks.ContainsKey(agentID))
|
||||||
|
{
|
||||||
|
GetClient().Avatars.RequestAvatarPicks(agentID);
|
||||||
|
return new KeyValuePair<bool, Dictionary<UUID, ProfilePick>>(false, []);
|
||||||
|
}
|
||||||
|
lock (avpicksDetails)
|
||||||
|
{
|
||||||
|
if (!avpicksDetails.ContainsKey(agentID))
|
||||||
|
{
|
||||||
|
avpicksDetails.Add(agentID, []);
|
||||||
|
foreach (UUID key in avpicks[agentID].Keys)
|
||||||
|
{
|
||||||
|
GetClient().Avatars.RequestPickInfo(agentID, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (UUID pickID in new List<UUID>(avpicksDetails[agentID].Keys))
|
||||||
|
{
|
||||||
|
ProfilePick value = avpicksDetails[agentID][pickID];
|
||||||
|
if (!string.IsNullOrEmpty(value.SimName) || !(value.PosGlobal.X > 0.0) || !(value.PosGlobal.Y > 0.0))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float localX;
|
||||||
|
float localY;
|
||||||
|
ulong handle = Helpers.GlobalPosToRegionHandle((float)value.PosGlobal.X, (float)value.PosGlobal.Y, out localX, out localY);
|
||||||
|
if (GetClient().Grid.RegionsByHandle.TryGetValue(handle, out var value2))
|
||||||
|
{
|
||||||
|
value.SimName = value2.Name;
|
||||||
|
avpicksDetails[agentID][pickID] = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Task.Run(delegate
|
||||||
|
{
|
||||||
|
if (GetClient().Grid.GetGridRegion(handle, GridLayerType.Terrain, out var region))
|
||||||
|
{
|
||||||
|
lock (avpicksDetails)
|
||||||
|
{
|
||||||
|
if (avpicksDetails.ContainsKey(agentID) && avpicksDetails[agentID].ContainsKey(pickID))
|
||||||
|
{
|
||||||
|
ProfilePick value3 = avpicksDetails[agentID][pickID];
|
||||||
|
value3.SimName = region.Name;
|
||||||
|
avpicksDetails[agentID][pickID] = value3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new KeyValuePair<bool, Dictionary<UUID, ProfilePick>>(true, avpicksDetails[agentID]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void AvatarPicksReply(object sender, AvatarPicksReplyEventArgs e)
|
protected void AvatarPicksReply(object sender, AvatarPicksReplyEventArgs e)
|
||||||
{
|
{
|
||||||
lock (avpicks)
|
lock (avpicks)
|
||||||
{
|
{
|
||||||
if (avpicks.ContainsKey(e.AvatarID) == false)
|
if (!avpicks.ContainsKey(e.AvatarID))
|
||||||
{
|
{
|
||||||
avpicks.Add(e.AvatarID, []);
|
avpicks.Add(e.AvatarID, []);
|
||||||
}
|
}
|
||||||
avpicks[e.AvatarID] = e.Picks;
|
avpicks[e.AvatarID] = e.Picks;
|
||||||
|
lock (avpicksDetails)
|
||||||
|
{
|
||||||
|
if (!avpicksDetails.ContainsKey(e.AvatarID))
|
||||||
|
{
|
||||||
|
avpicksDetails.Add(e.AvatarID, []);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<UUID> list = [];
|
||||||
|
foreach (UUID key in avpicksDetails[e.AvatarID].Keys)
|
||||||
|
{
|
||||||
|
if (!e.Picks.ContainsKey(key))
|
||||||
|
{
|
||||||
|
list.Add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (UUID item in list)
|
||||||
|
{
|
||||||
|
avpicksDetails[e.AvatarID].Remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (UUID key2 in e.Picks.Keys)
|
||||||
|
{
|
||||||
|
GetClient().Avatars.RequestPickInfo(e.AvatarID, key2);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeyValuePair<bool,Avatar.AvatarProperties> GetAvatarAvatarProperties(UUID agentID)
|
protected void PickInfoReply(object sender, PickInfoReplyEventArgs e)
|
||||||
{
|
{
|
||||||
if (avprops.ContainsKey(agentID) == false)
|
lock (avpicksDetails)
|
||||||
|
{
|
||||||
|
UUID avatarID = e.Pick.CreatorID;
|
||||||
|
if (avatarID == UUID.Zero)
|
||||||
|
{
|
||||||
|
lock (avpicks)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<UUID, Dictionary<UUID, string>> avpick in avpicks)
|
||||||
|
{
|
||||||
|
if (avpick.Value.ContainsKey(e.PickID))
|
||||||
|
{
|
||||||
|
avatarID = avpick.Key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (avatarID == UUID.Zero)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!avpicksDetails.ContainsKey(avatarID))
|
||||||
|
{
|
||||||
|
avpicksDetails.Add(avatarID, []);
|
||||||
|
}
|
||||||
|
ProfilePick pick = e.Pick;
|
||||||
|
if (string.IsNullOrEmpty(pick.SimName) && pick.PosGlobal.X > 0.0 && pick.PosGlobal.Y > 0.0)
|
||||||
|
{
|
||||||
|
float localX;
|
||||||
|
float localY;
|
||||||
|
ulong handle = Helpers.GlobalPosToRegionHandle((float)pick.PosGlobal.X, (float)pick.PosGlobal.Y, out localX, out localY);
|
||||||
|
if (GetClient().Grid.RegionsByHandle.TryGetValue(handle, out var value))
|
||||||
|
{
|
||||||
|
pick.SimName = value.Name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Task.Run(delegate
|
||||||
|
{
|
||||||
|
if (GetClient().Grid.GetGridRegion(handle, GridLayerType.Terrain, out var region))
|
||||||
|
{
|
||||||
|
lock (avpicksDetails)
|
||||||
|
{
|
||||||
|
if (avpicksDetails.ContainsKey(avatarID) && avpicksDetails[avatarID].ContainsKey(e.PickID))
|
||||||
|
{
|
||||||
|
ProfilePick value2 = avpicksDetails[avatarID][e.PickID];
|
||||||
|
value2.SimName = region.Name;
|
||||||
|
avpicksDetails[avatarID][e.PickID] = value2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avpicksDetails[avatarID][e.PickID] = pick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyValuePair<bool, Dictionary<UUID, string>> GetAvatarClassifieds(UUID agentID)
|
||||||
|
{
|
||||||
|
if (!avclassifieds.ContainsKey(agentID))
|
||||||
|
{
|
||||||
|
GetClient().Avatars.RequestAvatarClassified(agentID);
|
||||||
|
return new KeyValuePair<bool, Dictionary<UUID, string>>(false, []);
|
||||||
|
}
|
||||||
|
return new KeyValuePair<bool, Dictionary<UUID, string>>(true, avclassifieds[agentID]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyValuePair<bool, Dictionary<UUID, ClassifiedAd>> GetAvatarClassifiedsDetailed(UUID agentID)
|
||||||
|
{
|
||||||
|
if (!avclassifieds.ContainsKey(agentID))
|
||||||
|
{
|
||||||
|
GetClient().Avatars.RequestAvatarClassified(agentID);
|
||||||
|
return new KeyValuePair<bool, Dictionary<UUID, ClassifiedAd>>(false, []);
|
||||||
|
}
|
||||||
|
lock (avclassifiedsDetails)
|
||||||
|
{
|
||||||
|
if (!avclassifiedsDetails.ContainsKey(agentID))
|
||||||
|
{
|
||||||
|
avclassifiedsDetails.Add(agentID, []);
|
||||||
|
foreach (UUID key in avclassifieds[agentID].Keys)
|
||||||
|
{
|
||||||
|
GetClient().Avatars.RequestClassifiedInfo(agentID, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new KeyValuePair<bool, Dictionary<UUID, ClassifiedAd>>(true, avclassifiedsDetails[agentID]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AvatarClassifiedsReply(object sender, AvatarClassifiedReplyEventArgs e)
|
||||||
|
{
|
||||||
|
lock (avclassifieds)
|
||||||
|
{
|
||||||
|
if (!avclassifieds.ContainsKey(e.AvatarID))
|
||||||
|
{
|
||||||
|
avclassifieds.Add(e.AvatarID, []);
|
||||||
|
}
|
||||||
|
avclassifieds[e.AvatarID] = e.Classifieds;
|
||||||
|
lock (avclassifiedsDetails)
|
||||||
|
{
|
||||||
|
if (!avclassifiedsDetails.ContainsKey(e.AvatarID))
|
||||||
|
{
|
||||||
|
avclassifiedsDetails.Add(e.AvatarID, []);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<UUID> list = [];
|
||||||
|
foreach (UUID key in avclassifiedsDetails[e.AvatarID].Keys)
|
||||||
|
{
|
||||||
|
if (!e.Classifieds.ContainsKey(key))
|
||||||
|
{
|
||||||
|
list.Add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (UUID item in list)
|
||||||
|
{
|
||||||
|
avclassifiedsDetails[e.AvatarID].Remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (UUID key2 in e.Classifieds.Keys)
|
||||||
|
{
|
||||||
|
GetClient().Avatars.RequestClassifiedInfo(e.AvatarID, key2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ClassifiedInfoReply(object sender, ClassifiedInfoReplyEventArgs e)
|
||||||
|
{
|
||||||
|
lock (avclassifiedsDetails)
|
||||||
|
{
|
||||||
|
UUID uUID = UUID.Zero;
|
||||||
|
lock (avclassifieds)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<UUID, Dictionary<UUID, string>> avclassified in avclassifieds)
|
||||||
|
{
|
||||||
|
if (avclassified.Value.ContainsKey(e.ClassifiedID))
|
||||||
|
{
|
||||||
|
uUID = avclassified.Key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uUID != UUID.Zero)
|
||||||
|
{
|
||||||
|
if (!avclassifiedsDetails.ContainsKey(uUID))
|
||||||
|
{
|
||||||
|
avclassifiedsDetails.Add(uUID, []);
|
||||||
|
}
|
||||||
|
avclassifiedsDetails[uUID][e.ClassifiedID] = e.Classified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyValuePair<bool, Avatar.AvatarProperties> GetAvatarAvatarProperties(UUID agentID)
|
||||||
|
{
|
||||||
|
if (!avprops.ContainsKey(agentID))
|
||||||
{
|
{
|
||||||
GetClient().Avatars.RequestAvatarProperties(agentID);
|
GetClient().Avatars.RequestAvatarProperties(agentID);
|
||||||
return new KeyValuePair<bool, Avatar.AvatarProperties>(false, new Avatar.AvatarProperties());
|
return new KeyValuePair<bool, Avatar.AvatarProperties>(false, new Avatar.AvatarProperties());
|
||||||
|
|
@ -1191,15 +1532,51 @@ namespace SecondBotEvents.Services
|
||||||
return new KeyValuePair<bool, Avatar.AvatarProperties>(true, avprops[agentID]);
|
return new KeyValuePair<bool, Avatar.AvatarProperties>(true, avprops[agentID]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateAvatarProperties(UUID agentID, Avatar.AvatarProperties properties)
|
||||||
|
{
|
||||||
|
lock (avprops)
|
||||||
|
{
|
||||||
|
avprops[agentID] = properties;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void AvatarDetailsReply(object sender, AvatarPropertiesReplyEventArgs e)
|
protected void AvatarDetailsReply(object sender, AvatarPropertiesReplyEventArgs e)
|
||||||
{
|
{
|
||||||
if(avprops.ContainsKey(e.AvatarID) == false)
|
if (!avprops.ContainsKey(e.AvatarID))
|
||||||
{
|
{
|
||||||
avprops.Add(e.AvatarID,new Avatar.AvatarProperties());
|
avprops.Add(e.AvatarID, new Avatar.AvatarProperties());
|
||||||
}
|
}
|
||||||
avprops[e.AvatarID] = e.Properties;
|
avprops[e.AvatarID] = e.Properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public KeyValuePair<bool, List<AvatarGroup>> GetAvatarGroups(UUID agentID)
|
||||||
|
{
|
||||||
|
if (!avgroups.ContainsKey(agentID))
|
||||||
|
{
|
||||||
|
GetClient().Avatars.RequestAvatarProperties(agentID);
|
||||||
|
return new KeyValuePair<bool, List<AvatarGroup>>(false, []);
|
||||||
|
}
|
||||||
|
return new KeyValuePair<bool, List<AvatarGroup>>(true, avgroups[agentID]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AvatarGroupsReplyCallback(object sender, AvatarGroupsReplyEventArgs e)
|
||||||
|
{
|
||||||
|
lock (avgroups)
|
||||||
|
{
|
||||||
|
avgroups[e.AvatarID] = e.Groups;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetActiveGroupTitle()
|
||||||
|
{
|
||||||
|
return activeGroupTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetActiveGroupTitle(string title)
|
||||||
|
{
|
||||||
|
activeGroupTitle = title;
|
||||||
|
}
|
||||||
|
|
||||||
protected void AttachEventsAfterDelay()
|
protected void AttachEventsAfterDelay()
|
||||||
{
|
{
|
||||||
GetClient().Groups.CurrentGroups += GroupCurrent;
|
GetClient().Groups.CurrentGroups += GroupCurrent;
|
||||||
|
|
@ -1215,7 +1592,11 @@ namespace SecondBotEvents.Services
|
||||||
GetClient().Estate.EstateBansReply += EstateBans;
|
GetClient().Estate.EstateBansReply += EstateBans;
|
||||||
GetClient().Friends.FriendFoundReply += FindFriendReply;
|
GetClient().Friends.FriendFoundReply += FindFriendReply;
|
||||||
GetClient().Avatars.AvatarPropertiesReply += AvatarDetailsReply;
|
GetClient().Avatars.AvatarPropertiesReply += AvatarDetailsReply;
|
||||||
|
GetClient().Avatars.AvatarGroupsReply += AvatarGroupsReplyCallback;
|
||||||
GetClient().Avatars.AvatarPicksReply += AvatarPicksReply;
|
GetClient().Avatars.AvatarPicksReply += AvatarPicksReply;
|
||||||
|
GetClient().Avatars.AvatarClassifiedReply += AvatarClassifiedsReply;
|
||||||
|
GetClient().Avatars.PickInfoReply += PickInfoReply;
|
||||||
|
GetClient().Avatars.ClassifiedInfoReply += ClassifiedInfoReply;
|
||||||
GetClient().Groups.RequestCurrentGroups();
|
GetClient().Groups.RequestCurrentGroups();
|
||||||
_ = GetClient().Self.RetrieveInstantMessages();
|
_ = GetClient().Self.RetrieveInstantMessages();
|
||||||
if(myConfig.GetPrefetchInventory() == true)
|
if(myConfig.GetPrefetchInventory() == true)
|
||||||
|
|
@ -1591,6 +1972,11 @@ namespace SecondBotEvents.Services
|
||||||
GetClient().Estate.EstateBansReply -= EstateBans;
|
GetClient().Estate.EstateBansReply -= EstateBans;
|
||||||
GetClient().Friends.FriendFoundReply -= FindFriendReply;
|
GetClient().Friends.FriendFoundReply -= FindFriendReply;
|
||||||
GetClient().Avatars.AvatarPropertiesReply -= AvatarDetailsReply;
|
GetClient().Avatars.AvatarPropertiesReply -= AvatarDetailsReply;
|
||||||
|
GetClient().Avatars.AvatarGroupsReply -= AvatarGroupsReplyCallback;
|
||||||
|
GetClient().Avatars.AvatarPicksReply -= AvatarPicksReply;
|
||||||
|
GetClient().Avatars.AvatarClassifiedReply -= AvatarClassifiedsReply;
|
||||||
|
GetClient().Avatars.PickInfoReply -= PickInfoReply;
|
||||||
|
GetClient().Avatars.ClassifiedInfoReply -= ClassifiedInfoReply;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,7 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
GetClient().Self.ScriptDialog -= DialogWindowEvent;
|
GetClient().Self.ScriptDialog -= DialogWindowEvent;
|
||||||
GetClient().Self.LoadURL -= LoadUrlEvent;
|
GetClient().Self.LoadURL -= LoadUrlEvent;
|
||||||
|
GetClient().Self.AlertMessage -= BotAlertMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,6 +274,23 @@ namespace SecondBotEvents.Services
|
||||||
// attach events
|
// attach events
|
||||||
GetClient().Self.ScriptDialog += DialogWindowEvent;
|
GetClient().Self.ScriptDialog += DialogWindowEvent;
|
||||||
GetClient().Self.LoadURL += LoadUrlEvent;
|
GetClient().Self.LoadURL += LoadUrlEvent;
|
||||||
|
GetClient().Self.AlertMessage += BotAlertMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Attach()
|
||||||
|
{
|
||||||
|
botConnected = true;
|
||||||
|
// attach events
|
||||||
|
GetClient().Self.ScriptDialog += DialogWindowEvent;
|
||||||
|
GetClient().Self.LoadURL += LoadUrlEvent;
|
||||||
|
GetClient().Self.AlertMessage += BotAlertMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void BotAlertMessage(object sender, AlertMessageEventArgs e)
|
||||||
|
{
|
||||||
|
string message = " [SYSTEM] ALERT: " + e.Message;
|
||||||
|
master.DataStoreService.BotRecordLocalchatReply(message, "[SYSTEM] ");
|
||||||
|
LogFormater.Info("[AlertMessage] " + e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void LoadUrlEvent(object sender, LoadUrlEventArgs e)
|
protected void LoadUrlEvent(object sender, LoadUrlEventArgs e)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Discord;
|
using Discord;
|
||||||
using Discord.Rest;
|
using Discord.Rest;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
|
|
@ -1300,7 +1300,7 @@ namespace SecondBotEvents.Services
|
||||||
SocketRole role = server.GetRole(roleid);
|
SocketRole role = server.GetRole(roleid);
|
||||||
Dictionary<string, string> settings = new()
|
Dictionary<string, string> settings = new()
|
||||||
{
|
{
|
||||||
{ "Config|Color", String.Join(",", [role.Color.R, role.Color.G, role.Color.B]) },
|
{ "Config|Color", $"{role.Color.R},{role.Color.G},{role.Color.B}" },
|
||||||
{ "Config|Name", role.Name },
|
{ "Config|Name", role.Name },
|
||||||
{ "Info|AssignedCount", role.Members.ToString() },
|
{ "Info|AssignedCount", role.Members.ToString() },
|
||||||
{ "Perm|AttachFiles", role.Permissions.AttachFiles.ToString() },
|
{ "Perm|AttachFiles", role.Permissions.AttachFiles.ToString() },
|
||||||
|
|
|
||||||
|
|
@ -127,9 +127,11 @@ namespace SecondBotEvents.Services
|
||||||
|
|
||||||
private void CreateWebServer()
|
private void CreateWebServer()
|
||||||
{
|
{
|
||||||
Swan.Logging.Logger.UnregisterLogger<ConsoleLogger>();
|
// Swan.Logging.Logger.UnregisterLogger<ConsoleLogger>();
|
||||||
|
string hostName = System.Net.Dns.GetHostName();
|
||||||
|
int port = myConfig.GetPort();
|
||||||
HTTPendpoint = new WebServer(o => o
|
HTTPendpoint = new WebServer(o => o
|
||||||
.WithUrlPrefix("http://*:80")
|
.WithUrlPrefix($"http://{hostName}:{port}/")
|
||||||
.WithMode(HttpListenerMode.EmbedIO))
|
.WithMode(HttpListenerMode.EmbedIO))
|
||||||
.WithCors()
|
.WithCors()
|
||||||
.WithWebApi("/api", SerializationCallback, m => m.WithController(() => new HttpWebBot(master)))
|
.WithWebApi("/api", SerializationCallback, m => m.WithController(() => new HttpWebBot(master)))
|
||||||
|
|
@ -137,7 +139,7 @@ namespace SecondBotEvents.Services
|
||||||
.WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new { Message = "Error" }))
|
.WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new { Message = "Error" }))
|
||||||
);
|
);
|
||||||
HTTPendpoint.RunAsync();
|
HTTPendpoint.RunAsync();
|
||||||
LogFormater.Info("HTTP service [Enabled] on port 80");
|
LogFormater.Info($"HTTP service [Enabled] on {hostName}:{port}");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string JsonInput(IHttpContext httpContext)
|
protected string JsonInput(IHttpContext httpContext)
|
||||||
|
|
@ -399,9 +401,9 @@ namespace SecondBotEvents.Services
|
||||||
public string Run([FormField] string commandName, [FormField] string args, [FormField] string signing, [FormField] string unixtime)
|
public string Run([FormField] string commandName, [FormField] string args, [FormField] string signing, [FormField] string unixtime)
|
||||||
{
|
{
|
||||||
string[] myArgs = [];
|
string[] myArgs = [];
|
||||||
if(args != null)
|
if(!string.IsNullOrEmpty(args))
|
||||||
{
|
{
|
||||||
myArgs = args.Split("~#~", StringSplitOptions.RemoveEmptyEntries);
|
myArgs = args.Split("~#~", StringSplitOptions.None);
|
||||||
}
|
}
|
||||||
if(int.TryParse(unixtime, out int cmdUnixtime) == false)
|
if(int.TryParse(unixtime, out int cmdUnixtime) == false)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using SecondBotEvents.Config;
|
using SecondBotEvents.Config;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -209,6 +209,7 @@ namespace SecondBotEvents.Services
|
||||||
connection.CloseAsync().Await();
|
connection.CloseAsync().Await();
|
||||||
connection = null;
|
connection = null;
|
||||||
}
|
}
|
||||||
|
channels.Clear();
|
||||||
var endpoints = new System.Collections.Generic.List<AmqpTcpEndpoint> {
|
var endpoints = new System.Collections.Generic.List<AmqpTcpEndpoint> {
|
||||||
new AmqpTcpEndpoint(myConfig.GetHostIP(),myConfig.GetHostPort()),
|
new AmqpTcpEndpoint(myConfig.GetHostIP(),myConfig.GetHostPort()),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue