SecondBot/SecondBotEvents/Commands/Info.cs
2026-11-04 17:03:11 -07:00

2148 lines
111 KiB
C#

using System.Text.Json;
using OpenMetaverse;
using OpenMetaverse.Assets;
using OpenMetaverse.Imaging;
using OpenMetaverse.Rendering;
using OpenMetaverse.StructuredData;
using CoreJ2K;
using SecondBotEvents.Services;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
namespace SecondBotEvents.Commands
{
[ClassInfo("What is going on with the bot")]
public class Info(EventsSecondBot setmaster) : CommandsAPI(setmaster)
{
[About("Requests a 6-digit login token to be sent via IM to the avatar")]
[ArgHints("avatar", "The avatar to send the token to", "AVATAR")]
[CmdTypeDo()]
public object RequestLoginToken(string avatar)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero) return Failure("Unknown avatar - are you sure the name is correct? (Firstname Lastname)");
string token = master.DataStoreService.RequestLoginToken(avataruuid);
GetClient().Self.InstantMessage(avataruuid, $"[Bot Dashboard] Your login verification code is: {token} (Expires in 5 minutes)");
return BasicReply("Sent");
}
[About("Verifies a 6-digit login token")]
[ArgHints("avatar", "The avatar verifying", "AVATAR")]
[ArgHints("token", "The 6-digit token", "STRING")]
[CmdTypeGet()]
public object VerifyLoginToken(string avatar, string token)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero) return Failure("Unknown avatar");
bool ok = master.DataStoreService.VerifyLoginToken(avataruuid, token);
if (ok) return BasicReply(avataruuid.ToString());
return BasicReply("False");
}
[About("Fetchs the current region type the bot is in")]
[ReturnHints("Region type as string")]
[ReturnHintsFailure("Error not in a sim")]
[CmdTypeGet()]
public object GetCurrentRegionType()
{
if(GetClient().Network.CurrentSim == null)
{
return Failure("Error not in a sim");
}
return BasicReply(GetClient().Network.CurrentSim.ProductName);
}
[About("Lists objects that are sculpty type in the current sim that the bot can see")]
[ReturnHints("A json object")]
[CmdTypeGet()]
public object ListSculptys()
{
Dictionary<uint, Primitive> objects_copy = GetClient().Network.CurrentSim.ObjectsPrimitives.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Dictionary<uint, uint> mapLocalID = [];
Dictionary<uint, Primitive> sculpts = [];
foreach (KeyValuePair<uint, Primitive> Obj in objects_copy)
{
mapLocalID.Add(Obj.Value.LocalID, Obj.Key);
if (PrimType.Sculpt.Equals(Obj.Value.Type) == true)
{
sculpts.Add(Obj.Key, Obj.Value);
}
}
List<SculptysInfo> reply = [];
foreach (KeyValuePair<uint, Primitive> Obj in sculpts)
{
Vector3 pos = Obj.Value.Position;
string name = "?";
if (Obj.Value.NameValues != null)
{
name = Obj.Value.NameValues[0].Value.ToString();
}
UUID key = Obj.Value.ID;
UUID owner = Obj.Value.OwnerID;
bool asLink = false;
if (Obj.Value.ParentID > 0)
{
if(mapLocalID.ContainsKey(Obj.Value.ParentID) == true)
{
Primitive wip = objects_copy[mapLocalID[Obj.Value.ParentID]];
pos = wip.Position;
asLink = true;
owner = wip.OwnerID;
if (wip.NameValues != null)
{
name = wip.NameValues[0].Value.ToString();
}
key = wip.ID;
}
}
SculptysInfo A = new()
{
isLinked = asLink.ToString(),
name = name.ToString(),
owner = owner.ToString(),
uuid = key.ToString(),
pos = pos.ToString()
};
reply.Add(A);
}
if (reply.Count == 0)
{
return Failure("No objects found");
}
return BasicReply(JsonSerializer.Serialize(reply, JsonOptions.UnsafeRelaxed));
}
[About("Fetchs the current bot")]
[ReturnHints("The build ID of the bot")]
[CmdTypeGet()]
public object Version()
{
return BasicReply(master.GetVersion());
}
[About("Fetchs the name of the bot")]
[ReturnHints("Firstname Lastname")]
[CmdTypeGet()]
public object Name()
{
return BasicReply(GetClient().Self.FirstName + " " + GetClient().Self.LastName);
}
[About("Fetchs the current parcels name")]
[ReturnHints("Parcelname")]
[ReturnHintsFailure("Error parcel not found")]
[ReturnHintsFailure("Error not in a sim")]
[CmdTypeGet()]
public object ParcelName()
{
if (GetClient().Network.CurrentSim == null)
{
return Failure("Error not in a sim");
}
int localid = GetClient().Parcels.GetParcelLocalID(GetClient().Network.CurrentSim, GetClient().Self.SimPosition);
if (GetClient().Network.CurrentSim.Parcels.ContainsKey(localid) == false)
{
return Failure("Error parcel not found");
}
return BasicReply(GetClient().Network.CurrentSim.Parcels[localid].Name);
}
[About("Requests the current unixtime at the bot")]
[ReturnHints("Unixtime")]
[CmdTypeGet()]
public object UnixTimeNow()
{
return BasicReply(SecondbotHelpers.UnixTimeNow().ToString());
}
[About("Fetchs the current region name")]
[ReturnHints("Regionname")]
[ReturnHintsFailure("Error not in a sim")]
[CmdTypeGet()]
public object SimName()
{
if (GetClient().Network.CurrentSim == null)
{
return Failure("Error not in a sim");
}
return BasicReply(GetClient().Network.CurrentSim.Name);
}
[About("Fetchs the current location of the bot")]
[ReturnHints("array of X,Y,Z values")]
[ReturnHintsFailure("Error not in a sim")]
[CmdTypeGet()]
public object GetPosition()
{
if (GetClient().Network.CurrentSim == null)
{
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()
{
{ "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 }
};
return BasicReply(JsonSerializer.Serialize(pos, JsonOptions.UnsafeRelaxed));
}
[About("Fetchs the current RLV restrictions applied to the bot")]
[ReturnHints("A JSON array of RLV rules")]
[CmdTypeGet()]
public object RLVStatus()
{
if (master.RLV == null)
{
return Failure("RLV service is not running");
}
var rules = master.RLV.Rules;
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;
int primCount = GetClient().Network.CurrentSim.ObjectsPrimitives.Values.Count(p => p.ID == prim.ID || p.ParentID == prim.LocalID);
prim.Rotation.GetEulerAngles(out float roll, out float pitch, out float yaw);
float rotX = (float)Math.Round(roll * (180.0f / (float)Math.PI), 2);
float rotY = (float)Math.Round(pitch * (180.0f / (float)Math.PI), 2);
float rotZ = (float)Math.Round(yaw * (180.0f / (float)Math.PI), 2);
UUID primOwner = prim.Properties?.OwnerID ?? UUID.Zero;
bool canBotModify = false;
if (primOwner != UUID.Zero)
{
if (primOwner == GetClient().Self.AgentID)
{
canBotModify = true;
}
else if (GetClient().Friends.FriendList.TryGetValue(primOwner, out FriendInfo friend) && friend.CanModifyTheirObjects)
{
canBotModify = true;
}
}
reply.Add(new {
name = name,
description = prim.Properties?.Description ?? "",
uuid = prim.ID.ToString(),
local_id = prim.LocalID,
dist = Math.Round(dist, 1),
pos = prim.Position.ToString(),
pos_x = Math.Round(prim.Position.X, 3),
pos_y = Math.Round(prim.Position.Y, 3),
pos_z = Math.Round(prim.Position.Z, 3),
scale_x = Math.Round(prim.Scale.X, 3),
scale_y = Math.Round(prim.Scale.Y, 3),
scale_z = Math.Round(prim.Scale.Z, 3),
rot_x = rotX,
rot_y = rotY,
rot_z = rotZ,
flag_physical = (prim.Flags & PrimFlags.Physics) != 0,
flag_temporary = (prim.Flags & PrimFlags.Temporary) != 0,
flag_phantom = (prim.Flags & PrimFlags.Phantom) != 0,
physics_shape = prim.PhysicsProps != null ? prim.PhysicsProps.PhysicsShapeType.ToString() : "Prim",
has_light = prim.Light != null,
light_color = prim.Light != null ? prim.Light.Color.ToString() : "",
light_intensity = prim.Light != null ? Math.Round(prim.Light.Intensity, 2) : 0,
light_radius = prim.Light != null ? Math.Round(prim.Light.Radius, 2) : 0,
light_falloff = prim.Light != null ? Math.Round(prim.Light.Falloff, 2) : 0,
has_flexi = prim.Flexible != null,
flexi_softness = prim.Flexible != null ? prim.Flexible.Softness : 0,
flexi_gravity = prim.Flexible != null ? Math.Round(prim.Flexible.Gravity, 2) : 0,
flexi_drag = prim.Flexible != null ? Math.Round(prim.Flexible.Drag, 2) : 0,
flexi_wind = prim.Flexible != null ? Math.Round(prim.Flexible.Wind, 2) : 0,
flexi_tension = prim.Flexible != null ? Math.Round(prim.Flexible.Tension, 2) : 0,
is_sitting = (prim.LocalID == sittingOn),
owner_id = prim.Properties?.OwnerID.ToString() ?? "00000000-0000-0000-0000-000000000000",
creator_id = prim.Properties?.CreatorID.ToString() ?? "00000000-0000-0000-0000-000000000000",
last_owner_id = prim.Properties?.LastOwnerID.ToString() ?? "00000000-0000-0000-0000-000000000000",
group_id = prim.Properties?.GroupID.ToString() ?? "00000000-0000-0000-0000-000000000000",
click_action = prim.ClickAction.ToString(),
prims = primCount > 0 ? primCount : 1,
can_bot_modify = canBotModify,
perm_anyone_move = (prim.Properties != null && (prim.Properties.Permissions.EveryoneMask & PermissionMask.Move) != 0),
perm_anyone_copy = (prim.Properties != null && (prim.Properties.Permissions.EveryoneMask & PermissionMask.Copy) != 0),
perm_next_modify = (prim.Properties != null && (prim.Properties.Permissions.NextOwnerMask & PermissionMask.Modify) != 0),
perm_next_copy = (prim.Properties != null && (prim.Properties.Permissions.NextOwnerMask & PermissionMask.Copy) != 0),
perm_next_transfer = (prim.Properties != null && (prim.Properties.Permissions.NextOwnerMask & PermissionMask.Transfer) != 0),
sale_price = prim.Properties?.SalePrice ?? 0,
sale_type = prim.Properties?.SaleType.ToString() ?? "Not",
tex_default_id = prim.Textures?.DefaultTexture?.TextureID.ToString() ?? "00000000-0000-0000-0000-000000000000",
tex_glow = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.Glow, 3) : 0,
tex_transparency = prim.Textures?.DefaultTexture != null ? Math.Round((1.0f - prim.Textures.DefaultTexture.RGBA.A) * 100.0f, 1) : 0,
tex_shiny = prim.Textures?.DefaultTexture?.Shiny.ToString() ?? "None",
tex_bump = prim.Textures?.DefaultTexture?.Bump.ToString() ?? "None",
tex_fullbright = prim.Textures?.DefaultTexture?.Fullbright ?? false,
tex_mapping = prim.Textures?.DefaultTexture?.TexMapType.ToString() ?? "Default",
tex_repeat_u = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.RepeatU, 4) : 1.0,
tex_repeat_v = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.RepeatV, 4) : 1.0,
tex_offset_u = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.OffsetU, 4) : 0.0,
tex_offset_v = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.OffsetV, 4) : 0.0,
tex_rotation = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.Rotation * (180.0f / (float)Math.PI), 2) : 0.0,
tex_color = prim.Textures?.DefaultTexture != null ? $"#{((byte)(prim.Textures.DefaultTexture.RGBA.R * 255)):X2}{((byte)(prim.Textures.DefaultTexture.RGBA.G * 255)):X2}{((byte)(prim.Textures.DefaultTexture.RGBA.B * 255)):X2}" : "#FFFFFF",
material_type = prim.PrimData.Material.ToString(),
prim_type = prim.Type.ToString(),
sculpt_type = prim.Sculpt != null ? prim.Sculpt.Type.ToString() : "None",
sculpt_uuid = prim.Sculpt != null ? prim.Sculpt.SculptTexture.ToString() : "00000000-0000-0000-0000-000000000000",
phys_density = prim.PhysicsProps != null ? Math.Round(prim.PhysicsProps.Density, 1) : 1000.0,
phys_friction = prim.PhysicsProps != null ? Math.Round(prim.PhysicsProps.Friction, 2) : 0.6,
phys_gravity = prim.PhysicsProps != null ? Math.Round(prim.PhysicsProps.GravityMultiplier, 2) : 1.0,
phys_restitution = prim.PhysicsProps != null ? Math.Round(prim.PhysicsProps.Restitution, 2) : 0.5,
light_cutoff = prim.Light != null ? Math.Round(prim.Light.Cutoff, 2) : 0.0
});
}
}
return BasicReply(JsonSerializer.Serialize(reply, JsonOptions.UnsafeRelaxed));
}
[About("Fetches contents (Task Inventory) inside a specified object")]
[ArgHints("target", "The UUID or LocalID of the object", "STRING")]
[CmdTypeGet()]
public object GetObjectContents(string target)
{
if (GetClient().Network.CurrentSim == null) return Failure("Not in a sim");
string cleanTarget = (target ?? "").Trim('"', ' ', '\t', '\r', '\n');
uint localID = 0;
UUID objectID = UUID.Zero;
if (UUID.TryParse(cleanTarget, out objectID))
{
var prim = GetClient().Network.CurrentSim.ObjectsPrimitives.Values.FirstOrDefault(p => p.ID == objectID);
if (prim != null)
{
localID = prim.LocalID;
}
}
else if (uint.TryParse(cleanTarget, out localID))
{
if (GetClient().Network.CurrentSim.ObjectsPrimitives.TryGetValue(localID, out Primitive prim))
{
objectID = prim.ID;
}
}
// Fallback search across all simulator prims
if (objectID != UUID.Zero && localID == 0)
{
foreach (var p in GetClient().Network.CurrentSim.ObjectsPrimitives.Values)
{
if (p.ID == objectID)
{
localID = p.LocalID;
break;
}
}
}
else if (localID != 0 && objectID == UUID.Zero)
{
if (GetClient().Network.CurrentSim.ObjectsPrimitives.TryGetValue(localID, out Primitive prim))
{
objectID = prim.ID;
}
}
if (objectID == UUID.Zero || localID == 0)
{
return Failure($"Object '{cleanTarget}' not found in current sim (UUID={objectID}, LocalID={localID})");
}
var items = GetClient().Inventory.GetTaskInventory(objectID, localID, TimeSpan.FromSeconds(6));
if (items == null) return BasicReply("[]");
var replyList = items.Select(item => {
string permsStr = "";
string itemType = item.GetType().Name.Replace("Inventory", "");
if (item is InventoryItem invItem)
{
var p = invItem.Permissions.NextOwnerMask;
var noMod = (p & PermissionMask.Modify) == PermissionMask.None;
var noCopy = (p & PermissionMask.Copy) == PermissionMask.None;
var noXfer = (p & PermissionMask.Transfer) == PermissionMask.None;
List<string> restr = new List<string>();
if (noMod) restr.Add("no modify");
if (noCopy) restr.Add("no copy");
if (noXfer) restr.Add("no transfer");
if (restr.Count > 0) permsStr = " (" + string.Join(") (", restr) + ")";
if (invItem.InventoryType != InventoryType.Unknown)
{
itemType = invItem.InventoryType.ToString();
}
else if (invItem.AssetType != AssetType.Unknown)
{
itemType = invItem.AssetType.ToString();
}
}
string itemName = item.Name ?? "";
try { itemName = Uri.UnescapeDataString(itemName); } catch {}
return new {
name = itemName,
uuid = item.UUID.ToString(),
type = itemType,
perms = permsStr
};
}).ToList();
return BasicReply(JsonSerializer.Serialize(replyList, JsonOptions.UnsafeRelaxed));
}
[About("Fetches all prims in the linkset for a specified object")]
[ArgHints("target", "The UUID or LocalID of the object", "STRING")]
[CmdTypeGet()]
public object GetLinksetPrims(string target)
{
if (GetClient().Network.CurrentSim == null) return Failure("Not in a sim");
string cleanTarget = (target ?? "").Trim('"', ' ', '\t', '\r', '\n');
uint localID = 0;
UUID objectID = UUID.Zero;
if (UUID.TryParse(cleanTarget, out objectID))
{
var prim = GetClient().Network.CurrentSim.ObjectsPrimitives.Values.FirstOrDefault(p => p.ID == objectID);
if (prim != null) localID = prim.LocalID;
}
else if (uint.TryParse(cleanTarget, out localID))
{
if (GetClient().Network.CurrentSim.ObjectsPrimitives.TryGetValue(localID, out Primitive prim))
{
objectID = prim.ID;
}
}
if (objectID == UUID.Zero && localID == 0) return Failure($"Object '{cleanTarget}' not found");
Primitive targetPrim = GetClient().Network.CurrentSim.ObjectsPrimitives.Values.FirstOrDefault(p => p.ID == objectID || p.LocalID == localID);
if (targetPrim == null) return Failure("Target prim not found");
Primitive rootPrim = targetPrim;
if (targetPrim.ParentID != 0)
{
var parent = GetClient().Network.CurrentSim.ObjectsPrimitives.Values.FirstOrDefault(p => p.LocalID == targetPrim.ParentID);
if (parent != null) rootPrim = parent;
}
var linksetPrims = GetClient().Network.CurrentSim.ObjectsPrimitives.Values
.Where(p => p.ID == rootPrim.ID || p.ParentID == rootPrim.LocalID)
.OrderBy(p => p.ID == rootPrim.ID ? 0 : 1)
.ThenBy(p => p.LocalID)
.ToList();
var missingProps = linksetPrims.Where(p => p.Properties == null).Select(p => p.LocalID).ToArray();
if (missingProps.Length > 0)
{
GetClient().Objects.SelectObjects(GetClient().Network.CurrentSim, missingProps);
Thread.Sleep(400);
}
uint sittingOn = GetClient().Self.SittingOn;
int linkNum = 1;
List<object> replyList = [];
foreach (var prim in linksetPrims)
{
string name = prim.Properties?.Name;
if (string.IsNullOrEmpty(name)) name = (prim.ID == rootPrim.ID ? "Root Prim #" : "Child Prim #") + prim.LocalID;
prim.Rotation.GetEulerAngles(out float roll, out float pitch, out float yaw);
float rotX = (float)Math.Round(roll * (180.0f / (float)Math.PI), 2);
float rotY = (float)Math.Round(pitch * (180.0f / (float)Math.PI), 2);
float rotZ = (float)Math.Round(yaw * (180.0f / (float)Math.PI), 2);
UUID primOwner = prim.Properties?.OwnerID ?? rootPrim.Properties?.OwnerID ?? UUID.Zero;
bool canBotModify = false;
if (primOwner != UUID.Zero)
{
if (primOwner == GetClient().Self.AgentID) canBotModify = true;
else if (GetClient().Friends.FriendList.TryGetValue(primOwner, out FriendInfo friend) && friend.CanModifyTheirObjects) canBotModify = true;
}
replyList.Add(new {
link_num = linkNum++,
is_root = (prim.ID == rootPrim.ID),
name = name,
description = prim.Properties?.Description ?? "",
uuid = prim.ID.ToString(),
local_id = prim.LocalID,
pos = prim.Position.ToString(),
pos_x = Math.Round(prim.Position.X, 3),
pos_y = Math.Round(prim.Position.Y, 3),
pos_z = Math.Round(prim.Position.Z, 3),
scale_x = Math.Round(prim.Scale.X, 3),
scale_y = Math.Round(prim.Scale.Y, 3),
scale_z = Math.Round(prim.Scale.Z, 3),
rot_x = rotX,
rot_y = rotY,
rot_z = rotZ,
flag_physical = (prim.Flags & PrimFlags.Physics) != 0,
flag_temporary = (prim.Flags & PrimFlags.Temporary) != 0,
flag_phantom = (prim.Flags & PrimFlags.Phantom) != 0,
physics_shape = prim.PhysicsProps != null ? prim.PhysicsProps.PhysicsShapeType.ToString() : "Prim",
has_light = prim.Light != null,
light_color = prim.Light != null ? prim.Light.Color.ToString() : "",
light_intensity = prim.Light != null ? Math.Round(prim.Light.Intensity, 2) : 0,
light_radius = prim.Light != null ? Math.Round(prim.Light.Radius, 2) : 0,
light_falloff = prim.Light != null ? Math.Round(prim.Light.Falloff, 2) : 0,
has_flexi = prim.Flexible != null,
flexi_softness = prim.Flexible != null ? prim.Flexible.Softness : 0,
flexi_gravity = prim.Flexible != null ? Math.Round(prim.Flexible.Gravity, 2) : 0,
flexi_drag = prim.Flexible != null ? Math.Round(prim.Flexible.Drag, 2) : 0,
flexi_wind = prim.Flexible != null ? Math.Round(prim.Flexible.Wind, 2) : 0,
flexi_tension = prim.Flexible != null ? Math.Round(prim.Flexible.Tension, 2) : 0,
is_sitting = (prim.LocalID == sittingOn),
owner_id = primOwner.ToString(),
creator_id = prim.Properties?.CreatorID.ToString() ?? rootPrim.Properties?.CreatorID.ToString() ?? "00000000-0000-0000-0000-000000000000",
click_action = prim.ClickAction.ToString(),
can_bot_modify = canBotModify,
tex_default_id = prim.Textures?.DefaultTexture?.TextureID.ToString() ?? "00000000-0000-0000-0000-000000000000",
tex_glow = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.Glow, 3) : 0,
tex_transparency = prim.Textures?.DefaultTexture != null ? Math.Round((1.0f - prim.Textures.DefaultTexture.RGBA.A) * 100.0f, 1) : 0,
tex_shiny = prim.Textures?.DefaultTexture?.Shiny.ToString() ?? "None",
tex_bump = prim.Textures?.DefaultTexture?.Bump.ToString() ?? "None",
tex_fullbright = prim.Textures?.DefaultTexture?.Fullbright ?? false,
tex_mapping = prim.Textures?.DefaultTexture?.TexMapType.ToString() ?? "Default",
tex_repeat_u = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.RepeatU, 4) : 1.0,
tex_repeat_v = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.RepeatV, 4) : 1.0,
tex_offset_u = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.OffsetU, 4) : 0.0,
tex_offset_v = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.OffsetV, 4) : 0.0,
tex_rotation = prim.Textures?.DefaultTexture != null ? Math.Round(prim.Textures.DefaultTexture.Rotation * (180.0f / (float)Math.PI), 2) : 0.0,
tex_color = prim.Textures?.DefaultTexture != null ? $"#{((byte)(prim.Textures.DefaultTexture.RGBA.R * 255)):X2}{((byte)(prim.Textures.DefaultTexture.RGBA.G * 255)):X2}{((byte)(prim.Textures.DefaultTexture.RGBA.B * 255)):X2}" : "#FFFFFF",
material_type = prim.PrimData.Material.ToString(),
prim_type = prim.Type.ToString(),
sculpt_type = prim.Sculpt != null ? prim.Sculpt.Type.ToString() : "None",
sculpt_uuid = prim.Sculpt != null ? prim.Sculpt.SculptTexture.ToString() : "00000000-0000-0000-0000-000000000000",
phys_density = prim.PhysicsProps != null ? Math.Round(prim.PhysicsProps.Density, 1) : 1000.0,
phys_friction = prim.PhysicsProps != null ? Math.Round(prim.PhysicsProps.Friction, 2) : 0.6,
phys_gravity = prim.PhysicsProps != null ? Math.Round(prim.PhysicsProps.GravityMultiplier, 2) : 1.0,
phys_restitution = prim.PhysicsProps != null ? Math.Round(prim.PhysicsProps.Restitution, 2) : 0.5,
light_cutoff = prim.Light != null ? Math.Round(prim.Light.Cutoff, 2) : 0.0
});
}
return BasicReply(JsonSerializer.Serialize(replyList, JsonOptions.UnsafeRelaxed));
}
[About("Fetches the 256x256 terrain heightmap (sampled at 128x128) and water height of the current region")]
[ReturnHints("A JSON object with sim, water_height, min_height, max_height, patches_loaded, grid_size, and heights array")]
[CmdTypeGet()]
public object GetSimTerrain()
{
Simulator sim = GetClient().Network.CurrentSim;
if (sim == null) return Failure("Error not in a sim");
float waterHeight = sim.WaterHeight > 0 ? sim.WaterHeight : 20.0f;
int patchesLoaded = 0;
float sumHeight = 0f;
int validPoints = 0;
if (sim.Terrain != null)
{
for (int i = 0; i < sim.Terrain.Length; i++)
{
if (sim.Terrain[i]?.Data != null)
{
patchesLoaded++;
for (int j = 0; j < sim.Terrain[i].Data.Length; j++)
{
sumHeight += sim.Terrain[i].Data[j];
validPoints++;
}
}
}
}
float fallbackHeight = validPoints > 0 ? (sumHeight / validPoints) : waterHeight;
int gridSize = 128;
double[] heights = new double[gridSize * gridSize];
float minH = float.MaxValue;
float maxH = float.MinValue;
// Precompute nearest valid patch index for any missing patches so there are no holes
int[] nearestPatchIdx = new int[256];
for (int py = 0; py < 16; py++)
{
for (int px = 0; px < 16; px++)
{
int idx = py * 16 + px;
if (sim.Terrain != null && idx < sim.Terrain.Length && sim.Terrain[idx]?.Data != null)
{
nearestPatchIdx[idx] = idx;
}
else
{
int bestIdx = -1;
int bestDistSq = int.MaxValue;
if (sim.Terrain != null)
{
for (int ny = 0; ny < 16; ny++)
{
for (int nx = 0; nx < 16; nx++)
{
int nIdx = ny * 16 + nx;
if (nIdx < sim.Terrain.Length && sim.Terrain[nIdx]?.Data != null)
{
int dSq = (nx - px) * (nx - px) + (ny - py) * (ny - py);
if (dSq < bestDistSq)
{
bestDistSq = dSq;
bestIdx = nIdx;
}
}
}
}
}
nearestPatchIdx[idx] = bestIdx;
}
}
}
for (int gy = 0; gy < gridSize; gy++)
{
int sy = (int)Math.Round(gy * 255.0 / (gridSize - 1));
if (sy > 255) sy = 255;
for (int gx = 0; gx < gridSize; gx++)
{
int sx = (int)Math.Round(gx * 255.0 / (gridSize - 1));
if (sx > 255) sx = 255;
float h = fallbackHeight;
if (sim.TerrainHeightAtPoint(sx, sy, out float ptH))
{
h = ptH;
}
else if (sim.Terrain != null)
{
int px = sx / 16;
int py = sy / 16;
int nIdx = nearestPatchIdx[py * 16 + px];
if (nIdx >= 0 && sim.Terrain[nIdx]?.Data != null)
{
int npx = nIdx % 16;
int npy = nIdx / 16;
int cx = npx < px ? 15 : (npx > px ? 0 : (sx % 16));
int cy = npy < py ? 15 : (npy > py ? 0 : (sy % 16));
h = sim.Terrain[nIdx].Data[cy * 16 + cx];
}
}
if (h < minH) minH = h;
if (h > maxH) maxH = h;
heights[gy * gridSize + gx] = Math.Round(h, 2);
}
}
if (minH == float.MaxValue) { minH = waterHeight; maxH = waterHeight; }
int bx = Math.Clamp((int)Math.Round(GetClient().Self.SimPosition.X), 0, 255);
int by = Math.Clamp((int)Math.Round(GetClient().Self.SimPosition.Y), 0, 255);
float botGround = fallbackHeight;
if (sim.TerrainHeightAtPoint(bx, by, out float bgH)) botGround = bgH;
// Collect structural prims (walls, floors, roofs, platforms, buildings, trees) from CurrentSim AND neighboring sims
List<(float score, object data)> structCandidates = [];
List<object> neighborTerrains = [];
try
{
List<Simulator> simsToScan = [sim];
try
{
lock (GetClient().Network.Simulators)
{
foreach (var ns in GetClient().Network.Simulators)
{
if (ns != null && ns.Connected && ns != sim && !simsToScan.Contains(ns))
simsToScan.Add(ns);
}
}
}
catch { }
uint curOriginX = (uint)(sim.Handle >> 32);
uint curOriginY = (uint)(sim.Handle & 0xFFFFFFFF);
Vector3 selfPos = GetClient().Self.SimPosition;
foreach (Simulator s in simsToScan)
{
uint sOriginX = (uint)(s.Handle >> 32);
uint sOriginY = (uint)(s.Handle & 0xFFFFFFFF);
float offsetX = (long)sOriginX - (long)curOriginX;
float offsetY = (long)sOriginY - (long)curOriginY;
if (Math.Abs(offsetX) > 256f || Math.Abs(offsetY) > 256f)
continue;
// If this is a neighboring sim, extract its 32x32 terrain heightmap if patches are available
if (s != sim && s.Terrain != null)
{
int nPatches = 0;
for (int i = 0; i < 256; i++)
{
if (s.Terrain[i]?.Data != null) nPatches++;
}
if (nPatches > 0)
{
int nGrid = 32;
double[] nHeights = new double[nGrid * nGrid];
float nWater = s.WaterHeight > 0 ? s.WaterHeight : waterHeight;
for (int gy = 0; gy < nGrid; gy++)
{
int sy = Math.Clamp((int)Math.Round(gy * 255.0 / (nGrid - 1)), 0, 255);
for (int gx = 0; gx < nGrid; gx++)
{
int sx = Math.Clamp((int)Math.Round(gx * 255.0 / (nGrid - 1)), 0, 255);
float nh = nWater;
if (s.TerrainHeightAtPoint(sx, sy, out float th) && th > 0.01f)
nh = th;
nHeights[gy * nGrid + gx] = Math.Round(nh, 2);
}
}
neighborTerrains.Add(new
{
dx = (int)Math.Round(offsetX / 256f),
dy = (int)Math.Round(offsetY / 256f),
sim = s.Name,
patches = nPatches,
grid_size = nGrid,
heights = nHeights
});
}
}
Dictionary<uint, Primitive> primsCopy;
try
{
primsCopy = s.ObjectsPrimitives.ToDictionary(k => k.Key, v => v.Value);
}
catch { continue; }
foreach (var kvp in primsCopy)
{
Primitive p = kvp.Value;
if (p == null) continue;
if ((p.Flags & PrimFlags.Temporary) != 0) continue;
Vector3 worldPos = p.Position;
Quaternion worldRot = p.Rotation;
Primitive rootParent = null;
if (p.ParentID != 0)
{
if (!primsCopy.TryGetValue(p.ParentID, out rootParent) || rootParent == null)
continue; // Skip avatar attachments or orphaned child prims
worldPos = rootParent.Position + (p.Position * rootParent.Rotation);
worldRot = rootParent.Rotation * p.Rotation;
}
if (p.PrimData.PCode == PCode.Grass) continue;
if (worldPos.X < -10f || worldPos.X > 266f || worldPos.Y < -10f || worldPos.Y > 266f || worldPos.Z < 0f || worldPos.Z > 4096f)
continue;
float sx = p.Scale.X, sy = p.Scale.Y, sz = p.Scale.Z;
float maxDim = Math.Max(sx, Math.Max(sy, sz));
float minDim = Math.Min(sx, Math.Min(sy, sz));
float midDim = (sx + sy + sz) - maxDim - minDim;
// Structural size: >= 1.8m longest side, >= 0.5m second side, <= 128m to skip huge sim-surround domes
if (maxDim < 1.8f || midDim < 0.5f || maxDim > 128.0f)
continue;
float alpha = 1.0f;
string hexColor = "#c8d0dc";
byte rByte = 200, gByte = 208, bByte = 220;
if (p.Textures?.DefaultTexture != null)
{
var rgba = p.Textures.DefaultTexture.RGBA;
alpha = rgba.A;
if (alpha < 0.08f) continue; // Skip invisible collision/sensor prims
rByte = (byte)Math.Clamp((int)(rgba.R * 255), 45, 240);
gByte = (byte)Math.Clamp((int)(rgba.G * 255), 45, 240);
bByte = (byte)Math.Clamp((int)(rgba.B * 255), 45, 240);
hexColor = $"#{rByte:X2}{gByte:X2}{bByte:X2}";
}
string shapeType = "box";
string objName = (p.Properties?.Name ?? rootParent?.Properties?.Name ?? "").ToLowerInvariant();
bool isFoliageName = objName.Contains("tree") || objName.Contains("foliage") || objName.Contains("bush") ||
objName.Contains("shrub") || objName.Contains("leaves") || objName.Contains("canopy") ||
objName.Contains("pine") || objName.Contains("willow") || objName.Contains("maple") ||
objName.Contains("birch") || objName.Contains("oak") || objName.Contains("palm") ||
objName.Contains("plant") || objName.Contains("fern");
bool isFoliageTint = (gByte >= 80 && gByte >= bByte + 30 && rByte <= gByte + 50);
bool isBulkyCanopy = (minDim >= 3.5f && maxDim >= 6.0f && isFoliageTint) ||
(minDim >= 5.0f && maxDim >= 10.0f && ((p.Flags & PrimFlags.Phantom) != 0 || p.Type == PrimType.Sculpt));
if (p.PrimData.PCode == PCode.Tree || p.PrimData.PCode == PCode.NewTree || p.Flexible != null || isFoliageName || isBulkyCanopy)
{
shapeType = "tree";
}
else if (p.Type == PrimType.Cylinder)
{
shapeType = "cyl";
}
else if (p.Type == PrimType.Sphere)
{
shapeType = "sphere";
}
float wx = worldPos.X + offsetX;
float wy = worldPos.Y + offsetY;
float distToBot = Vector3.Distance(selfPos, new Vector3(wx, wy, worldPos.Z));
float score = (maxDim * midDim) / (1.0f + distToBot * 0.03f);
structCandidates.Add((score, new
{
t = shapeType,
x = Math.Round(wx, 2),
y = Math.Round(wy, 2),
z = Math.Round(worldPos.Z, 2),
sx = Math.Round(sx, 2),
sy = Math.Round(sy, 2),
sz = Math.Round(sz, 2),
qx = Math.Round(worldRot.X, 3),
qy = Math.Round(worldRot.Y, 3),
qz = Math.Round(worldRot.Z, 3),
qw = Math.Round(worldRot.W, 3),
c = hexColor,
a = Math.Round(alpha, 2)
}));
}
}
}
catch { }
var structures = structCandidates
.OrderByDescending(c => c.score)
.Take(950)
.Select(c => c.data)
.ToList();
var result = new
{
sim = sim.Name,
water_height = Math.Round(waterHeight, 2),
min_height = Math.Round(minH, 2),
max_height = Math.Round(maxH, 2),
bot_ground_height = Math.Round(botGround, 2),
patches_loaded = patchesLoaded,
grid_size = gridSize,
heights = heights,
neighbors = neighborTerrains,
structures = structures
};
return BasicReply(JsonSerializer.Serialize(result, JsonOptions.UnsafeRelaxed));
}
private static readonly ConcurrentDictionary<UUID, AssetMesh> _avatarMeshCache = new();
private static readonly ConcurrentDictionary<string, byte[]> _pngTextureCache = new();
private static Dictionary<string, Vector3> _cachedBoneWorldPos = null;
private static HashSet<string> _cachedTrueBones = null;
private static void EnsureSkeletonLoaded()
{
if (_cachedBoneWorldPos != null && _cachedTrueBones != null) return;
var bonePos = new Dictionary<string, Vector3>(StringComparer.OrdinalIgnoreCase);
var trueBones = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
try
{
string[] candidatePaths =
[
System.IO.Path.Combine(Settings.RESOURCE_DIR, "character", "avatar_skeleton.xml"),
"/app/linden/character/avatar_skeleton.xml",
System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "linden", "character", "avatar_skeleton.xml")
];
string skelPath = candidatePaths.FirstOrDefault(File.Exists);
if (skelPath != null)
{
XDocument doc = XDocument.Load(skelPath);
void WalkElement(XElement el, Vector3 parentWorldPos)
{
string name = el.Attribute("name")?.Value;
string posStr = el.Attribute("pos")?.Value ?? "0 0 0";
var parts = posStr.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
Vector3 localPos = Vector3.Zero;
if (parts.Length >= 3 &&
float.TryParse(parts[0], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float px) &&
float.TryParse(parts[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float py) &&
float.TryParse(parts[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float pz))
{
localPos = new Vector3(px, py, pz);
}
Vector3 curWorld = parentWorldPos + localPos;
if (!string.IsNullOrEmpty(name))
{
bonePos[name] = curWorld;
if (el.Name.LocalName == "bone")
trueBones.Add(name);
}
foreach (var child in el.Elements())
{
if (child.Name.LocalName == "bone" || child.Name.LocalName == "collision_volume")
WalkElement(child, curWorld);
}
}
var rootBone = doc.Root?.Element("bone");
if (rootBone != null) WalkElement(rootBone, Vector3.Zero);
}
}
catch { }
if (!bonePos.ContainsKey("mPelvis")) bonePos["mPelvis"] = new Vector3(0f, 0f, 1.067f);
if (!bonePos.ContainsKey("mTorso")) bonePos["mTorso"] = new Vector3(0f, 0f, 1.151f);
if (!bonePos.ContainsKey("mChest")) bonePos["mChest"] = new Vector3(-0.015f, 0f, 1.356f);
if (!bonePos.ContainsKey("mNeck")) bonePos["mNeck"] = new Vector3(-0.025f, 0f, 1.607f);
if (!bonePos.ContainsKey("mHead")) bonePos["mHead"] = new Vector3(-0.025f, 0f, 1.683f);
if (!bonePos.ContainsKey("mSkull")) bonePos["mSkull"] = new Vector3(-0.025f, 0f, 1.762f);
if (!bonePos.ContainsKey("mEyeLeft")) bonePos["mEyeLeft"] = new Vector3(0.073f, 0.036f, 1.762f);
if (!bonePos.ContainsKey("mEyeRight")) bonePos["mEyeRight"] = new Vector3(0.073f, -0.036f, 1.762f);
_cachedBoneWorldPos = bonePos;
_cachedTrueBones = trueBones;
}
private static string LookupInventoryItemName(GridClient client, UUID itemUuid)
{
if (itemUuid == UUID.Zero) return null;
try
{
Inventory store = client?.Inventory?.GetStore;
if (store != null && store.Items.TryGetValue(itemUuid, out InventoryNode node) && node?.Data is InventoryItem invItem)
return invItem.Name;
}
catch { }
return null;
}
private static Vector3 GetAttachmentBoneWorldPos(AttachmentPoint pt, Dictionary<string, Vector3> bonePosMap = null)
{
EnsureSkeletonLoaded();
string boneName = pt switch
{
AttachmentPoint.Chest => "mChest",
AttachmentPoint.Skull => "mSkull",
AttachmentPoint.LeftShoulder => "mCollarLeft",
AttachmentPoint.RightShoulder => "mCollarRight",
AttachmentPoint.LeftHand or AttachmentPoint.LeftHandRing => "mWristLeft",
AttachmentPoint.RightHand or AttachmentPoint.RightHandRing or AttachmentPoint.Default => "mWristRight",
AttachmentPoint.LeftFoot or AttachmentPoint.LeftHindFoot => "mAnkleLeft",
AttachmentPoint.RightFoot or AttachmentPoint.RightHindFoot => "mAnkleRight",
AttachmentPoint.Spine or AttachmentPoint.Stomach => "mTorso",
AttachmentPoint.Pelvis or AttachmentPoint.Root or AttachmentPoint.TailBase or AttachmentPoint.Groin => "mPelvis",
AttachmentPoint.Mouth or AttachmentPoint.Chin or AttachmentPoint.Nose or AttachmentPoint.Jaw or AttachmentPoint.Tongue => "mHead",
AttachmentPoint.LeftEar or AttachmentPoint.AltLeftEar => "mHead",
AttachmentPoint.RightEar or AttachmentPoint.AltRightEar => "mHead",
AttachmentPoint.LeftEyeball or AttachmentPoint.AltLeftEye => "mEyeLeft",
AttachmentPoint.RightEyeball or AttachmentPoint.AltRightEye => "mEyeRight",
AttachmentPoint.RightUpperArm => "mShoulderRight",
AttachmentPoint.RightForearm => "mElbowRight",
AttachmentPoint.LeftUpperArm => "mShoulderLeft",
AttachmentPoint.LeftForearm => "mElbowLeft",
AttachmentPoint.RightHip or AttachmentPoint.RightUpperLeg => "mHipRight",
AttachmentPoint.RightLowerLeg => "mKneeRight",
AttachmentPoint.LeftHip or AttachmentPoint.LeftUpperLeg => "mHipLeft",
AttachmentPoint.LeftLowerLeg => "mKneeLeft",
AttachmentPoint.LeftPec or AttachmentPoint.RightPec or AttachmentPoint.LeftWing or AttachmentPoint.RightWing => "mChest",
AttachmentPoint.Neck => "mNeck",
_ => "mPelvis"
};
var map = bonePosMap ?? _cachedBoneWorldPos;
return (map != null && map.TryGetValue(boneName, out Vector3 p)) ? p : new Vector3(0f, 0f, 1.067f);
}
private static string[] GetPreferredBonesForAttachment(string itemName, AttachmentPoint pt)
{
string name = (itemName ?? "").ToLowerInvariant();
// 1. Head / Face / Eyes / Mouth / Teeth / Tongue / Ears / Piercings / Glasses
if (name.Contains("head") || name.Contains("face") || name.Contains("eye") || name.Contains("lash") ||
name.Contains("brow") || name.Contains("teeth") || name.Contains("mouth") || name.Contains("tongue") ||
name.Contains("earring") || name.Contains("glasses") || name.Contains("piercing") ||
pt is AttachmentPoint.Chin or AttachmentPoint.Mouth or AttachmentPoint.Nose or AttachmentPoint.Jaw or
AttachmentPoint.Tongue or AttachmentPoint.LeftEar or AttachmentPoint.RightEar or
AttachmentPoint.LeftEyeball or AttachmentPoint.RightEyeball or AttachmentPoint.AltLeftEye or AttachmentPoint.AltRightEye)
{
return ["mHead", "mSkull", "mNeck", "mChest", "mPelvis"];
}
// 2. Neck / Collar / Choker / Extender
if (name.Contains("neck") || name.Contains("collar") || name.Contains("choker") || pt == AttachmentPoint.Neck)
{
return ["mNeck", "mChest", "mHead", "mPelvis"];
}
// 3. Hair / Hat / Helmet / Wig
if (name.Contains("hair") || name.Contains("hat") || name.Contains("helmet") || name.Contains("wig") || pt == AttachmentPoint.Skull)
{
return ["mSkull", "mHead", "mNeck", "mChest", "mPelvis"];
}
// 4. Feet / Shoes / Boots / Socks
if (name.Contains("shoe") || name.Contains("boot") || name.Contains("sneaker") || name.Contains("heel") ||
name.Contains("foot") || name.Contains("feet") || name.Contains("sock") ||
pt is AttachmentPoint.LeftFoot or AttachmentPoint.RightFoot or AttachmentPoint.LeftLowerLeg or AttachmentPoint.RightLowerLeg)
{
if (pt is AttachmentPoint.LeftFoot or AttachmentPoint.LeftLowerLeg || name.Contains("left") || name.Contains(" - l") || name.Contains("[l]"))
return ["mAnkleLeft", "mKneeLeft", "mHipLeft", "mPelvis"];
if (pt is AttachmentPoint.RightFoot or AttachmentPoint.RightLowerLeg || name.Contains("right") || name.Contains(" - r") || name.Contains("[r]"))
return ["mAnkleRight", "mKneeRight", "mHipRight", "mPelvis"];
return ["mAnkleLeft", "mAnkleRight", "mKneeLeft", "mKneeRight", "mHipLeft", "mHipRight", "mPelvis"];
}
// 5. Hands / Gloves / Rings / Bracelets
if (name.Contains("glove") || name.Contains("ring") || name.Contains("bracelet") || name.Contains("nail") ||
(name.Contains("hand") && !name.Contains("handle") && !name.Contains("bag") && !name.Contains("prop")))
{
if (pt is AttachmentPoint.LeftHand or AttachmentPoint.LeftHandRing or AttachmentPoint.LeftForearm || name.Contains("left") || name.Contains(" - l") || name.Contains("[l]"))
return ["mWristLeft", "mElbowLeft", "mShoulderLeft", "mChest", "mPelvis"];
if (pt is AttachmentPoint.RightHand or AttachmentPoint.RightHandRing or AttachmentPoint.RightForearm || name.Contains("right") || name.Contains(" - r") || name.Contains("[r]"))
return ["mWristRight", "mElbowRight", "mShoulderRight", "mChest", "mPelvis"];
return ["mWristLeft", "mWristRight", "mElbowLeft", "mElbowRight", "mChest", "mPelvis"];
}
// 6. Upper Body / Tops / Shirts / Jackets / Pecs
if (name.Contains("shirt") || name.Contains("top") || name.Contains("jacket") || name.Contains("sweater") ||
name.Contains("vest") || name.Contains("bra") || pt is AttachmentPoint.Chest or AttachmentPoint.LeftPec or AttachmentPoint.RightPec)
{
return ["mChest", "mTorso", "mPelvis"];
}
// 7. Lower Body / Pants / Shorts / Skirts / Pelvis
if (name.Contains("pant") || name.Contains("short") || name.Contains("skirt") || name.Contains("thong") ||
name.Contains("jean") || name.Contains("undie") || pt is AttachmentPoint.Pelvis or AttachmentPoint.Groin)
{
return ["mPelvis", "mTorso", "mHipLeft", "mHipRight"];
}
// Default fallback
return
[
"mPelvis", "mTorso", "mChest", "mNeck", "mHead", "mSkull",
"mSpine1", "mSpine2", "mSpine3", "mSpine4",
"mHipLeft", "mHipRight", "mKneeLeft", "mKneeRight", "mAnkleLeft", "mAnkleRight",
"mCollarLeft", "mCollarRight", "mShoulderLeft", "mShoulderRight", "mElbowLeft", "mElbowRight", "mWristLeft", "mWristRight"
];
}
private static bool IsHudAttachmentPoint(AttachmentPoint pt)
{
return pt is AttachmentPoint.HUDCenter2 or AttachmentPoint.HUDTopRight or AttachmentPoint.HUDTop
or AttachmentPoint.HUDTopLeft or AttachmentPoint.HUDCenter or AttachmentPoint.HUDBottomLeft
or AttachmentPoint.HUDBottom or AttachmentPoint.HUDBottomRight;
}
private static bool TryGetMatrix16(OSD osd, int index, out float[] mat)
{
mat = null;
if (osd is not OSDArray arr) return false;
if (index >= 0 && index < arr.Count && arr[index] is OSDArray subArr && subArr.Count >= 16)
{
mat = new float[16];
for (int i = 0; i < 16; i++) mat[i] = (float)subArr[i].AsReal();
return true;
}
if (index == 0 && arr.Count >= 16 && arr[0] is not OSDArray)
{
mat = new float[16];
for (int i = 0; i < 16; i++) mat[i] = (float)arr[i].AsReal();
return true;
}
if (arr.Count >= (index + 1) * 16 && arr[0] is not OSDArray)
{
mat = new float[16];
for (int i = 0; i < 16; i++) mat[i] = (float)arr[index * 16 + i].AsReal();
return true;
}
return false;
}
private static string FloatsToBase64(float[] arr)
{
byte[] bytes = new byte[arr.Length * 4];
Buffer.BlockCopy(arr, 0, bytes, 0, bytes.Length);
return Convert.ToBase64String(bytes);
}
private static string UShortsToBase64(ushort[] arr)
{
byte[] bytes = new byte[arr.Length * 2];
Buffer.BlockCopy(arr, 0, bytes, 0, bytes.Length);
return Convert.ToBase64String(bytes);
}
private static object LoadLlmPart(string fileName, string partName, string texUuid, string bakeName, Vector3 offset)
{
try
{
string[] dirs =
[
System.IO.Path.Combine(Settings.RESOURCE_DIR, "character"),
"/app/linden/character",
System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "linden", "character")
];
string dir = dirs.FirstOrDefault(d => File.Exists(System.IO.Path.Combine(d, fileName)));
if (dir == null) return null;
var lm = new LindenMesh(partName);
lm.LoadMesh(System.IO.Path.Combine(dir, fileName));
if (lm.NumVertices == 0 || lm.NumFaces == 0) return null;
float[] pos = new float[lm.NumVertices * 3];
float[] norm = new float[lm.NumVertices * 3];
float[] uv = new float[lm.NumVertices * 2];
for (int i = 0; i < lm.NumVertices; i++)
{
var v = lm.Vertices[i];
pos[i * 3] = v.Coord.X + offset.X;
pos[i * 3 + 1] = v.Coord.Y + offset.Y;
pos[i * 3 + 2] = v.Coord.Z + offset.Z;
norm[i * 3] = v.Normal.X;
norm[i * 3 + 1] = v.Normal.Y;
norm[i * 3 + 2] = v.Normal.Z;
uv[i * 2] = v.TexCoord.X;
uv[i * 2 + 1] = v.TexCoord.Y;
}
ushort[] idx = new ushort[lm.NumFaces * 3];
for (int f = 0; f < lm.NumFaces; f++)
{
idx[f * 3] = (ushort)lm.Faces[f].Indices[0];
idx[f * 3 + 1] = (ushort)lm.Faces[f].Indices[1];
idx[f * 3 + 2] = (ushort)lm.Faces[f].Indices[2];
}
return new
{
name = partName,
tex = texUuid ?? "",
bake = bakeName ?? "",
v_count = lm.NumVertices,
b64_pos = FloatsToBase64(pos),
b64_norm = FloatsToBase64(norm),
b64_uv = FloatsToBase64(uv),
b64_idx = UShortsToBase64(idx)
};
}
catch
{
return null;
}
}
[About("Returns decoded 3D avatar meshes, baked texture UUIDs, and worn attachment metadata for the bot")]
[CmdTypeGet()]
public object GetBotAvatarRender()
{
var client = GetClient();
if (client?.Network?.CurrentSim == null) return Failure("Not in a sim");
EnsureSkeletonLoaded();
UUID botID = client.Self.AgentID;
Avatar botAvatar = client.Network.CurrentSim.ObjectsAvatars.Values.FirstOrDefault(a => a.ID == botID);
uint botLocalID = botAvatar?.LocalID ?? client.Self.LocalID;
// Transparent / invisible placeholder textures & empty server bake hashes in SL
var invisibleTextures = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"8dcd4a48-2d37-4909-9f78-f7a9eb4ef903", // Default transparent texture
"f54a0c32-3cd1-d49a-5b4f-7b792bebc204", // Invisible alpha texture
"3a367d1c-bef1-6d43-7595-e88c1e3aadb3", // Full-alpha transparent bake hash
"76bf1429-475f-d122-8a32-b52716b43e99" // Empty universal/aux bake hash
};
// Download missing wearable assets for worn wearables (especially Universal wearables for BOM)
var wornWearables = client.Appearance.GetWearables().ToList();
var missingWearables = wornWearables.Where(w => w.Asset == null && w.AssetID != UUID.Zero).ToList();
if (missingWearables.Count > 0)
{
using var wCde = new CountdownEvent(missingWearables.Count);
foreach (var w in missingWearables)
{
client.Assets.RequestAsset(w.AssetID, w.AssetType, true, delegate (AssetDownload transfer, Asset asset)
{
try
{
if (transfer.Success && asset is AssetWearable aw)
{
w.Asset = aw;
w.Asset.Decode();
}
}
catch { }
finally { wCde.Signal(); }
});
}
wCde.Wait(TimeSpan.FromSeconds(3));
}
string wearableAux1 = "";
string wearableAux2 = "";
string wearableAux3 = "";
string wearableLeftArm = "";
string wearableLeftLeg = "";
foreach (var w in wornWearables)
{
if (w.Asset?.Textures != null)
{
if (string.IsNullOrEmpty(wearableAux1) &&
(w.Asset.Textures.TryGetValue(AvatarTextureIndex.Aux1Tattoo, out UUID t1) ||
w.Asset.Textures.TryGetValue(AvatarTextureIndex.Aux1Baked, out t1)) &&
t1 != UUID.Zero && !invisibleTextures.Contains(t1.ToString()))
{
wearableAux1 = t1.ToString();
}
if (string.IsNullOrEmpty(wearableAux2) &&
(w.Asset.Textures.TryGetValue(AvatarTextureIndex.Aux2Tattoo, out UUID t2) ||
w.Asset.Textures.TryGetValue(AvatarTextureIndex.Aux2Baked, out t2)) &&
t2 != UUID.Zero && !invisibleTextures.Contains(t2.ToString()))
{
wearableAux2 = t2.ToString();
}
if (string.IsNullOrEmpty(wearableAux3) &&
(w.Asset.Textures.TryGetValue(AvatarTextureIndex.Aux3Tattoo, out UUID t3) ||
w.Asset.Textures.TryGetValue(AvatarTextureIndex.Aux3Baked, out t3)) &&
t3 != UUID.Zero && !invisibleTextures.Contains(t3.ToString()))
{
wearableAux3 = t3.ToString();
}
if (string.IsNullOrEmpty(wearableLeftArm) &&
(w.Asset.Textures.TryGetValue(AvatarTextureIndex.LeftArmTattoo, out UUID tla) ||
w.Asset.Textures.TryGetValue(AvatarTextureIndex.LeftArmBaked, out tla)) &&
tla != UUID.Zero && !invisibleTextures.Contains(tla.ToString()))
{
wearableLeftArm = tla.ToString();
}
if (string.IsNullOrEmpty(wearableLeftLeg) &&
(w.Asset.Textures.TryGetValue(AvatarTextureIndex.LeftLegTattoo, out UUID tll) ||
w.Asset.Textures.TryGetValue(AvatarTextureIndex.LegLegBaked, out tll)) &&
tll != UUID.Zero && !invisibleTextures.Contains(tll.ToString()))
{
wearableLeftLeg = tll.ToString();
}
}
}
string GetBakeTex(AvatarTextureIndex idx, AvatarTextureIndex fallbackIdx = AvatarTextureIndex.Unknown)
{
UUID id = UUID.Zero;
var face = botAvatar?.Textures?.GetFace((uint)idx);
if (face != null && face.TextureID != UUID.Zero && face.TextureID != AppearanceManager.DEFAULT_AVATAR_TEXTURE)
id = face.TextureID;
if (id == UUID.Zero && fallbackIdx != AvatarTextureIndex.Unknown)
{
var fbFace = botAvatar?.Textures?.GetFace((uint)fallbackIdx);
if (fbFace != null && fbFace.TextureID != UUID.Zero && fbFace.TextureID != AppearanceManager.DEFAULT_AVATAR_TEXTURE)
id = fbFace.TextureID;
}
return id == UUID.Zero ? "" : id.ToString();
}
string headBake = GetBakeTex(AvatarTextureIndex.HeadBaked, AvatarTextureIndex.HeadBodypaint);
string upperBake = GetBakeTex(AvatarTextureIndex.UpperBaked, AvatarTextureIndex.UpperBodypaint);
string lowerBake = GetBakeTex(AvatarTextureIndex.LowerBaked, AvatarTextureIndex.LowerBodypaint);
string eyesBake = GetBakeTex(AvatarTextureIndex.EyesBaked, AvatarTextureIndex.EyesIris);
string skirtBake = GetBakeTex(AvatarTextureIndex.SkirtBaked, AvatarTextureIndex.Skirt);
string hairBake = GetBakeTex(AvatarTextureIndex.HairBaked, AvatarTextureIndex.Hair);
string leftArmBake = GetBakeTex(AvatarTextureIndex.LeftArmBaked);
if (string.IsNullOrEmpty(leftArmBake) || invisibleTextures.Contains(leftArmBake))
leftArmBake = !string.IsNullOrEmpty(wearableLeftArm) ? wearableLeftArm : upperBake;
string leftLegBake = GetBakeTex(AvatarTextureIndex.LegLegBaked);
if (string.IsNullOrEmpty(leftLegBake) || invisibleTextures.Contains(leftLegBake))
leftLegBake = !string.IsNullOrEmpty(wearableLeftLeg) ? wearableLeftLeg : lowerBake;
string aux1Bake = GetBakeTex(AvatarTextureIndex.Aux1Baked);
if (string.IsNullOrEmpty(aux1Bake) || invisibleTextures.Contains(aux1Bake))
aux1Bake = !string.IsNullOrEmpty(wearableAux1) ? wearableAux1 : headBake;
string aux2Bake = GetBakeTex(AvatarTextureIndex.Aux2Baked);
if (string.IsNullOrEmpty(aux2Bake) || invisibleTextures.Contains(aux2Bake))
aux2Bake = !string.IsNullOrEmpty(wearableAux2) ? wearableAux2 : headBake;
string aux3Bake = GetBakeTex(AvatarTextureIndex.Aux3Baked);
if (string.IsNullOrEmpty(aux3Bake) || invisibleTextures.Contains(aux3Bake))
aux3Bake = !string.IsNullOrEmpty(wearableAux3) ? wearableAux3 : headBake;
// Bakes-on-Mesh (BoM) magic UUID lookup -> (bakeName, realTextureUUID)
var bomMap = new Dictionary<string, (string bake, string tex)>(StringComparer.OrdinalIgnoreCase)
{
["5a9f4a74-30f2-821c-b88d-70499d3e7183"] = ("head", headBake),
["ae2de45c-d252-50b8-5c6e-19f39ce79317"] = ("upper", upperBake),
["24daea5f-0539-cfcf-047f-fbc40b2786ba"] = ("lower", lowerBake),
["52cc6bb6-2ee5-e632-d3ad-50197b1dcb8a"] = ("eyes", eyesBake),
["43529ce8-7faa-ad92-165a-bc4078371687"] = ("skirt", skirtBake),
["09aac1fb-6bce-0bee-7d44-caac6dbb6c63"] = ("hair", hairBake),
["ff62763f-d60a-9855-890b-0c96f8f8cd98"] = ("leftarm", leftArmBake),
["8e915e25-31d1-cc95-ae08-d58a47488251"] = ("leftleg", leftLegBake),
["9742065b-19b5-297c-858a-29711d539043"] = ("aux1", aux1Bake),
["03642e83-2bd1-4eb9-34b4-4c47ed586d2d"] = ("aux2", aux2Bake),
["26deb8f9-f618-ef6f-13c2-2b2c99765604"] = ("aux3", aux3Bake)
};
// 1. Base System Avatar Meshes (.llm)
var baseMeshes = new List<object>();
var mHead = LoadLlmPart("avatar_head.llm", "head", headBake, "head", Vector3.Zero);
if (mHead != null) baseMeshes.Add(mHead);
var mUpper = LoadLlmPart("avatar_upper_body.llm", "upper_body", upperBake, "upper", Vector3.Zero);
if (mUpper != null) baseMeshes.Add(mUpper);
var mLower = LoadLlmPart("avatar_lower_body.llm", "lower_body", lowerBake, "lower", Vector3.Zero);
if (mLower != null) baseMeshes.Add(mLower);
var mEyeL = LoadLlmPart("avatar_eye.llm", "eye_left", eyesBake, "eyes", _cachedBoneWorldPos["mEyeLeft"]);
if (mEyeL != null) baseMeshes.Add(mEyeL);
var mEyeR = LoadLlmPart("avatar_eye.llm", "eye_right", eyesBake, "eyes", _cachedBoneWorldPos["mEyeRight"]);
if (mEyeR != null) baseMeshes.Add(mEyeR);
if (!string.IsNullOrEmpty(skirtBake))
{
var mSkirt = LoadLlmPart("avatar_skirt.llm", "skirt", skirtBake, "skirt", Vector3.Zero);
if (mSkirt != null) baseMeshes.Add(mSkirt);
}
// 2. Worn Wearables Metadata
var wearablesList = new List<object>();
try
{
foreach (var wd in wornWearables)
{
string wName = LookupInventoryItemName(client, wd.ItemID) ?? wd.Asset?.Name ?? wd.WearableType.ToString();
var wTexs = new Dictionary<string, string>();
if (wd.Asset?.Textures != null)
{
foreach (var kvp in wd.Asset.Textures)
wTexs[kvp.Key.ToString()] = kvp.Value.ToString();
}
wearablesList.Add(new
{
type = wd.WearableType.ToString(),
name = wName,
item_uuid = wd.ItemID.ToString(),
asset_uuid = wd.AssetID.ToString(),
textures = wTexs
});
}
}
catch { }
// 3. Find all attached prims on the bot
var allPrims = client.Network.CurrentSim.ObjectsPrimitives.Values.ToList();
var rootAttachments = allPrims.Where(p => botLocalID != 0 && p.ParentID == botLocalID).ToList();
// Collect all mesh asset UUIDs needed across all non-HUD attachments & their linkset children
var meshUuidsToFetch = new HashSet<UUID>();
foreach (var rootPrim in rootAttachments)
{
if (IsHudAttachmentPoint(rootPrim.PrimData.AttachmentPoint)) continue;
var linkset = new List<Primitive> { rootPrim };
linkset.AddRange(allPrims.Where(cp => cp.ParentID == rootPrim.LocalID));
foreach (var p in linkset)
{
if (p.Sculpt != null && p.Sculpt.Type == SculptType.Mesh && p.Sculpt.SculptTexture != UUID.Zero)
{
if (!_avatarMeshCache.ContainsKey(p.Sculpt.SculptTexture))
meshUuidsToFetch.Add(p.Sculpt.SculptTexture);
}
}
}
if (meshUuidsToFetch.Count > 0)
{
using var cde = new CountdownEvent(meshUuidsToFetch.Count);
foreach (UUID meshId in meshUuidsToFetch)
{
client.Assets.RequestMesh(meshId, (success, meshAsset) =>
{
try
{
if (success && meshAsset != null && meshAsset.Decode())
_avatarMeshCache[meshId] = meshAsset;
}
catch { }
finally
{
try { cde.Signal(); } catch { }
}
});
}
cde.Wait(5500);
}
EnsureSkeletonLoaded();
var avatarBoneWorldPos = new Dictionary<string, Vector3>(_cachedBoneWorldPos, StringComparer.OrdinalIgnoreCase);
// Scan non-HUD attachments to find primary worn mesh body and extract bone overrides
int maxBodyVertCount = 0;
OSDMap primaryBodySkin = null;
foreach (var rootPrim in rootAttachments)
{
if (IsHudAttachmentPoint(rootPrim.PrimData.AttachmentPoint)) continue;
var linkset = new List<Primitive> { rootPrim };
linkset.AddRange(allPrims.Where(cp => cp.ParentID == rootPrim.LocalID));
foreach (var p in linkset)
{
if (p.Sculpt == null || p.Sculpt.Type != SculptType.Mesh || p.Sculpt.SculptTexture == UUID.Zero) continue;
if (!_avatarMeshCache.TryGetValue(p.Sculpt.SculptTexture, out AssetMesh meshAsset) || meshAsset?.MeshData == null) continue;
if (meshAsset.MeshData.TryGetValue("skin", out OSD skinOsd) && skinOsd is OSDMap skinMap)
{
if (skinMap.TryGetValue("joint_names", out OSD jnOsd) && jnOsd is OSDArray jnArr &&
skinMap.TryGetValue("inverse_bind_matrix", out OSD ibmOsd) && ibmOsd is OSDArray)
{
bool hasPelvis = false;
bool hasNeckOrChest = false;
for (int j = 0; j < jnArr.Count; j++)
{
string jn = jnArr[j].AsString();
if (string.Equals(jn, "mPelvis", StringComparison.OrdinalIgnoreCase)) hasPelvis = true;
if (string.Equals(jn, "mNeck", StringComparison.OrdinalIgnoreCase) || string.Equals(jn, "mChest", StringComparison.OrdinalIgnoreCase)) hasNeckOrChest = true;
}
if (hasPelvis && hasNeckOrChest)
{
int vCount = 0;
if (meshAsset.MeshData.TryGetValue("high_lod", out OSD hlOsd) && hlOsd is OSDArray subMeshes)
{
foreach (var sm in subMeshes)
{
if (sm is OSDMap smMap && smMap.TryGetValue("Position", out OSD pOsd))
{
byte[] pb = pOsd;
if (pb != null) vCount += pb.Length / 6;
}
}
}
if (vCount > maxBodyVertCount && vCount > 800)
{
maxBodyVertCount = vCount;
primaryBodySkin = skinMap;
}
}
}
}
}
}
if (primaryBodySkin != null &&
primaryBodySkin.TryGetValue("joint_names", out OSD pbJnOsd) && pbJnOsd is OSDArray pbJnArr &&
primaryBodySkin.TryGetValue("inverse_bind_matrix", out OSD pbIbmOsd) && pbIbmOsd is OSDArray)
{
int pelvisIdx = -1;
for (int j = 0; j < pbJnArr.Count; j++)
{
if (string.Equals(pbJnArr[j].AsString(), "mPelvis", StringComparison.OrdinalIgnoreCase))
{
pelvisIdx = j;
break;
}
}
if (pelvisIdx >= 0 && TryGetMatrix16(pbIbmOsd, pelvisIdx, out float[] pelvisIbm))
{
Vector3 bodyBindPelvis = new(-pelvisIbm[12], -pelvisIbm[13], -pelvisIbm[14]);
Vector3 worldPelvis = _cachedBoneWorldPos["mPelvis"];
for (int j = 0; j < pbJnArr.Count; j++)
{
string jName = pbJnArr[j].AsString();
if (_cachedBoneWorldPos.ContainsKey(jName) && TryGetMatrix16(pbIbmOsd, j, out float[] boneIbm))
{
Vector3 bodyBindPos = new(-boneIbm[12], -boneIbm[13], -boneIbm[14]);
avatarBoneWorldPos[jName] = worldPelvis + (bodyBindPos - bodyBindPelvis);
}
}
if (avatarBoneWorldPos.TryGetValue("mNeck", out Vector3 curNeck) && _cachedBoneWorldPos.TryGetValue("mNeck", out Vector3 defNeck))
{
Vector3 deltaNeck = curNeck - defNeck;
avatarBoneWorldPos["mHead"] = _cachedBoneWorldPos["mHead"] + deltaNeck;
avatarBoneWorldPos["mSkull"] = _cachedBoneWorldPos["mSkull"] + deltaNeck;
avatarBoneWorldPos["mEyeLeft"] = _cachedBoneWorldPos["mEyeLeft"] + deltaNeck;
avatarBoneWorldPos["mEyeRight"] = _cachedBoneWorldPos["mEyeRight"] + deltaNeck;
}
}
}
var attachmentsMeta = new List<object>();
var decodedAttachmentMeshes = new List<object>();
foreach (var rootPrim in rootAttachments)
{
AttachmentPoint attachPt = rootPrim.PrimData.AttachmentPoint;
bool isHud = IsHudAttachmentPoint(attachPt);
string invUuid = "";
if (rootPrim.NameValues != null)
{
foreach (var nv in rootPrim.NameValues)
{
if (nv.Name == "AttachItemID" && nv.Value != null)
{
invUuid = nv.Value.ToString();
break;
}
}
}
string itemName = rootPrim.Properties?.Name;
if (string.IsNullOrEmpty(itemName) && UUID.TryParse(invUuid, out UUID parsedInvId))
{
itemName = LookupInventoryItemName(client, parsedInvId);
}
if (string.IsNullOrEmpty(itemName))
{
client.Objects.RequestObjectPropertiesFamily(client.Network.CurrentSim, rootPrim.ID);
itemName = $"Attachment ({attachPt})";
}
var linkset = new List<Primitive> { rootPrim };
linkset.AddRange(allPrims.Where(cp => cp.ParentID == rootPrim.LocalID));
bool anyRigged = false;
int meshFacesAdded = 0;
var linksetDebug = new List<string>();
if (!isHud)
{
Vector3 attachBonePos = GetAttachmentBoneWorldPos(attachPt, avatarBoneWorldPos);
int primIdx = 0;
foreach (var p in linkset)
{
primIdx++;
string sculptTexStr = (p.Sculpt != null && p.Sculpt.SculptTexture != UUID.Zero) ? p.Sculpt.SculptTexture.ToString() : "none";
if (p.Sculpt == null || p.Sculpt.Type != SculptType.Mesh || p.Sculpt.SculptTexture == UUID.Zero)
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} ({p.LocalID}): Non-mesh sculpt={sculptTexStr} type={p.Sculpt?.Type}");
continue;
}
if (!_avatarMeshCache.TryGetValue(p.Sculpt.SculptTexture, out AssetMesh meshAsset) || meshAsset?.MeshData == null)
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} ({p.LocalID}): Mesh {sculptTexStr} not in cache/null");
continue;
}
bool isRigged = false;
float[] bsm = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
float[] refIbm = null;
Vector3 refBoneWorldPos = Vector3.Zero;
float pelvisZOffset = 0f;
string chosenBoneName = "";
var boneBindMap = new Dictionary<string, float[]>();
bool hasAltIbm = false;
if (meshAsset.MeshData.TryGetValue("skin", out OSD skinOsd) && skinOsd is OSDMap skinMap)
{
isRigged = true;
anyRigged = true;
hasAltIbm = skinMap.ContainsKey("alt_inverse_bind_matrix");
if (skinMap.TryGetValue("bind_shape_matrix", out OSD bsmOsd) && TryGetMatrix16(bsmOsd, 0, out float[] parsedBsm))
bsm = parsedBsm;
if (skinMap.TryGetValue("pelvis_offset", out OSD poOsd))
pelvisZOffset = (float)poOsd.AsReal();
if (skinMap.TryGetValue("joint_names", out OSD jnOsd) && jnOsd is OSDArray jnArr &&
skinMap.TryGetValue("inverse_bind_matrix", out OSD ibmOsd))
{
var jointNames = new List<string>(jnArr.Count);
for (int j = 0; j < jnArr.Count; j++)
{
string jName = jnArr[j].AsString();
jointNames.Add(jName);
if (jName is "mPelvis" or "mTorso" or "mChest" or "mNeck" or "mHead" or "mSkull")
{
if (TryGetMatrix16(ibmOsd, j, out float[] jm))
{
float[] altM = null;
if (skinMap.TryGetValue("alt_inverse_bind_matrix", out OSD altOsd))
TryGetMatrix16(altOsd, j, out altM);
boneBindMap[jName] = [jm[12], jm[13], jm[14], altM != null ? altM[12] : 0f, altM != null ? altM[13] : 0f, altM != null ? altM[14] : 0f];
}
}
}
string[] preferredRefBones = GetPreferredBonesForAttachment(itemName, attachPt);
int chosenIdx = -1;
foreach (string pref in preferredRefBones)
{
int idx = jointNames.FindIndex(n => string.Equals(n, pref, StringComparison.OrdinalIgnoreCase));
if (idx >= 0 && avatarBoneWorldPos.ContainsKey(jointNames[idx]))
{
chosenIdx = idx;
break;
}
}
if (chosenIdx < 0)
{
for (int j = 0; j < jointNames.Count; j++)
{
if (_cachedTrueBones.Contains(jointNames[j]) && avatarBoneWorldPos.ContainsKey(jointNames[j]))
{
chosenIdx = j;
break;
}
}
}
if (chosenIdx >= 0 && TryGetMatrix16(ibmOsd, chosenIdx, out float[] parsedIbm))
{
chosenBoneName = jointNames[chosenIdx];
refIbm = parsedIbm;
refBoneWorldPos = avatarBoneWorldPos[chosenBoneName];
}
}
}
// Always use high_lod because SL mesh creators frequently upload 4-vertex dummy triangles in medium_lod
OSD facesOsd = meshAsset.MeshData.TryGetValue("high_lod", out OSD hl) ? hl : null;
if (facesOsd is not OSDArray subMeshes)
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} ({p.LocalID}): No high_lod submeshes");
continue;
}
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} ({p.LocalID}): sculpt={sculptTexStr} rigged={isRigged} subMeshes={subMeshes.Count}");
Vector3 primPos = p == rootPrim ? rootPrim.Position : (rootPrim.Position + (p.Position * rootPrim.Rotation));
Quaternion primRot = p == rootPrim ? rootPrim.Rotation : (rootPrim.Rotation * p.Rotation);
Vector3 primScale = p.Scale;
for (int faceNr = 0; faceNr < subMeshes.Count; faceNr++)
{
if (subMeshes[faceNr] is not OSDMap subMesh)
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: not OSDMap");
continue;
}
if (subMesh.TryGetValue("NoGeometry", out OSD noGeom) && noGeom.AsBoolean())
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: NoGeometry");
continue;
}
if (!subMesh.TryGetValue("Position", out OSD posOsd) || !subMesh.TryGetValue("TriangleList", out OSD triOsd))
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: Missing pos/tri OSD");
continue;
}
var teFace = p.Textures?.GetFace((uint)faceNr) ?? p.Textures?.DefaultTexture;
float alpha = teFace != null ? teFace.RGBA.A : 1.0f;
string rawFaceTex = (teFace != null && teFace.TextureID != UUID.Zero) ? teFace.TextureID.ToString() : "";
string faceTex = rawFaceTex;
if (alpha < 0.02f)
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: SKIPPED Alpha low ({alpha:F2}) rawTex={rawFaceTex}");
continue;
}
if (invisibleTextures.Contains(faceTex))
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: SKIPPED In invisibleTextures ({faceTex})");
continue;
}
string faceBake = "";
if (bomMap.TryGetValue(faceTex, out var bomTarget))
{
faceBake = bomTarget.bake;
faceTex = bomTarget.tex;
if (string.IsNullOrEmpty(faceTex) || invisibleTextures.Contains(faceTex))
{
// Fallback to primary body part bake so we NEVER discard body/head/ear geometry
if (faceBake is "aux1" or "aux2" or "aux3" or "head")
{
faceTex = headBake;
faceBake = "head";
}
else if (faceBake is "leftarm" or "upper")
{
faceTex = upperBake;
faceBake = "upper";
}
else if (faceBake is "leftleg" or "lower")
{
faceTex = lowerBake;
faceBake = "lower";
}
}
}
if (string.IsNullOrEmpty(faceTex) || invisibleTextures.Contains(faceTex))
{
if (invisibleTextures.Contains(rawFaceTex))
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: SKIPPED Explicit invisible raw texture ({rawFaceTex})");
continue;
}
faceTex = headBake;
}
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: ACCEPTED alpha={alpha:F2} rawTex={rawFaceTex} finalTex={faceTex} bake={faceBake}");
string hexColor = "#ffffff";
if (teFace != null)
{
byte rB = (byte)Math.Clamp((int)(teFace.RGBA.R * 255), 0, 255);
byte gB = (byte)Math.Clamp((int)(teFace.RGBA.G * 255), 0, 255);
byte bB = (byte)Math.Clamp((int)(teFace.RGBA.B * 255), 0, 255);
hexColor = $"#{rB:X2}{gB:X2}{bB:X2}";
}
Vector3 posMin = new(-0.5f, -0.5f, -0.5f);
Vector3 posMax = new(0.5f, 0.5f, 0.5f);
if (subMesh.TryGetValue("PositionDomain", out OSD pdOsd) && pdOsd is OSDMap pdMap)
{
posMin = pdMap["Min"];
posMax = pdMap["Max"];
}
Vector2 uvMin = Vector2.Zero, uvMax = Vector2.Zero;
byte[] uvBytes = null;
if (subMesh.TryGetValue("TexCoord0", out OSD tcOsd) && subMesh.TryGetValue("TexCoord0Domain", out OSD tcdOsd) && tcdOsd is OSDMap tcdMap)
{
uvBytes = tcOsd;
uvMin = tcdMap["Min"];
uvMax = tcdMap["Max"];
}
byte[] posBytes = posOsd;
byte[] norBytes = subMesh.TryGetValue("Normal", out OSD nOsd) ? (byte[])nOsd : null;
byte[] triBytes = triOsd;
int vCount = posBytes.Length / 6;
if (vCount == 0 || triBytes.Length < 6)
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: vCount 0 or tri < 6");
continue;
}
float[] outPos = new float[vCount * 3];
float[] outNorm = new float[vCount * 3];
float[] outUv = new float[vCount * 2];
float minWX = float.MaxValue, maxWX = float.MinValue;
float minWY = float.MaxValue, maxWY = float.MinValue;
float minWZ = float.MaxValue, maxWZ = float.MinValue;
float minBZ = float.MaxValue, maxBZ = float.MinValue;
for (int vi = 0; vi < vCount; vi++)
{
int bOff = vi * 6;
float vx = Utils.UInt16ToFloat(Utils.BytesToUInt16(posBytes, bOff), posMin.X, posMax.X);
float vy = Utils.UInt16ToFloat(Utils.BytesToUInt16(posBytes, bOff + 2), posMin.Y, posMax.Y);
float vz = Utils.UInt16ToFloat(Utils.BytesToUInt16(posBytes, bOff + 4), posMin.Z, posMax.Z);
float nx = 0f, ny = 0f, nz = 1f;
if (norBytes != null && norBytes.Length >= bOff + 6)
{
nx = Utils.UInt16ToFloat(Utils.BytesToUInt16(norBytes, bOff), posMin.X, posMax.X);
ny = Utils.UInt16ToFloat(Utils.BytesToUInt16(norBytes, bOff + 2), posMin.Y, posMax.Y);
nz = Utils.UInt16ToFloat(Utils.BytesToUInt16(norBytes, bOff + 4), posMin.Z, posMax.Z);
}
float wx, wy, wz, wnx, wny, wnz;
if (isRigged)
{
float bx = vx * bsm[0] + vy * bsm[4] + vz * bsm[8] + bsm[12];
float by = vx * bsm[1] + vy * bsm[5] + vz * bsm[9] + bsm[13];
float bz = vx * bsm[2] + vy * bsm[6] + vz * bsm[10] + bsm[14];
if (bz < minBZ) minBZ = bz; if (bz > maxBZ) maxBZ = bz;
wnx = nx * bsm[0] + ny * bsm[4] + nz * bsm[8];
wny = nx * bsm[1] + ny * bsm[5] + nz * bsm[9];
wnz = nx * bsm[2] + ny * bsm[6] + nz * bsm[10];
if (refIbm != null)
{
wx = (bx * refIbm[0] + by * refIbm[4] + bz * refIbm[8] + refIbm[12]) + refBoneWorldPos.X;
wy = (bx * refIbm[1] + by * refIbm[5] + bz * refIbm[9] + refIbm[13]) + refBoneWorldPos.Y;
wz = (bx * refIbm[2] + by * refIbm[6] + bz * refIbm[10] + refIbm[14]) + refBoneWorldPos.Z + pelvisZOffset;
float rnx = wnx * refIbm[0] + wny * refIbm[4] + wnz * refIbm[8];
float rny = wnx * refIbm[1] + wny * refIbm[5] + wnz * refIbm[9];
float rnz = wnx * refIbm[2] + wny * refIbm[6] + wnz * refIbm[10];
wnx = rnx; wny = rny; wnz = rnz;
}
else
{
wx = bx;
wy = by;
wz = bz + 1.067f + pelvisZOffset;
}
}
else
{
Vector3 scaled = new(vx * primScale.X, vy * primScale.Y, vz * primScale.Z);
Vector3 rotated = scaled * primRot;
Vector3 finalP = attachBonePos + primPos + rotated;
wx = finalP.X; wy = finalP.Y; wz = finalP.Z;
Vector3 rotN = new Vector3(nx, ny, nz) * primRot;
wnx = rotN.X; wny = rotN.Y; wnz = rotN.Z;
}
float nLen = (float)Math.Sqrt(wnx * wnx + wny * wny + wnz * wnz);
if (nLen > 1e-4f) { wnx /= nLen; wny /= nLen; wnz /= nLen; }
if (wx < minWX) minWX = wx; if (wx > maxWX) maxWX = wx;
if (wy < minWY) minWY = wy; if (wy > maxWY) maxWY = wy;
if (wz < minWZ) minWZ = wz; if (wz > maxWZ) maxWZ = wz;
outPos[vi * 3] = wx;
outPos[vi * 3 + 1] = wy;
outPos[vi * 3 + 2] = wz;
outNorm[vi * 3] = wnx;
outNorm[vi * 3 + 1] = wny;
outNorm[vi * 3 + 2] = wnz;
int uvOff = vi * 4;
if (uvBytes != null && uvBytes.Length >= uvOff + 4)
{
float u = Utils.UInt16ToFloat(Utils.BytesToUInt16(uvBytes, uvOff), uvMin.X, uvMax.X);
float v = Utils.UInt16ToFloat(Utils.BytesToUInt16(uvBytes, uvOff + 2), uvMin.Y, uvMax.Y);
if (teFace != null)
{
u = (u - 0.5f) * teFace.RepeatU + teFace.OffsetU + 0.5f;
v = (v - 0.5f) * teFace.RepeatV + teFace.OffsetV + 0.5f;
}
outUv[vi * 2] = u;
outUv[vi * 2 + 1] = v;
}
}
// Skip microscopic hidden root/script prims (< 8mm or <= 36 vertices with < 3.5cm box)
float maxExtent = Math.Max(maxWX - minWX, Math.Max(maxWY - minWY, maxWZ - minWZ));
if (maxExtent < 0.008f || (vCount <= 36 && (maxExtent < 0.035f || (linkset.Count > 1 && vCount <= 8))))
{
if (itemName.Contains("CEYLON") || itemName.Contains("HEAD"))
linksetDebug.Add($"Prim {primIdx} F{faceNr}: maxExtent small ({maxExtent:F3}, vCount={vCount})");
continue;
}
decodedAttachmentMeshes.Add(new
{
name = itemName,
attach_point = attachPt.ToString(),
is_rigged = isRigged,
chosen_bone = chosenBoneName,
has_alt_ibm = hasAltIbm,
bz_min = Math.Round(minBZ, 3),
bz_max = Math.Round(maxBZ, 3),
bone_bind = boneBindMap,
tex = faceTex,
bake = faceBake,
color = hexColor,
alpha = Math.Round(alpha, 2),
v_count = vCount,
b64_pos = FloatsToBase64(outPos),
b64_norm = FloatsToBase64(outNorm),
b64_uv = FloatsToBase64(outUv),
b64_idx = Convert.ToBase64String(triBytes)
});
meshFacesAdded++;
}
}
}
attachmentsMeta.Add(new
{
uuid = rootPrim.ID.ToString(),
inv_uuid = invUuid,
name = itemName,
point = attachPt.ToString(),
is_hud = isHud,
is_rigged = anyRigged,
prim_count = linkset.Count,
mesh_faces = meshFacesAdded,
debug = linksetDebug
});
}
var response = new
{
avatar_uuid = botID.ToString(),
avatar_name = client.Self.Name,
bakes = new
{
head = headBake,
upper = upperBake,
lower = lowerBake,
eyes = eyesBake,
skirt = skirtBake,
hair = hairBake,
leftarm = leftArmBake,
leftleg = leftLegBake,
aux1 = aux1Bake,
aux2 = aux2Bake,
aux3 = aux3Bake
},
wearables = wearablesList,
attachments = attachmentsMeta,
base_meshes = baseMeshes,
meshes = decodedAttachmentMeshes
};
return BasicReply(JsonSerializer.Serialize(response, JsonOptions.UnsafeRelaxed));
}
[About("Downloads an SL texture or server-baked avatar texture and decodes it from JPEG2000 to base64 PNG")]
[ArgHints("textureUuid", "UUID of the texture", "UUID")]
[ArgHints("bakeName", "Optional server bake slot name (head, upper, lower, eyes, skirt, hair, or none)", "Text", "none")]
[CmdTypeGet()]
public object GetBotTexturePng(string textureUuid, string bakeName)
{
if (!UUID.TryParse(textureUuid, out UUID texId) || texId == UUID.Zero)
return Failure("Invalid texture UUID");
string cleanBake = (bakeName ?? "").Trim().ToLowerInvariant();
if (cleanBake == "none" || cleanBake == "null" || cleanBake == "-") cleanBake = "";
string cacheKey = $"{texId}_{cleanBake}_v2";
if (_pngTextureCache.TryGetValue(cacheKey, out byte[] cachedPng))
return BasicReply(Convert.ToBase64String(cachedPng));
var client = GetClient();
byte[] j2kBytes = null;
if (client.Assets.Cache.HasAsset(texId))
j2kBytes = client.Assets.Cache.GetCachedAssetBytes(texId);
if ((j2kBytes == null || j2kBytes.Length < 16) && !string.IsNullOrEmpty(cleanBake))
{
using var mreBake = new ManualResetEventSlim(false);
client.Assets.RequestServerBakedImage(client.Self.AgentID, texId, cleanBake, (state, asset) =>
{
if (state == TextureRequestState.Finished && asset?.AssetData != null && asset.AssetData.Length > 16)
j2kBytes = asset.AssetData;
if (state is TextureRequestState.Finished or TextureRequestState.NotFound or TextureRequestState.Timeout or TextureRequestState.Aborted)
mreBake.Set();
});
mreBake.Wait(6000);
}
if (j2kBytes == null || j2kBytes.Length < 16)
{
using var mreNorm = new ManualResetEventSlim(false);
client.Assets.RequestImage(texId, ImageType.Normal, (state, asset) =>
{
if (state == TextureRequestState.Finished && asset?.AssetData != null && asset.AssetData.Length > 16)
j2kBytes = asset.AssetData;
if (state is TextureRequestState.Finished or TextureRequestState.NotFound or TextureRequestState.Timeout or TextureRequestState.Aborted)
mreNorm.Set();
});
mreNorm.Wait(6500);
}
if (j2kBytes == null || j2kBytes.Length < 16)
return Failure("Texture download timed out or not found");
try
{
byte[] pngBytes = DecodeJ2kToPng(j2kBytes, cleanBake);
if (pngBytes == null || pngBytes.Length == 0)
return Failure("Failed to decode J2K image");
if (_pngTextureCache.Count > 160) _pngTextureCache.Clear();
_pngTextureCache[cacheKey] = pngBytes;
return BasicReply(Convert.ToBase64String(pngBytes));
}
catch (Exception ex)
{
return Failure("J2K decode error: " + ex.Message);
}
}
private static byte[] DecodeJ2kToPng(byte[] j2kBytes, string cleanBake = "")
{
var pImg = J2kImage.FromBytes(j2kBytes);
var mImg = new ManagedImage(pImg);
int srcW = mImg.Width, srcH = mImg.Height;
if (srcW <= 0 || srcH <= 0) return null;
int step = 1;
while (srcW / step > 512 || srcH / step > 512) step *= 2;
int dstW = Math.Max(1, srcW / step);
int dstH = Math.Max(1, srcH / step);
bool hasColor = (mImg.Channels & ManagedImage.ImageChannels.Color) != 0 && mImg.Red != null && mImg.Green != null && mImg.Blue != null;
bool hasGray = (mImg.Channels & ManagedImage.ImageChannels.Gray) != 0 && mImg.Red != null;
bool hasAlpha = (mImg.Channels & ManagedImage.ImageChannels.Alpha) != 0 && mImg.Alpha != null;
bool isOpaqueBake = cleanBake is "head" or "upper" or "lower" or "eyes" or "leftarm" or "leftleg" or "aux1" or "aux2" or "aux3";
byte[] scanlines = new byte[dstH * (1 + dstW * 4)];
int pos = 0;
for (int y = 0; y < dstH; y++)
{
scanlines[pos++] = 1; // PNG Filter 1 (Sub) for much better compression
int sy = Math.Min(srcH - 1, y * step);
int rowOffset = sy * srcW;
byte prevR = 0, prevG = 0, prevB = 0, prevA = 0;
for (int x = 0; x < dstW; x++)
{
int sx = Math.Min(srcW - 1, x * step);
int idx = rowOffset + sx;
byte r, g, b, a;
if (hasColor)
{
r = mImg.Red[idx];
g = mImg.Green[idx];
b = mImg.Blue[idx];
}
else if (hasGray)
{
r = g = b = mImg.Red[idx];
}
else
{
r = g = b = 255;
}
a = hasAlpha ? mImg.Alpha[idx] : (byte)255;
if (isOpaqueBake && a > 0)
a = 255;
scanlines[pos++] = unchecked((byte)(r - prevR));
scanlines[pos++] = unchecked((byte)(g - prevG));
scanlines[pos++] = unchecked((byte)(b - prevB));
scanlines[pos++] = unchecked((byte)(a - prevA));
prevR = r; prevG = g; prevB = b; prevA = a;
}
}
return EncodeRgbaToPng(dstW, dstH, scanlines);
}
private static byte[] EncodeRgbaToPng(int width, int height, byte[] rgbaScanlines)
{
using var ms = new MemoryStream();
ms.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }, 0, 8);
void WriteChunk(string type, byte[] data)
{
byte[] lenBytes = BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(data.Length));
ms.Write(lenBytes, 0, 4);
byte[] typeBytes = System.Text.Encoding.ASCII.GetBytes(type);
ms.Write(typeBytes, 0, 4);
if (data.Length > 0) ms.Write(data, 0, data.Length);
uint crc = 0xFFFFFFFF;
for (int i = 0; i < 4; i++)
{
crc ^= typeBytes[i];
for (int k = 0; k < 8; k++) crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320 : (crc >> 1);
}
for (int i = 0; i < data.Length; i++)
{
crc ^= data[i];
for (int k = 0; k < 8; k++) crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320 : (crc >> 1);
}
crc ^= 0xFFFFFFFF;
byte[] crcBytes = BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(unchecked((int)crc)));
ms.Write(crcBytes, 0, 4);
}
byte[] ihdr = new byte[13];
Buffer.BlockCopy(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(width)), 0, ihdr, 0, 4);
Buffer.BlockCopy(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(height)), 0, ihdr, 4, 4);
ihdr[8] = 8;
ihdr[9] = 6;
ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
WriteChunk("IHDR", ihdr);
byte[] idatData;
using (var zms = new MemoryStream())
{
using (var zs = new ZLibStream(zms, CompressionLevel.Fastest, true))
{
zs.Write(rgbaScanlines, 0, rgbaScanlines.Length);
}
idatData = zms.ToArray();
}
WriteChunk("IDAT", idatData);
WriteChunk("IEND", Array.Empty<byte>());
return ms.ToArray();
}
}
public class SculptysInfo
{
public string name = "";
public string isLinked = "False";
public string owner = "?";
public string uuid = "?";
public string pos = "<0,0,0>";
}
}