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 objects_copy = GetClient().Network.CurrentSim.ObjectsPrimitives.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); Dictionary mapLocalID = []; Dictionary sculpts = []; foreach (KeyValuePair 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 reply = []; foreach (KeyValuePair 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 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 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 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 restr = new List(); 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 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 neighborTerrains = []; try { List 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 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 _avatarMeshCache = new(); private static readonly ConcurrentDictionary _pngTextureCache = new(); private static Dictionary _cachedBoneWorldPos = null; private static HashSet _cachedTrueBones = null; private static void EnsureSkeletonLoaded() { if (_cachedBoneWorldPos != null && _cachedTrueBones != null) return; var bonePos = new Dictionary(StringComparer.OrdinalIgnoreCase); var trueBones = new HashSet(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 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(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(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(); 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(); try { foreach (var wd in wornWearables) { string wName = LookupInventoryItemName(client, wd.ItemID) ?? wd.Asset?.Name ?? wd.WearableType.ToString(); var wTexs = new Dictionary(); 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(); foreach (var rootPrim in rootAttachments) { if (IsHudAttachmentPoint(rootPrim.PrimData.AttachmentPoint)) continue; var linkset = new List { 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(_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 { 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(); var decodedAttachmentMeshes = new List(); 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 { rootPrim }; linkset.AddRange(allPrims.Where(cp => cp.ParentID == rootPrim.LocalID)); bool anyRigged = false; int meshFacesAdded = 0; var linksetDebug = new List(); 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(); 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(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()); 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>"; } }