Compare commits

..

No commits in common. "f9094f6fca936a5f29bbcbd3a1fcbf64aac446dc" and "27cf89c736432c6fc693a59e2992e8b3ce20839f" have entirely different histories.

28 changed files with 101 additions and 1543 deletions

View file

@ -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.
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["NuGet.Config", "."]
COPY ["SecondBotEvents/SecondBotEvents.csproj", "SecondBotEvents/"]
@ -27,8 +27,6 @@ ENV basic_Username='' \
basic_LoginURI='secondlife' \
basic_LogCommands='true' \
basic_DefaultHoverHeight=0.1 \
basic_MAC='' \
basic_Id0='' \
commands_AllowFundsCommands='false' \
commands_AllowIMcontrol='false' \
commands_SharedSecret='examplecode' \

View file

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>LibreMetaverse.StructuredData</AssemblyName>
<Description>LibreMetaverse structured data library</Description>
<PackageId>LibreMetaverse.StructuredData</PackageId>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
<OutputType>Library</OutputType>
<RootNamespace>LibreMetaverse.StructuredData</RootNamespace>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>

View file

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>LibreMetaverse.Types</AssemblyName>
<PackageId>LibreMetaverse.Types</PackageId>
<Description>LibreMetaverse type library</Description>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
<OutputType>Library</OutputType>
<RootNamespace>LibreMetaverse</RootNamespace>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>

View file

@ -375,11 +375,11 @@ namespace OpenMetaverse
public ulong TimeOfDay { get; private set; }
/// <summary>A dictionary of all the regions, indexed by region name</summary>
public Dictionary<string, GridRegion> Regions = new Dictionary<string, GridRegion>();
internal Dictionary<string, GridRegion> Regions = new Dictionary<string, GridRegion>();
/// <summary>A dictionary of all the regions, indexed by region handle</summary>
public Dictionary<ulong, GridRegion> RegionsByHandle = new Dictionary<ulong, GridRegion>();
internal Dictionary<ulong, GridRegion> RegionsByHandle = new Dictionary<ulong, GridRegion>();
/// <summary>A dictionary of regions by region handle</summary>
public Dictionary<UUID, ulong> RegionsByUUID = new Dictionary<UUID, ulong>();
internal Dictionary<UUID, ulong> RegionsByUUID = new Dictionary<UUID, ulong>();
private readonly GridClient Client;

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>LibreMetaverse</AssemblyName>
<PackageId>LibreMetaverse</PackageId>
@ -12,7 +12,7 @@
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<NoWarn>$(NoWarn);CS0419;CS8981;CS1591;CS1574</NoWarn>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
<PlatformTarget>AnyCPU</PlatformTarget>
<OutputPath>..\bin\</OutputPath>
</PropertyGroup>

View file

@ -1,5 +1,4 @@
using OpenMetaverse;
using OpenMetaverse.Assets;
using OpenMetaverse;
using SecondBotEvents.Services;
using System;
using System.Collections.Generic;
@ -8,90 +7,8 @@ using System.Linq;
namespace SecondBotEvents.Commands
{
[ClassInfo("Animations and Gestures")]
public class AnimationCommands(EventsSecondBot setmaster) : CommandsAPI(setmaster)
internal 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")]
@ -108,7 +25,7 @@ namespace SecondBotEvents.Commands
// @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")]
@ -124,10 +41,8 @@ namespace SecondBotEvents.Commands
InventoryItem itm = GetClient().Inventory.FetchItem(gestureUUID, GetClient().Self.AgentID, TimeSpan.FromSeconds(15));
if (itm == null)
{
return BasicReply("unable to get gesture from inventory", [gesture]);
return BasicReply("unable to get getsture 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]);
}
@ -146,339 +61,6 @@ namespace SecondBotEvents.Commands
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()]

View file

@ -193,7 +193,6 @@ namespace SecondBotEvents.Commands
Avatar.AvatarProperties props = reply.Value;
props.ProfileImage = textureUUID;
GetClient().Self.UpdateProfile(props);
master.DataStoreService.UpdateAvatarProperties(GetClient().Self.AgentID, props);
return BasicReply("ok");
}
@ -231,7 +230,7 @@ namespace SecondBotEvents.Commands
{
return Failure("Invaild avatar uuid", [avatar]);
}
KeyValuePair<bool, Dictionary<UUID, ProfilePick>> reply = master.DataStoreService.GetAvatarPicksDetailed(avataruuid);
KeyValuePair<bool, Dictionary<UUID, string>> reply = master.DataStoreService.GetAvatarPicks(avataruuid);
if (reply.Key == false)
{
return Failure("Requesting avatar details [Retry later]", [avatar]);
@ -260,7 +259,7 @@ namespace SecondBotEvents.Commands
[ReturnHints("Requesting avatar details [Retry later]")]
[ReturnHintsFailure("about text to short")]
[ReturnHintsFailure("about text to long")]
[ArgHints("abouttext", "text to use in the about me area, length 10 to 510 bytes", "Text", "Hello world!")]
[ArgHints("abouttext", "text to use in the about me area, length 10 to 300", "Text", "Hello world!")]
[CmdTypeSet()]
public object SetProfileAbout(string abouttext)
{
@ -268,7 +267,7 @@ namespace SecondBotEvents.Commands
{
return Failure("About text to short", [abouttext]);
}
else if (System.Text.Encoding.UTF8.GetByteCount(abouttext) > 510)
else if (abouttext.Length > 300)
{
return Failure("About text to long", [abouttext]);
}
@ -280,7 +279,6 @@ namespace SecondBotEvents.Commands
Avatar.AvatarProperties props = reply.Value;
props.AboutText = abouttext;
GetClient().Self.UpdateProfile(props);
master.DataStoreService.UpdateAvatarProperties(GetClient().Self.AgentID, props);
return BasicReply("ok");
}
@ -316,11 +314,11 @@ namespace SecondBotEvents.Commands
[CmdTypeDo()]
public object Key2Name(string uuid)
{
if (UUID.TryParse(uuid, out UUID parsedUUID) == false)
if (UUID.TryParse(uuid, out _) == false)
{
return Failure("Not a vaild UUID");
}
return BasicReply(master.DataStoreService.GetAvatarName(parsedUUID));
return BasicReply(master.DataStoreService.GetAvatarName(UUID.Zero));
}
[About("searchs the AV database if not found triggers a lookup")]
@ -337,187 +335,5 @@ namespace SecondBotEvents.Commands
}
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));
}
}
}

View file

@ -32,6 +32,7 @@ namespace SecondBotEvents.Commands
return Failure("Invaild channel", [channel, message]);
}
string typeTag = (channelnum == 0) ? "" : $"[Channel {channelnum}] ";
master.DataStoreService.BotRecordLocalchatReply(message, typeTag);
GetClient().Self.Chat(message, channelnum, ChatType.Normal);
return BasicReply("ok");
}
@ -52,6 +53,7 @@ namespace SecondBotEvents.Commands
{
return Failure("Invaild channel", [channel, message]);
}
master.DataStoreService.BotRecordLocalchatReply(message, "[Shout] ");
GetClient().Self.Chat(message, channelnum, ChatType.Shout);
return BasicReply("ok");
}
@ -72,6 +74,7 @@ namespace SecondBotEvents.Commands
{
return Failure("Invaild channel", [channel, message]);
}
master.DataStoreService.BotRecordLocalchatReply(message, "[Whisper] ");
GetClient().Self.Chat(message, channelnum, ChatType.Whisper);
return BasicReply("ok");
}

View file

@ -20,14 +20,6 @@ namespace SecondBotEvents.Commands
{
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

View file

@ -1,7 +1,5 @@
using OpenMetaverse;
using OpenMetaverse;
using SecondBotEvents.Services;
using System.Text.Json;
using System.Collections.Generic;
namespace SecondBotEvents.Commands
@ -86,13 +84,5 @@ namespace SecondBotEvents.Commands
return BasicReply(master.DialogService.DialogAction(dialogidnum, buttontext));
}
[About("Lists all active dialogs currently waiting for a response")]
[ReturnHints("A JSON array of active dialogs")]
[CmdTypeGet()]
public object GetDialogs()
{
var dialogs = master.DialogService.GetActiveDialogs();
return BasicReply(JsonSerializer.Serialize(dialogs, JsonOptions.UnsafeRelaxed));
}
}
}

View file

@ -44,48 +44,21 @@ namespace SecondBotEvents.Commands
{
Dictionary<UUID, FriendInfo> FriendListCopy = GetClient().Friends.FriendList.ToDictionary(k => k.Key, v => v.Value);
List< FriendListEntry > CleanedFriendsList = [];
int index = 0;
foreach (FriendInfo A in FriendListCopy.Values)
{
FriendListEntry entry = new()
{
id = A.UUID.ToString(),
name = A.Name,
online = A.IsOnline,
canSeeMeOnline = A.CanSeeMeOnline,
canSeeMeOnMap = A.CanSeeMeOnMap,
canModifyMyObjects = A.CanModifyMyObjects,
canSeeThemOnline = A.CanSeeThemOnline,
canSeeThemOnMap = A.CanSeeThemOnMap,
canModifyTheirObjects = A.CanModifyTheirObjects
online = A.IsOnline
};
CleanedFriendsList.Add(entry);
index++;
}
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")]
[ArgHints("avatar", "Who we are talking about","AVATAR")]
[ArgHints("state", "true: Grant perms, false: Remove perms","BOOL")]
@ -213,11 +186,5 @@ namespace SecondBotEvents.Commands
public string name;
public string id;
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;
}
}

View file

@ -2,7 +2,6 @@ using System.Text.Json;
using OpenMetaverse;
using SecondBotEvents.Services;
using System.Collections.Generic;
using System.Threading;
namespace SecondBotEvents.Commands
{
@ -355,63 +354,24 @@ namespace SecondBotEvents.Commands
return BasicReply("Working on it");
}
[About("Returns a list of roles for a group")]
[ReturnHints("dictionary uuid:name")]
[About("Requests the roles for the selected group")]
[ReturnHints("GroupRoleDetails object")]
[ReturnHintsFailure("Group is not currently known")]
[ReturnHintsFailure("Invaild group UUID")]
[ReturnHintsFailure("Updating")]
[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()]
public object GetGroupRoles(string group, string botOnly = "false", string force = "false")
public object GetGroupRoles(string group)
{
if (UUID.TryParse(group, out UUID groupuuid) == false)
{
return Failure("Invaild group UUID", [group]);
}
bool doForce = force.ToLower() == "true";
Dictionary<UUID, string> allRoles = master.DataStoreService.GetGroupRoles(groupuuid, doForce);
if (botOnly.ToLower() == "true")
if (GetClient().Groups.GroupName2KeyCache.ContainsKey(groupuuid) == false)
{
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];
}
}
}
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 Failure("Unknown group", [group]);
}
return BasicReply(JsonSerializer.Serialize(allRoles, JsonOptions.UnsafeRelaxed));
return BasicReply(JsonSerializer.Serialize(master.DataStoreService.GetGroupRoles(groupuuid), JsonOptions.UnsafeRelaxed));
}
[About("fetchs a list of all groups with unread messages")]
@ -495,6 +455,7 @@ namespace SecondBotEvents.Commands
return Failure("Unknown group", [group]);
}
GetClient().Self.InstantMessageGroup(groupUUID, message);
master.DataStoreService.BotRecordReplyIM(groupUUID, message);
return BasicReply("Sending");
}
[About("fetchs a list of all pending group invitations")]
@ -523,12 +484,5 @@ namespace SecondBotEvents.Commands
master.DataStoreService.RemovePendingGroupInvite(groupuuid);
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)]);
}
}
}

View file

@ -179,22 +179,11 @@ namespace SecondBotEvents.Commands
{
return Failure("Error not in a sim");
}
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()
Dictionary<string, int> pos = new()
{
{ "x", (int)Math.Round(GetClient().Self.SimPosition.X) },
{ "y", (int)Math.Round(GetClient().Self.SimPosition.Y) },
{ "z", (int)Math.Round(GetClient().Self.SimPosition.Z) },
{ "sitting_on", sittingOnUUID }
{ "z", (int)Math.Round(GetClient().Self.SimPosition.Z) }
};
return BasicReply(JsonSerializer.Serialize(pos, JsonOptions.UnsafeRelaxed));
}
@ -211,59 +200,6 @@ namespace SecondBotEvents.Commands
if (rules == null) return BasicReply("[]");
return BasicReply(JsonSerializer.Serialize(rules, JsonOptions.UnsafeRelaxed));
}
[About("Lists objects near the bot (within 20m)")]
[ReturnHints("A JSON array of nearby objects")]
[CmdTypeGet()]
public object GetNearbyObjects()
{
if (GetClient().Network.CurrentSim == null) return Failure("Not in a sim");
Vector3 botPos = GetClient().Self.SimPosition;
uint sittingOn = GetClient().Self.SittingOn;
List<uint> localIDsToRequest = [];
// First pass: Find objects and collect localIDs for properties we don't have
foreach (var prim in GetClient().Network.CurrentSim.ObjectsPrimitives.Values)
{
if (prim.ParentID != 0) continue;
if (Vector3.Distance(botPos, prim.Position) <= 20.0f)
{
if (prim.Properties == null) localIDsToRequest.Add(prim.LocalID);
}
}
// If we have missing names, request them and wait a short moment
if (localIDsToRequest.Count > 0)
{
GetClient().Objects.SelectObjects(GetClient().Network.CurrentSim, localIDsToRequest.ToArray());
Thread.Sleep(500); // Wait for properties to arrive
}
List<object> reply = [];
foreach (var prim in GetClient().Network.CurrentSim.ObjectsPrimitives.Values)
{
if (prim.ParentID != 0) continue;
float dist = Vector3.Distance(botPos, prim.Position);
if (dist <= 20.0f)
{
string name = prim.Properties?.Name;
if (string.IsNullOrEmpty(name)) name = "Object #" + prim.LocalID;
reply.Add(new {
name = name,
uuid = prim.ID.ToString(),
local_id = prim.LocalID,
dist = Math.Round(dist, 1),
pos = prim.Position.ToString(),
is_sitting = (prim.LocalID == sittingOn)
});
}
}
return BasicReply(JsonSerializer.Serialize(reply, JsonOptions.UnsafeRelaxed));
}
}
public class SculptysInfo

View file

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using OpenMetaverse;
using OpenMetaverse.ImportExport.Collada14;
using SecondBotEvents.Services;
@ -86,21 +86,6 @@ namespace SecondBotEvents.Commands
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")]
[ReturnHints("ok")]
[CmdTypeDo()]

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
@ -14,16 +14,6 @@ namespace SecondBotEvents.Config
settings.Add("LoginURI");
settings.Add("LogCommands");
settings.Add("DefaultHoverHeight");
settings.Add("MAC");
settings.Add("Id0");
settings.Add("StartLocation");
settings.Add("ReconnectDelaySecs");
settings.Add("MaxReconnectDelaySecs");
}
public string GetStartLocation()
{
return ReadSettingAsString("StartLocation", "last");
}
public double GetDefaultHoverHeight()
@ -65,33 +55,5 @@ namespace SecondBotEvents.Config
{
return ReadSettingAsString("LoginURI", "secondlife");
}
public string GetMAC()
{
return ReadSettingAsString("MAC", "");
}
public string GetId0()
{
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);
}
}
}

View file

@ -1,20 +0,0 @@
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
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
@ -10,7 +10,6 @@ namespace SecondBotEvents.Config
{
filename = "http";
settings.Add("Enabled");
settings.Add("Port");
settings.Add("HideStatusOutput");
settings.Add("DisableCommandValidation");
}
@ -24,11 +23,6 @@ namespace SecondBotEvents.Config
{
return ReadSettingAsBool("DisableCommandValidation", false);
}
public int GetPort()
{
return ReadSettingAsInt("Port", 80);
}
}
}

View file

@ -11,16 +11,6 @@ namespace SecondBotEvents
{
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)
new WikiMake();
#endif

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk;Microsoft.NET.Sdk.Publish">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<StartupObject>SecondBotEvents.Startup</StartupObject>
<MSBuildGitHashCommand>git rev-parse --short HEAD</MSBuildGitHashCommand>
<MSBuildGitHashValidate>False</MSBuildGitHashValidate>

View file

@ -12,12 +12,8 @@ namespace SecondBotEvents.Services
{
public GridClient client = null;
public BasicConfig basicCfg = null;
private BotRejoinConfig rejoinCfg;
protected bool LogoutExpected = false;
protected int consecutiveLoginFailures = 0;
protected DateTime lastConnectTime = DateTime.MinValue;
protected bool isRegionRestartScheduled = false;
public void flagLogoutExpected()
{
LogoutExpected = true;
@ -44,21 +40,16 @@ private BotRejoinConfig rejoinCfg;
public BotClientService(EventsSecondBot setMaster) : base(setMaster)
{
basicCfg = new BasicConfig(master.fromEnv, master.fromFolder);
rejoinCfg = new BotRejoinConfig();
}
protected void RestartTimer(object o, ElapsedEventArgs e)
{
AutoRestartLoginTimer?.Stop();
AutoRestartLoginTimer = null;
// Ensure logger is initialized early to avoid "logger not registered" errors
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();
}
protected void RestartTimer(object o, ElapsedEventArgs e)
{
AutoRestartLoginTimer.Stop();
AutoRestartLoginTimer = null;
LogFormater.Info("Recovery underway - waiting 4 secs then restarting bot");
Thread.Sleep(4000);
Restart();
}
public bool IsLoaded()
{
@ -99,56 +90,10 @@ private BotRejoinConfig rejoinCfg;
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)
{
LogFormater.Info("Client service ~ Logged out");
if (!getLogoutExpected())
{
HandleUnexpectedDisconnect("Unexpected logout");
}
master.TriggerBotClientEvent(true, !getLogoutExpected());
master.TriggerBotClientEvent(true,!getLogoutExpected());
}
protected void BotSimConnected(object o, SimConnectedEventArgs e)
@ -158,59 +103,28 @@ private BotRejoinConfig rejoinCfg;
AutoRestartLoginTimer.Stop();
AutoRestartLoginTimer = null;
}
isRegionRestartScheduled = false;
lastConnectTime = DateTime.UtcNow;
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)
{
goodticks = 0;
LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name);
if (!getLogoutExpected())
{
HandleUnexpectedDisconnect("Unexpected sim disconnect");
}
}
protected void BotDisconnected(object o, DisconnectedEventArgs e)
{
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)
{
if (e.Status == LoginStatus.Failed || !string.IsNullOrEmpty(e.FailReason))
{
if(e.FailReason != "")
{
string reason = string.IsNullOrEmpty(e.FailReason) ? e.Message : e.FailReason;
LogFormater.Info("Client service ~ {FAILED} Login status: " + reason);
LogFormater.Info("Client service ~ {FAILED} Login status: " + e.FailReason.ToString());
client.Network.Logout();
ResetClient();
HandleUnexpectedDisconnect("Login failed: " + reason);
return;
}
LogFormater.Info("Client service ~ Login status: " + e.Status.ToString());
@ -295,18 +209,12 @@ private BotRejoinConfig rejoinCfg;
client.Network.Disconnected += BotDisconnected;
client.Network.SimDisconnected += BotSimDisconnected;
client.Network.LoginProgress += BotLoginStatus;
client.Self.AlertMessage += BotAlertMessage;
client.Self.AgentDataReply += (s, e) =>
{
master.DataStoreService.SetActiveGroupTitle(e.GroupTitle);
};
}
public void Login()
{
ResetClient();
goodticks = 0;
LogoutExpected = false;
master.TriggerBotClientEvent(false, false);
LoginParams loginParams = new(
client,
@ -320,25 +228,6 @@ private BotRejoinConfig rejoinCfg;
{
loginParams.URI = basicCfg.GetLoginURI();
}
string customMac = basicCfg.GetMAC();
if (!string.IsNullOrEmpty(customMac))
{
loginParams.MAC = customMac;
}
string customId0 = basicCfg.GetId0();
if (!string.IsNullOrEmpty(customId0))
{
loginParams.ID0 = customId0;
}
string customStart = basicCfg.GetStartLocation();
if (!string.IsNullOrEmpty(customStart))
{
loginParams.Start = customStart;
}
AutoRestartLoginTimer.Start();
client.Network.BeginLogin(loginParams);
}

View file

@ -1,4 +1,4 @@
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq;
using OpenMetaverse;
using OpenMetaverse.ImportExport.Collada14;
using SecondBotEvents.Config;

View file

@ -522,15 +522,20 @@ namespace SecondBotEvents.Services
AddLink(item);
}
// Queued swap: Detach everything, wait for SL to settle, then attach new.
// Re-implementing ReplaceOutfit logic manually to ensure a clean swap without toggling.
var wearables = outfit.OfType<InventoryWearable>().ToList();
var attachments = outfit.Where(item => item is InventoryAttachment || item is InventoryObject).ToList();
// 1. Replace wearables (this is usually safe and doesn't toggle)
GetClient().Appearance.ReplaceOutfit(wearables.Cast<InventoryItem>().ToList());
// 2. Replace attachments with FirstDetachAll=true and replace=true to prevent toggling
GetClient().Appearance.AddAttachments(attachments, true, true);
ThreadPool.QueueUserWorkItem(sync =>
{
var current = ContentLinks().Select(RealInventoryItem).ToList();
RemoveFromOutfit(current);
Thread.Sleep(2000); // Wait for SL servers to process detaches
AddToOutfit(outfit, false);
Thread.Sleep(2000);
GetClient().Appearance.RequestSetAppearance(true);
});
}

View file

@ -42,7 +42,6 @@ namespace SecondBotEvents.Services
public DataStoreService(EventsSecondBot setMaster) : base(setMaster)
{
myConfig = new DataStoreConfig(master.fromEnv,master.fromFolder);
LoadPendingGroupInvites();
}
protected Dictionary<string, UUID> avatarsName2Key = [];
@ -457,11 +456,12 @@ namespace SecondBotEvents.Services
}
}
public Dictionary<UUID, string> GetGroupRoles(UUID group, bool force = false)
public Dictionary<UUID, string> GetGroupRoles(UUID group)
{
Dictionary<UUID, string> reply = [];
if (force || groupRoles.ContainsKey(group) == false)
if (groupRoles.ContainsKey(group) == false)
{
AutoResetEvent fetchEvent = new(false);
void callback(object sender, GroupRolesDataReplyEventArgs e)
{
@ -541,14 +541,13 @@ namespace SecondBotEvents.Services
public void BotRecordReplyIM(UUID sessionid,string message)
{
RecordChat(sessionid, "{BOT} " + GetClient().Self.Name, message);
RecordChat(sessionid, "Bot", message);
}
public void BotRecordLocalchatReply(string message, string typeTag = "")
{
lock (localChatHistory)
{
long ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
localChatHistory.Add("[" + ms + "] {BOT} " + typeTag + GetClient().Self.Name + ": " + message);
localChatHistory.Add("{BOT} " + typeTag + GetClient().Self.Name + ": " + message);
if (localChatHistory.Count <= myConfig.GetLocalChatHistoryLimit())
{
return;
@ -565,8 +564,7 @@ namespace SecondBotEvents.Services
{
return; // chat window not open unable to record message
}
long ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
chatWindows[sessionID].Add("[" + ms + "] " + fromname + ": " + message);
chatWindows[sessionID].Add(fromname + ": " + message);
chatWindowsUnread[sessionID] = true;
int purgeLen = myConfig.GetImChatHistoryLimit();
if (chatWindowsIsGroup[sessionID] == true)
@ -675,49 +673,6 @@ namespace SecondBotEvents.Services
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)
{
lock (PendingGroupInvites)
@ -727,50 +682,14 @@ namespace SecondBotEvents.Services
{
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
{
GroupID = groupID,
GroupName = groupName,
GroupName = "Unknown Group", // Will be updated on lookup
AgentID = agentID,
AgentName = agentName,
Message = message
});
SavePendingGroupInvites();
}
}
@ -787,7 +706,6 @@ namespace SecondBotEvents.Services
lock (PendingGroupInvites)
{
PendingGroupInvites.RemoveAll(i => i.GroupID == groupID);
SavePendingGroupInvites();
}
}
@ -1236,35 +1154,12 @@ namespace SecondBotEvents.Services
}
protected string activeGroupTitle = "";
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);
}
}
Dictionary<UUID, Avatar.AvatarProperties> avprops = [];
Dictionary<UUID, Dictionary<UUID, string>> avpicks = [];
public KeyValuePair<bool, Dictionary<UUID, string>> GetAvatarPicks(UUID agentID)
{
if (!avpicks.ContainsKey(agentID))
if (avpicks.ContainsKey(agentID) == false)
{
GetClient().Avatars.RequestAvatarPicks(agentID);
return new KeyValuePair<bool, Dictionary<UUID, string>>(false, []);
@ -1272,259 +1167,21 @@ namespace SecondBotEvents.Services
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)
{
lock (avpicks)
{
if (!avpicks.ContainsKey(e.AvatarID))
if (avpicks.ContainsKey(e.AvatarID) == false)
{
avpicks.Add(e.AvatarID, []);
}
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);
}
}
}
}
protected void PickInfoReply(object sender, PickInfoReplyEventArgs e)
public KeyValuePair<bool,Avatar.AvatarProperties> GetAvatarAvatarProperties(UUID agentID)
{
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))
if (avprops.ContainsKey(agentID) == false)
{
GetClient().Avatars.RequestAvatarProperties(agentID);
return new KeyValuePair<bool, Avatar.AvatarProperties>(false, new Avatar.AvatarProperties());
@ -1532,51 +1189,15 @@ namespace SecondBotEvents.Services
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)
{
if (!avprops.ContainsKey(e.AvatarID))
if(avprops.ContainsKey(e.AvatarID) == false)
{
avprops.Add(e.AvatarID, new Avatar.AvatarProperties());
avprops.Add(e.AvatarID,new Avatar.AvatarProperties());
}
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()
{
GetClient().Groups.CurrentGroups += GroupCurrent;
@ -1592,11 +1213,7 @@ namespace SecondBotEvents.Services
GetClient().Estate.EstateBansReply += EstateBans;
GetClient().Friends.FriendFoundReply += FindFriendReply;
GetClient().Avatars.AvatarPropertiesReply += AvatarDetailsReply;
GetClient().Avatars.AvatarGroupsReply += AvatarGroupsReplyCallback;
GetClient().Avatars.AvatarPicksReply += AvatarPicksReply;
GetClient().Avatars.AvatarClassifiedReply += AvatarClassifiedsReply;
GetClient().Avatars.PickInfoReply += PickInfoReply;
GetClient().Avatars.ClassifiedInfoReply += ClassifiedInfoReply;
GetClient().Groups.RequestCurrentGroups();
_ = GetClient().Self.RetrieveInstantMessages();
if(myConfig.GetPrefetchInventory() == true)
@ -1734,8 +1351,7 @@ namespace SecondBotEvents.Services
AddAvatar(e.SourceID, e.FromName);
}
long ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
localChatHistory.Add("[" + ms + "] " + source + " " + typeTag + e.FromName + ": " + e.Message);
localChatHistory.Add(source + " " + typeTag + e.FromName + ": " + e.Message);
if (localChatHistory.Count <= myConfig.GetLocalChatHistoryLimit())
{
break;
@ -1893,14 +1509,7 @@ namespace SecondBotEvents.Services
{
// group IM
OpenChatWindow(true, e.IM.IMSessionID, e.IM.IMSessionID);
if (e.IM.FromAgentID == GetClient().Self.AgentID)
{
RecordChat(e.IM.IMSessionID, "{BOT} " + GetClient().Self.Name, e.IM.Message);
}
else
{
RecordChat(e.IM.IMSessionID, e.IM.FromAgentName, e.IM.Message);
}
RecordChat(e.IM.IMSessionID, e.IM.FromAgentName, e.IM.Message);
break;
}
UUID SessionID = e.IM.IMSessionID;
@ -1915,10 +1524,6 @@ namespace SecondBotEvents.Services
{
RecordChat(SessionID, e.IM.FromAgentName, e.IM.Message);
}
else
{
RecordChat(SessionID, "{BOT} " + GetClient().Self.Name, e.IM.Message);
}
break;
}
case InstantMessageDialog.FriendshipOffered:
@ -1972,11 +1577,6 @@ namespace SecondBotEvents.Services
GetClient().Estate.EstateBansReply -= EstateBans;
GetClient().Friends.FriendFoundReply -= FindFriendReply;
GetClient().Avatars.AvatarPropertiesReply -= AvatarDetailsReply;
GetClient().Avatars.AvatarGroupsReply -= AvatarGroupsReplyCallback;
GetClient().Avatars.AvatarPicksReply -= AvatarPicksReply;
GetClient().Avatars.AvatarClassifiedReply -= AvatarClassifiedsReply;
GetClient().Avatars.PickInfoReply -= PickInfoReply;
GetClient().Avatars.ClassifiedInfoReply -= ClassifiedInfoReply;
}
}

View file

@ -1,4 +1,4 @@
using OpenMetaverse;
using OpenMetaverse;
using SecondBotEvents.Config;
using System;
using System.Collections.Generic;
@ -108,28 +108,6 @@ namespace SecondBotEvents.Services
}
}
public List<DialogWindow> GetActiveDialogs()
{
lock (DialogWindows)
{
CleanUp();
List<DialogWindow> reply = [];
foreach (var kvp in DialogWindows)
{
var e = kvp.Value;
reply.Add(new DialogWindow
{
buttons = [.. e.ButtonLabels],
dialogid = kvp.Key,
message = e.Message,
objectname = e.ObjectName,
expires = DialogWindowsExpire[kvp.Key]
});
}
return reply;
}
}
public string DialogAction(int dialogID, string button)
{
lock(DialogWindows)
@ -163,7 +141,7 @@ namespace SecondBotEvents.Services
List<int> removeids = [];
foreach(KeyValuePair<int,long> a in DialogWindowsExpire)
{
if(a.Value < unixtime)
if(a.Value > unixtime)
{
removeids.Add(a.Key);
}
@ -262,8 +240,6 @@ namespace SecondBotEvents.Services
if (GetClient() != null)
{
GetClient().Self.ScriptDialog -= DialogWindowEvent;
GetClient().Self.LoadURL -= LoadUrlEvent;
GetClient().Self.AlertMessage -= BotAlertMessage;
}
}
@ -273,37 +249,6 @@ namespace SecondBotEvents.Services
botConnected = true;
// attach events
GetClient().Self.ScriptDialog += DialogWindowEvent;
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)
{
// We'll relay this as a special chat message so it shows up in the dashboard chat log
// In this version of LibOMV, the property is 'URL' (all caps).
string address = e.URL;
string message = " [SYSTEM] Object '" + e.ObjectName + "' sent a link: " + e.Message + " URL: " + address;
// Record it in the bot's local chat history so the dashboard picks it up on the next poll
master.DataStoreService.BotRecordLocalchatReply(message, "[SYSTEM] ");
LogFormater.Info("[LoadURL] " + e.ObjectName + " sent: " + e.Message + " (" + address + ")");
}
public override void Start(bool updateEnabled = false, bool setEnabledTo = false)

View file

@ -1,4 +1,4 @@
using Discord;
using Discord;
using Discord.Rest;
using Discord.WebSocket;
using OpenMetaverse;
@ -1300,7 +1300,7 @@ namespace SecondBotEvents.Services
SocketRole role = server.GetRole(roleid);
Dictionary<string, string> settings = new()
{
{ "Config|Color", $"{role.Color.R},{role.Color.G},{role.Color.B}" },
{ "Config|Color", String.Join(",", [role.Color.R, role.Color.G, role.Color.B]) },
{ "Config|Name", role.Name },
{ "Info|AssignedCount", role.Members.ToString() },
{ "Perm|AttachFiles", role.Permissions.AttachFiles.ToString() },

View file

@ -127,11 +127,9 @@ namespace SecondBotEvents.Services
private void CreateWebServer()
{
// Swan.Logging.Logger.UnregisterLogger<ConsoleLogger>();
string hostName = System.Net.Dns.GetHostName();
int port = myConfig.GetPort();
Swan.Logging.Logger.UnregisterLogger<ConsoleLogger>();
HTTPendpoint = new WebServer(o => o
.WithUrlPrefix($"http://{hostName}:{port}/")
.WithUrlPrefix("http://*:80")
.WithMode(HttpListenerMode.EmbedIO))
.WithCors()
.WithWebApi("/api", SerializationCallback, m => m.WithController(() => new HttpWebBot(master)))
@ -139,7 +137,7 @@ namespace SecondBotEvents.Services
.WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new { Message = "Error" }))
);
HTTPendpoint.RunAsync();
LogFormater.Info($"HTTP service [Enabled] on {hostName}:{port}");
LogFormater.Info("HTTP service [Enabled] on port 80");
}
protected string JsonInput(IHttpContext httpContext)
@ -401,9 +399,9 @@ namespace SecondBotEvents.Services
public string Run([FormField] string commandName, [FormField] string args, [FormField] string signing, [FormField] string unixtime)
{
string[] myArgs = [];
if(!string.IsNullOrEmpty(args))
if(args != null)
{
myArgs = args.Split("~#~", StringSplitOptions.None);
myArgs = args.Split("~#~", StringSplitOptions.RemoveEmptyEntries);
}
if(int.TryParse(unixtime, out int cmdUnixtime) == false)
{

View file

@ -1,4 +1,4 @@
using SecondBotEvents.Config;
using SecondBotEvents.Config;
using RabbitMQ.Client;
using System;
using System.Text;
@ -209,7 +209,6 @@ namespace SecondBotEvents.Services
connection.CloseAsync().Await();
connection = null;
}
channels.Clear();
var endpoints = new System.Collections.Generic.List<AmqpTcpEndpoint> {
new AmqpTcpEndpoint(myConfig.GetHostIP(),myConfig.GetHostPort()),
};

View file

@ -1,4 +1,4 @@
using OpenMetaverse;
using OpenMetaverse;
using OpenMetaverse.ImportExport.Collada14;
using SecondBotEvents.Services;
using System;
@ -131,42 +131,15 @@ namespace BetterSecondBot.Static
{
if (contents.Count > 0)
{
HashSet<UUID> seenIds = [];
foreach (InventoryBase item in contents)
{
if (item is InventoryItem itm)
mapitem = new InventoryMapItem
{
UUID effectiveID = itm.UUID;
// If it's a link, the AssetUUID is the UUID of the item it points to
if (itm.AssetType == AssetType.Link)
{
effectiveID = itm.AssetUUID;
}
if (seenIds.Contains(effectiveID))
{
continue;
}
seenIds.Add(effectiveID);
mapitem = new InventoryMapItem
{
name = item.Name,
id = item.UUID.ToString(),
typename = item.GetType().Name
};
entrys.Add(mapitem);
}
else if (item is InventoryFolder fold)
{
mapitem = new InventoryMapItem
{
name = fold.Name,
id = fold.UUID.ToString(),
typename = "Folder"
};
entrys.Add(mapitem);
}
name = item.Name,
id = item.UUID.ToString(),
typename = item.GetType().Name
};
entrys.Add(mapitem);
}
}
}