2148 lines
111 KiB
C#
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>";
|
|
|
|
}
|
|
}
|