From 468091dd502fbc01e6527d8a2e1f88d95606ba0e Mon Sep 17 00:00:00 2001 From: JohnLegionH Date: Sun, 20 Sep 2026 12:46:02 -0500 Subject: [PATCH 1/8] feat(appearance-baking): OpenSimNGC.Appearance.Baking library + tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A standalone server-side bake compositor: it reproduces the Linden Lab viewer's texture-layer composition (avatar_lad.xml layer sets, alpha masks, the 5th "morph mask" component, single-tile J2K encode) so the simulator can bake an avatar without the viewer. Ships avatar_lad.xml and 56 character TGAs as embedded data resources. Their origin, licence (LGPL 2.1 with the Linden Lab viewer linking exception), viewer version (26.1.1) and per-file SHA-256 are recorded in THIRD-PARTY-NOTICES.md. The golden harness compares our output against reference viewer bakes. The fixtures are NOT committed (they are avatar data from a live grid); each set degrades to a printed SKIPPED line with the fetch instructions when its fixtures directory is absent, and asserts nothing. That is why the suite is green in a fresh checkout. Folded from feature/ais-v3, 15 commits: 564ca614d4 feat(ssb): OpenSimNGC.Appearance.Baking skeleton with embedded avatar_lad.xml 2142706024 feat(ssb): compositor moved into OpenSimNGC.Appearance.Baking; single-tile J2K; BakeHash e3b969c9ab test(ssb): golden harness against Truly's Firestorm bakes (fixtures fetched, not committed) 2e5c960485 fix(ssb): J2K decode treats 5-component viewer bakes as RGBA (bump channel dropped) 6f52f8bd81 docs(ssb): ledger P-1 (LL compositor is the authority), Q-7 resolved, Q-8/Q-9 opened, S0b logged; harness wording: reference bakes 203ec18577 feat(ssb): bump (5th component) pass per LL lltexlayer; masks re-verified against viewer tree 8835f480f7 fix(ssb): render no-texture layers once per worn wearable instance (LL template semantics) 0176eed007 docs(ssb): 5th component is the morph mask — rename, cite packing order bbc065bc5f test(ssb): golden RGB/A thresholds; docs: ADR-007 masks, packing-order citation 296051b301 test(ssb): multi-set golden harness + aleric-max reference set ade0e01a44 fix(ssb): upper morph mask gathers from the top worn wearable, asset or not e338ecc598 feat(ssb): library reports a channel where nothing was drawn 3455deff98 test(ssb): close S1 DoD — golden diff at the shipped bake size 86880b7316 feat(ssb): persist bakes and skip recompute on unchanged inputs 162bfadcc3 perf(ssb): instrument the bake phases and answer Q-10 --- .../AvatarLadResourceTests.cs | 25 + .../BackendTests.cs | 411 + .../CompositorTests.cs | 356 + .../Golden/.gitignore | 5 + .../Golden/GoldenTests.cs | 318 + .../Golden/aleric-max/manifest.json | 24 + .../Golden/fetch-fixtures.sh | 191 + .../Golden/truly-stock/manifest.json | 23 + .../OpenSimNGC.Appearance.Baking.Tests.csproj | 26 + .../VisualParamEncoderTests.cs | 67 + .../OpenSimNGC.Appearance.Baking/AvatarLad.cs | 236 + .../BakeChannel.cs | 32 + .../OpenSimNGC.Appearance.Baking/BakeHash.cs | 69 + .../BakeTimings.cs | 101 + .../OpenSimNGC.Appearance.Baking/Contracts.cs | 130 + .../Data/avatar_lad.xml | 17504 ++++++++++++++++ .../Data/character/aux_base.tga | Bin 0 -> 10284 bytes .../Data/character/blush_alpha.tga | Bin 0 -> 17352 bytes .../Data/character/body_skingrain.tga | Bin 0 -> 262188 bytes .../Data/character/bodyfreckles_alpha.tga | Bin 0 -> 257249 bytes .../Data/character/bump_face_wrinkles.tga | Bin 0 -> 25243 bytes .../Data/character/bump_head_base.tga | Bin 0 -> 105525 bytes .../Data/character/bump_lowerbody_base.tga | Bin 0 -> 112782 bytes .../Data/character/bump_pants_wrinkles.tga | Bin 0 -> 83183 bytes .../Data/character/bump_shirt_wrinkles.tga | Bin 0 -> 81501 bytes .../Data/character/bump_upperbody_base.tga | Bin 0 -> 147581 bytes .../Data/character/eyebrows_alpha.tga | Bin 0 -> 9469 bytes .../Data/character/eyeliner_alpha.tga | Bin 0 -> 4720 bytes .../Data/character/eyeshadow_inner_alpha.tga | Bin 0 -> 5466 bytes .../Data/character/eyeshadow_outer_alpha.tga | Bin 0 -> 7382 bytes .../Data/character/eyewhite.tga | Bin 0 -> 42353 bytes .../character/facehair_chincurtains_alpha.tga | Bin 0 -> 34610 bytes .../character/facehair_moustache_alpha.tga | Bin 0 -> 14017 bytes .../character/facehair_sideburns_alpha.tga | Bin 0 -> 27328 bytes .../character/facehair_soulpatch_alpha.tga | Bin 0 -> 11277 bytes .../Data/character/freckles_alpha.tga | Bin 0 -> 140558 bytes .../Data/character/glove_length_alpha.tga | Bin 0 -> 49745 bytes .../Data/character/gloves_fingers_alpha.tga | Bin 0 -> 39616 bytes .../Data/character/head_alpha.tga | Bin 0 -> 6066 bytes .../Data/character/head_color.tga | Bin 0 -> 70715 bytes .../Data/character/head_hair.tga | Bin 0 -> 75600 bytes .../Data/character/head_highlights_alpha.tga | Bin 0 -> 20503 bytes .../Data/character/head_shading_alpha.tga | Bin 0 -> 35304 bytes .../Data/character/head_skingrain.tga | Bin 0 -> 262376 bytes .../character/jacket_length_lower_alpha.tga | Bin 0 -> 9768 bytes .../character/jacket_length_upper_alpha.tga | Bin 0 -> 14617 bytes .../character/jacket_open_lower_alpha.tga | Bin 0 -> 19732 bytes .../character/jacket_open_upper_alpha.tga | Bin 0 -> 41606 bytes .../Data/character/lipgloss_alpha.tga | Bin 0 -> 4738 bytes .../Data/character/lips_mask.tga | Bin 0 -> 6110 bytes .../Data/character/lipstick_alpha.tga | Bin 0 -> 7966 bytes .../Data/character/lowerbody_color.tga | Bin 0 -> 135395 bytes .../character/lowerbody_highlights_alpha.tga | Bin 0 -> 8695 bytes .../character/lowerbody_shading_alpha.tga | Bin 0 -> 41766 bytes .../Data/character/nailpolish_alpha.tga | Bin 0 -> 4656 bytes .../Data/character/pants_length_alpha.tga | Bin 0 -> 26843 bytes .../Data/character/pants_waist_alpha.tga | Bin 0 -> 10487 bytes .../Data/character/rosyface_alpha.tga | Bin 0 -> 44382 bytes .../Data/character/shirt_bottom_alpha.tga | Bin 0 -> 32242 bytes .../Data/character/shirt_collar_alpha.tga | Bin 0 -> 14417 bytes .../character/shirt_collar_back_alpha.tga | Bin 0 -> 12789 bytes .../Data/character/shirt_sleeve_alpha.tga | Bin 0 -> 72196 bytes .../Data/character/shoe_height_alpha.tga | Bin 0 -> 24461 bytes .../Data/character/skirt_length_alpha.tga | Bin 0 -> 4114 bytes .../Data/character/skirt_slit_back_alpha.tga | Bin 0 -> 90350 bytes .../Data/character/skirt_slit_front_alpha.tga | Bin 0 -> 90350 bytes .../Data/character/skirt_slit_left_alpha.tga | Bin 0 -> 82006 bytes .../Data/character/skirt_slit_right_alpha.tga | Bin 0 -> 91410 bytes .../Data/character/upperbody_color.tga | Bin 0 -> 23348 bytes .../character/upperbody_highlights_alpha.tga | Bin 0 -> 6509 bytes .../character/upperbody_shading_alpha.tga | Bin 0 -> 25297 bytes .../character/upperbodyfreckles_alpha.tga | Bin 0 -> 180104 bytes .../Docs/MORPH-MASK-PASS.md | 219 + Source/OpenSimNGC.Appearance.Baking/Enums.cs | 70 + .../FidelityCheck.cs | 51 + .../IBakeBackend.cs | 23 + .../OpenSimNGC.Appearance.Baking/J2kCodec.cs | 119 + .../OpenSimNGC.Appearance.Baking.csproj | 63 + Source/OpenSimNGC.Appearance.Baking/Raster.cs | 142 + .../ResourceImages.cs | 79 + .../SkiaBakeBackend.cs | 154 + .../THIRD-PARTY-NOTICES.md | 110 + .../TexLayerCompositor.cs | 685 + Source/OpenSimNGC.Appearance.Baking/Tga.cs | 109 + .../VisualParamEncoder.cs | 77 + .../WearableParser.cs | 74 + 86 files changed, 21493 insertions(+) create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/AvatarLadResourceTests.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/BackendTests.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/CompositorTests.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/Golden/.gitignore create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenTests.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/Golden/aleric-max/manifest.json create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/manifest.json create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/OpenSimNGC.Appearance.Baking.Tests.csproj create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/VisualParamEncoderTests.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/AvatarLad.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/BakeChannel.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/BakeHash.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/BakeTimings.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/Contracts.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/avatar_lad.xml create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/aux_base.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/blush_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/body_skingrain.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/bodyfreckles_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/bump_face_wrinkles.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/bump_head_base.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/bump_lowerbody_base.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/bump_pants_wrinkles.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/bump_shirt_wrinkles.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/bump_upperbody_base.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/eyebrows_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/eyeliner_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/eyeshadow_inner_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/eyeshadow_outer_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/eyewhite.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/facehair_chincurtains_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/facehair_moustache_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/facehair_sideburns_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/facehair_soulpatch_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/freckles_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/glove_length_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/gloves_fingers_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/head_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/head_color.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/head_hair.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/head_highlights_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/head_shading_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/head_skingrain.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_length_lower_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_length_upper_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_open_lower_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_open_upper_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/lipgloss_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/lips_mask.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/lipstick_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/lowerbody_color.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/lowerbody_highlights_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/lowerbody_shading_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/nailpolish_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/pants_length_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/pants_waist_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/rosyface_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_bottom_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_collar_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_collar_back_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_sleeve_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/shoe_height_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_length_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_back_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_front_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_left_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_right_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/upperbody_color.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/upperbody_highlights_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/upperbody_shading_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Data/character/upperbodyfreckles_alpha.tga create mode 100644 Source/OpenSimNGC.Appearance.Baking/Docs/MORPH-MASK-PASS.md create mode 100644 Source/OpenSimNGC.Appearance.Baking/Enums.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/FidelityCheck.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/IBakeBackend.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/J2kCodec.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/OpenSimNGC.Appearance.Baking.csproj create mode 100644 Source/OpenSimNGC.Appearance.Baking/Raster.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/ResourceImages.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/SkiaBakeBackend.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md create mode 100644 Source/OpenSimNGC.Appearance.Baking/TexLayerCompositor.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/Tga.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/VisualParamEncoder.cs create mode 100644 Source/OpenSimNGC.Appearance.Baking/WearableParser.cs diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/AvatarLadResourceTests.cs b/Source/OpenSimNGC.Appearance.Baking.Tests/AvatarLadResourceTests.cs new file mode 100644 index 00000000000..86e2eeebf8f --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/AvatarLadResourceTests.cs @@ -0,0 +1,25 @@ +using System.Xml.Linq; +using OpenSimNGC.Appearance.Baking; +using Xunit; + +namespace OpenSimNGC.Appearance.Baking.Tests; + +public class AvatarLadResourceTests +{ + private const string ResourceName = "OpenSimNGC.Appearance.Baking.Data.avatar_lad.xml"; + + [Fact] + public void AvatarLad_IsEmbedded_AndContainsAtLeastOneLayerSet() + { + var asm = typeof(BakeHash).Assembly; + + using Stream? stream = asm.GetManifestResourceStream(ResourceName); + Assert.NotNull(stream); + + XDocument doc = XDocument.Load(stream!); + Assert.Equal("linden_avatar", doc.Root?.Name.LocalName); + + int layerSets = doc.Descendants("layer_set").Count(); + Assert.True(layerSets >= 1, $"expected at least one , found {layerSets}"); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/BackendTests.cs b/Source/OpenSimNGC.Appearance.Baking.Tests/BackendTests.cs new file mode 100644 index 00000000000..d7b19f81506 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/BackendTests.cs @@ -0,0 +1,411 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using OpenMetaverse; +using Xunit; + +namespace OpenSimNGC.Appearance.Baking.Tests; + +/// The bytes-in/bytes-out surface: wearable text parsing, JPEG 2000 in and out (single tile), the backend end to end, and the input hash. +public class BackendTests +{ + // ---------------- wearable text ---------------- + + private static string WearableText(WearableKind kind, string name, IReadOnlyDictionary prms, IReadOnlyDictionary textures) + { + var sb = new System.Text.StringBuilder(); + sb.Append("LLWearable version 22\n").Append(name).Append('\n').Append("\n"); + sb.Append("\tpermissions 0\n\t{\n\t\tbase_mask\t7fffffff\n\t\towner_mask\t7fffffff\n\t\tgroup_mask\t00000000\n\t\teveryone_mask\t00000000\n\t\tnext_owner_mask\t00082000\n\t\tcreator_id\t11111111-1111-0000-0000-000100bba000\n\t\towner_id\t11111111-1111-0000-0000-000100bba000\n\t\tlast_owner_id\t00000000-0000-0000-0000-000000000000\n\t\tgroup_id\t00000000-0000-0000-0000-000000000000\n\t}\n"); + sb.Append("\tsale_info\t0\n\t{\n\t\tsale_type\tnot\n\t\tsale_price\t10\n\t}\n"); + sb.Append("type ").Append((int)kind).Append('\n'); + sb.Append("parameters ").Append(prms.Count).Append('\n'); + foreach (var (id, v) in prms) sb.Append(id).Append(' ').Append(v.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append('\n'); + sb.Append("textures ").Append(textures.Count).Append('\n'); + foreach (var (slot, id) in textures) sb.Append((int)slot).Append(' ').Append(id).Append('\n'); + return sb.ToString(); + } + + [Fact] + public void wearable_text_parses_type_parameters_and_textures() + { + var tex = UUID.Random(); + var text = WearableText(WearableKind.Shirt, "Blue Shirt", new Dictionary { [800] = 0.75f, [803] = 0f }, new Dictionary { [TextureSlot.UpperShirt] = tex }); + var w = WearableParser.Parse(text); + Assert.Equal(WearableKind.Shirt, w.Kind); + Assert.Equal("Blue Shirt", w.Name); + Assert.Equal(0.75f, w.Params[800]); + Assert.Equal(0f, w.Params[803]); + Assert.Equal(tex, w.Textures[TextureSlot.UpperShirt]); + Assert.Throws(() => WearableParser.Parse("not a wearable")); + Assert.Throws(() => WearableParser.Parse("LLWearable version 22\nName\n\nparameters 1\n800 x\n")); + } + + // ---------------- JPEG 2000 ---------------- + + [Fact] + public void encoded_bakes_are_a_single_tile_codestream_and_round_trip() + { + var img = CompositorTests.Flat(96, 96, 200, 40, 10, 128); + for (var i = 0; i < img.R.Length; i++) img.R[i] = (byte)(i % 96 * 2); // some structure so the encoder has work to do + var bytes = J2kCodec.Encode(img); + Assert.True(bytes.Length > 100); + Assert.Equal(0xFF, bytes[0]); Assert.Equal(0x4F, bytes[1]); // raw codestream (SOC), not a JP2 file + var siz = J2kCodec.ParseSiz(bytes); + Assert.Equal(96, siz.Xsiz); Assert.Equal(96, siz.Ysiz); + Assert.True(siz.XTsiz >= siz.Xsiz && siz.YTsiz >= siz.Ysiz, $"tile {siz.XTsiz}x{siz.YTsiz} smaller than image {siz.Xsiz}x{siz.Ysiz}"); + Assert.True(siz.SingleTile, $"{siz.TileCount} tiles"); + Assert.Equal(4, siz.Csiz); + + var back = J2kCodec.Decode(bytes); + Assert.Equal(96, back.W); Assert.Equal(96, back.H); + Assert.True(back.HasAlpha); + var mid = 48 * 96 + 48; + Assert.InRange(back.G[mid], 30, 50); + Assert.InRange(back.B[mid], 0, 20); + Assert.InRange(back.A[mid], 118, 138); + } + + [Fact] + public void a_bake_encodes_five_components_and_round_trips_the_morph_mask() + { + var img = CompositorTests.Flat(96, 96, 180, 90, 30, 200); + var mask = new byte[96 * 96]; + for (var i = 0; i < mask.Length; i++) mask[i] = (byte)(i % 96 < 48 ? 0 : 255); + var bytes = J2kCodec.EncodeBake(img, mask); + var siz = J2kCodec.ParseSiz(bytes); + Assert.Equal(5, siz.Csiz); + Assert.True(siz.SingleTile, $"{siz.TileCount} tiles"); + Assert.Equal(96, siz.Xsiz); + var back = J2kCodec.Decode(bytes); + Assert.NotNull(back.Mask); + Assert.True(back.HasAlpha); + var row = 48 * 96; + Assert.InRange(back.Mask![row + 10], 0, 8); + Assert.InRange(back.Mask![row + 80], 247, 255); + Assert.InRange(back.A[row + 10], 190, 210); + Assert.InRange(back.R[row + 10], 170, 190); + // no mask supplied: 255 everywhere, the value gatherMorphMaskAlpha starts from + var plain = J2kCodec.Decode(J2kCodec.EncodeBake(img, null)); + Assert.NotNull(plain.Mask); + Assert.All(plain.Mask!, v => Assert.InRange(v, 250, 255)); + // and the backend's output is five-component + var (req, _) = ClassicRequest(); + foreach (var r in new SkiaBakeBackend().Bake(req)) Assert.Equal(5, J2kCodec.ParseSiz(r.J2kBytes).Csiz); + } + + [Fact] + public void the_default_encoder_would_tile_a_512_bake_so_the_config_pins_one_tile() + { + var cfg = J2kCodec.EncoderConfig(512, 512); + Assert.Equal(512, cfg.Tiles.Width); + Assert.Equal(512, cfg.Tiles.Height); + Assert.False(cfg.UseFileFormat); + var siz = J2kCodec.ParseSiz(J2kCodec.Encode(CompositorTests.Flat(512, 512, 10, 20, 30))); + Assert.Equal(1, siz.TileCount); + } + + // ---------------- the backend ---------------- + + private static (BakeRequest Request, Dictionary Assets) ClassicRequest(int size = 64, float sleeve = 1f) + { + var skinTex = UUID.Random(); var hairTex = UUID.Random(); var irisTex = UUID.Random(); var shirtTex = UUID.Random(); + var assets = new Dictionary { [WearableKind.Shape] = UUID.Random(), [WearableKind.Skin] = UUID.Random(), [WearableKind.Hair] = UUID.Random(), [WearableKind.Eyes] = UUID.Random(), [WearableKind.Shirt] = UUID.Random() }; + var wearables = new List + { + new(assets[WearableKind.Shape], 0, WearableText(WearableKind.Shape, "Shape", new Dictionary { [80] = 0f, [33] = 0.5f }, new Dictionary())), + new(assets[WearableKind.Skin], 1, WearableText(WearableKind.Skin, "Skin", new Dictionary { [111] = 0.5f }, new Dictionary { [TextureSlot.HeadBodypaint] = skinTex, [TextureSlot.UpperBodypaint] = skinTex, [TextureSlot.LowerBodypaint] = skinTex })), + new(assets[WearableKind.Hair], 2, WearableText(WearableKind.Hair, "Hair", new Dictionary { [114] = 0.5f }, new Dictionary { [TextureSlot.Hair] = hairTex })), + new(assets[WearableKind.Eyes], 3, WearableText(WearableKind.Eyes, "Eyes", new Dictionary { [99] = 0f }, new Dictionary { [TextureSlot.EyesIris] = irisTex })), + new(assets[WearableKind.Shirt], 4, WearableText(WearableKind.Shirt, "Shirt", new Dictionary { [800] = sleeve, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 0f, [804] = 0f, [805] = 1f }, new Dictionary { [TextureSlot.UpperShirt] = shirtTex })), + }; + var textures = new Dictionary + { + [skinTex] = new(skinTex, J2kCodec.Encode(CompositorTests.Flat(32, 32, 200, 150, 120))), + [hairTex] = new(hairTex, J2kCodec.Encode(CompositorTests.Flat(16, 16, 255, 255, 255, 0))), + [irisTex] = new(irisTex, J2kCodec.Encode(CompositorTests.Flat(16, 16, 255, 255, 255))), + [shirtTex] = new(shirtTex, J2kCodec.Encode(CompositorTests.Flat(16, 16, 0, 0, 255))), + }; + return (new BakeRequest(wearables, new Dictionary(), textures, size), assets); + } + + [Fact] + public async Task the_backend_bakes_the_five_classic_channels_from_bytes_and_reports_fidelity() + { + var (req, _) = ClassicRequest(); + var results = await new SkiaBakeBackend().BakeAsync(req, CancellationToken.None); + Assert.Equal(new[] { BakeChannel.Head, BakeChannel.Upper, BakeChannel.Lower, BakeChannel.Eyes, BakeChannel.Hair }, results.Select(r => r.Channel).ToArray()); + foreach (var r in results) + { + var siz = J2kCodec.ParseSiz(r.J2kBytes); + Assert.Equal(64, siz.Xsiz); Assert.True(siz.SingleTile); + Assert.Equal(64, r.InputHash.Length); + Assert.Empty(r.Fidelity.Refusals); + Assert.Empty(r.Fidelity.MissingTextures); + Assert.NotEmpty(r.Fidelity.Notes); + } + var upper = J2kCodec.Decode(results.Single(r => r.Channel == BakeChannel.Upper).J2kBytes); + Assert.Contains(upper.B, b => b > 150); // the blue shirt made it onto the upper bake + Assert.Contains(results.Single(r => r.Channel == BakeChannel.Upper).Fidelity.Notes, n => n.StartsWith("upper_clothes drawn")); + + // a texture the request does not carry is reported on the channel that needs it, and only there + var missingTex = req.Textures.Keys.First(); + var trimmed = req with { Textures = req.Textures.Where(kv => kv.Key != missingTex).ToDictionary(kv => kv.Key, kv => kv.Value) }; + var results2 = new SkiaBakeBackend().Bake(trimmed); + Assert.Contains(results2, r => r.Fidelity.MissingTextures.Contains(missingTex)); + + // an unsupported wearable type lands in Refusals on every channel; corrupt text is an exception + var odd = req with { Wearables = req.Wearables.Append(new WearableInput(UUID.Random(), 99, WearableText((WearableKind)99, "Mystery", new Dictionary(), new Dictionary()))).ToList() }; + Assert.All(new SkiaBakeBackend().Bake(odd), r => Assert.Contains(r.Fidelity.Refusals, s => s.Contains("not composited"))); + var corrupt = req with { Wearables = req.Wearables.Append(new WearableInput(UUID.Random(), 4, "garbage")).ToList() }; + Assert.Throws(() => new SkiaBakeBackend().Bake(corrupt)); + } + + /// + /// narrows what is composited without changing what comes out. It is the + /// mechanism behind the ADR-004 skip: the orchestrator leaves out the channels whose inputs are unchanged, so + /// no texture of theirs is even decoded. Two properties are asserted — the results are exactly the requested + /// channels intersected with what the outfit needs, and each one is byte-for-byte the bake a full run makes. + /// + [Fact] + public void requesting_a_subset_of_channels_bakes_only_those_and_changes_no_bytes() + { + var (req, _) = ClassicRequest(); + var full = new SkiaBakeBackend().Bake(req); + + var subset = new[] { BakeChannel.Upper, BakeChannel.Eyes }; + var partial = new SkiaBakeBackend().Bake(req with { Channels = subset }); + Assert.Equal(subset, partial.Select(r => r.Channel).ToArray()); + foreach (var r in partial) + { + var same = full.Single(f => f.Channel == r.Channel); + Assert.Equal(same.J2kBytes, r.J2kBytes); + Assert.Equal(same.InputHash, r.InputHash); + Assert.Equal(same.Fidelity.Notes, r.Fidelity.Notes); + } + + // naming a channel the outfit does not feed does not conjure it + Assert.Empty(new SkiaBakeBackend().Bake(req with { Channels = new[] { BakeChannel.Skirt } })); + // an empty set bakes nothing; null (the default) bakes everything + Assert.Empty(new SkiaBakeBackend().Bake(req with { Channels = Array.Empty() })); + Assert.Equal(full.Count, new SkiaBakeBackend().Bake(req with { Channels = null }).Count); + } + + [Fact] + public void skirt_and_extra_channels_appear_only_when_worn_or_painted() + { + var (req, _) = ClassicRequest(); + var parsed = req.Wearables.Select(w => WearableParser.Parse(w.RawText)).ToList(); + Assert.Equal(5, SkiaBakeBackend.ChannelsFor(parsed).Count); + parsed.Add(new ParsedWearable(WearableKind.Skirt, "Skirt", new Dictionary(), new Dictionary())); + parsed.Add(new ParsedWearable(WearableKind.Universal, "U", new Dictionary(), new Dictionary { [TextureSlot.Aux2Tattoo] = UUID.Random(), [TextureSlot.LeftArmTattoo] = BakeConstants.DefaultAvatarTexture })); + var chs = SkiaBakeBackend.ChannelsFor(parsed); + Assert.Contains(BakeChannel.Skirt, chs); + Assert.Contains(BakeChannel.Aux2, chs); + Assert.DoesNotContain(BakeChannel.LeftArm, chs); + } + + // ---------------- the hash ---------------- + + [Fact] + public void bake_hash_is_stable_across_ordering_and_changes_with_every_input() + { + var (req, assets) = ClassicRequest(); + var h = BakeHash.Compute(BakeChannel.Upper, req); + Assert.Matches("^[0-9a-f]{64}$", h); + + // ordering: reversed wearable list, reversed texture dictionary, reversed visual params + var reordered = new BakeRequest( + req.Wearables.Reverse().ToList(), + new Dictionary { [33] = 0.5f, [80] = 0f }, + req.Textures.Reverse().ToDictionary(kv => kv.Key, kv => kv.Value), + req.BakeSize); + var baseline = req with { VisualParams = new Dictionary { [80] = 0f, [33] = 0.5f } }; + Assert.Equal(BakeHash.Compute(BakeChannel.Upper, baseline), BakeHash.Compute(BakeChannel.Upper, reordered)); + + // channel and size + Assert.NotEqual(h, BakeHash.Compute(BakeChannel.Lower, req)); + Assert.NotEqual(h, BakeHash.Compute(BakeChannel.Upper, req with { BakeSize = 128 })); + + // a wearable that feeds the channel: new asset id, or a changed parameter it stores + var shirt = req.Wearables.Single(w => w.WearableType == 4); + Assert.NotEqual(h, BakeHash.Compute(BakeChannel.Upper, req with { Wearables = req.Wearables.Select(w => w == shirt ? w with { AssetId = UUID.Random() } : w).ToList() })); + var (req2, _) = ClassicRequest(sleeve: 0.25f); + var sameIds = req2 with { Wearables = req2.Wearables.Select((w, i) => w with { AssetId = req.Wearables[i].AssetId }).ToList(), Textures = req.Textures }; + // (textures differ by id between the two requests; align them so only the sleeve differs) + var shirtText = sameIds.Wearables.Single(w => w.WearableType == 4); + var shirtTex = WearableParser.Parse(shirt.RawText).Textures[TextureSlot.UpperShirt]; + var realigned = sameIds with { Wearables = sameIds.Wearables.Select(w => w == shirtText + ? w with { RawText = WearableText(WearableKind.Shirt, "Shirt", new Dictionary { [800] = 0.25f, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 0f, [804] = 0f, [805] = 1f }, new Dictionary { [TextureSlot.UpperShirt] = shirtTex }) } + : req.Wearables[sameIds.Wearables.ToList().IndexOf(w)]).ToList() }; + Assert.NotEqual(h, BakeHash.Compute(BakeChannel.Upper, realigned)); + + // a texture id the channel draws + var retextured = req with { Wearables = req.Wearables.Select(w => w == shirt ? w with { RawText = WearableText(WearableKind.Shirt, "Shirt", WearableParser.Parse(shirt.RawText).Params, new Dictionary { [TextureSlot.UpperShirt] = UUID.Random() }) } : w).ToList() }; + Assert.NotEqual(h, BakeHash.Compute(BakeChannel.Upper, retextured)); + + // a visual param the channel's layers read (skin colour 111 feeds the upper body's global colour) + Assert.NotEqual(BakeHash.Compute(BakeChannel.Upper, req with { VisualParams = new Dictionary { [111] = 0.1f } }), + BakeHash.Compute(BakeChannel.Upper, req with { VisualParams = new Dictionary { [111] = 0.9f } })); + + // something that does not feed the upper body (pants asset id) leaves it alone + var pants = new WearableInput(UUID.Random(), 5, WearableText(WearableKind.Pants, "Pants", new Dictionary { [615] = 1f }, new Dictionary())); + Assert.Equal(h, BakeHash.Compute(BakeChannel.Upper, req with { Wearables = req.Wearables.Append(pants).ToList() })); + Assert.NotEqual(BakeHash.Compute(BakeChannel.Lower, req), BakeHash.Compute(BakeChannel.Lower, req with { Wearables = req.Wearables.Append(pants).ToList() })); + _ = assets; + } + + // ---------------- a worn wearable with no texture asset (S1c, Docs/MORPH-MASK-PASS.md 2.4) ---------------- + + /// + /// The rule: a morph-mask layer with a local_texture contributes the mask of the top worn wearable of its + /// type, and a wearable counts as worn whether or not it carries a texture asset + /// (LLTexLayerTemplate::updateWearableCache counts LLWearable objects, lltexlayer.cpp:1615-1638; its + /// getLayer only needs a local texture object, :1639-1656). The layer draws nothing in the colour pass + /// without an image, but its parameter alphas still make a morph mask, and their values come from the + /// avatar parameters rather than from the absent asset. + /// + [Fact] + public void a_worn_wearable_with_no_texture_asset_still_contributes_its_morph_mask() + { + var jacketTex = UUID.Random(); + var textures = new Dictionary + { + [jacketTex] = new TextureInput(jacketTex, J2kCodec.Encode(CompositorTests.Flat(32, 32, 40, 40, 60))), + }; + // the shirt-owned drivers of upper_clothes's four param alphas (600/601/602/778): short sleeves, so the + // mask has a zero region. They live on the avatar, not on the (absent) shirt asset. + var visual = new Dictionary { [800] = 0f, [801] = 1f, [802] = 1f, [781] = 1f }; + var shape = new WearableInput(UUID.Random(), (int)WearableKind.Shape, WearableText(WearableKind.Shape, "S", new Dictionary { [80] = 1f }, new Dictionary())); + var jacket = new WearableInput(UUID.Random(), (int)WearableKind.Jacket, WearableText(WearableKind.Jacket, "J", new Dictionary(), new Dictionary { [TextureSlot.UpperJacket] = jacketTex })); + + byte[] UpperMask(params WearableInput[] worn) + { + var r = new SkiaBakeBackend().Bake(new BakeRequest(worn, visual, textures, 128)); + return r.Single(x => x.Channel == BakeChannel.Upper).Fidelity is not null + ? J2kCodec.Decode(r.Single(x => x.Channel == BakeChannel.Upper).J2kBytes).Mask! + : throw new InvalidOperationException(); + } + + // jacket alone, no shirt slot at all: upper_clothes has no instance and the morph mask stays flat at 255 + // (the J2C round-trip renders a flat 255 plane back as a flat 254, as the golden tables show) + var withoutShirtSlot = UpperMask(shape, jacket); + Assert.True(withoutShirtSlot.Max() - withoutShirtSlot.Min() <= 2, "no shirt slot: the morph mask must be flat"); + Assert.True(withoutShirtSlot.Min() >= 250, $"no shirt slot: the flat mask must be ~255, was {withoutShirtSlot.Min()}"); + + // the same outfit, plus a worn Shirt slot with no asset behind it: the mask is now real + var assetlessShirt = new WearableInput(UUID.Zero, (int)WearableKind.Shirt, ""); + var withShirtSlot = UpperMask(shape, jacket, assetlessShirt); + Assert.True(withShirtSlot.Any(v => v < 250), "an assetless but worn shirt must still produce a morph mask"); + Assert.True(withShirtSlot.Any(v => v > 250), "the mask must be a mask, not zero everywhere"); + + // and it is the shirt layer that did it, not the jacket: upper_jacket is not in + Assert.DoesNotContain("upper_jacket", AvatarLad.Embedded.MorphMaskLayers["upper_body"]); + Assert.Contains("upper_clothes", AvatarLad.Embedded.MorphMaskLayers["upper_body"]); + } + + /// + /// The other half of the same rule: LLTexLayerTemplate::gatherAlphaMasks uses getLayer(num_wearables - 1) + /// only — "For rendering morph masks, we only want to use the top wearable" (lltexlayer.cpp:1710-1719) — + /// unlike render(), which loops over every instance. Two shirts must give the second one's mask, not the + /// product of both. + /// + [Fact] + public void the_morph_mask_of_a_template_layer_comes_from_the_top_wearable_only() + { + var shape = new WearableInput(UUID.Random(), (int)WearableKind.Shape, WearableText(WearableKind.Shape, "S", new Dictionary { [80] = 1f }, new Dictionary())); + WearableInput Shirt(float sleeve) => new(UUID.Random(), (int)WearableKind.Shirt, + WearableText(WearableKind.Shirt, "shirt", new Dictionary { [800] = sleeve, [801] = 1f, [802] = 1f, [781] = 1f }, new Dictionary())); + + byte[] Mask(params WearableInput[] worn) + { + var r = new SkiaBakeBackend().Bake(new BakeRequest(worn, new Dictionary(), new Dictionary(), 128)); + return J2kCodec.Decode(r.Single(x => x.Channel == BakeChannel.Upper).J2kBytes).Mask!; + } + + var shortOnly = Mask(shape, Shirt(0f)); + var longOnly = Mask(shape, Shirt(1f)); + var shortThenLong = Mask(shape, Shirt(0f), Shirt(1f)); + + long Diff(byte[] a, byte[] b) { long d = 0; for (var i = 0; i < a.Length; i++) d += Math.Abs(a[i] - b[i]); return d / a.Length; } + Assert.True(Diff(shortOnly, longOnly) > 4, "the two sleeve lengths must give different masks for this test to mean anything"); + Assert.True(Diff(shortThenLong, longOnly) <= 2, $"two shirts must give the top (last) shirt's mask; differs by {Diff(shortThenLong, longOnly)}"); + Assert.True(Diff(shortThenLong, shortOnly) > 4, "two shirts must not give the first shirt's mask"); + } + + // ---------------- a channel where nothing was drawn (S1e) ---------------- + + private static string TrulyFixtures([CallerFilePath] string here = "") + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "Golden", "truly-stock", "fixtures")); + + /// + /// BakeResult.NothingDrawn is set only when every colour layer of the channel was skipped. An assetless + /// Skirt slot is exactly that case: skirt_fabric has no texture, skirt_fabric_alpha is a mask layer and + /// skirt_tattoo needs a Universal, so nothing reaches the canvas — and because the set's alpha starts + /// opaque, what encodes is a solid image, not a blank one. S1d found this painting a dark skirt onto face 19. + /// + [Fact] + public void a_channel_whose_every_layer_was_skipped_reports_nothing_drawn() + { + var shape = new WearableInput(UUID.Random(), (int)WearableKind.Shape, WearableText(WearableKind.Shape, "S", new Dictionary { [80] = 0f }, new Dictionary())); + var skirt = new WearableInput(UUID.Zero, (int)WearableKind.Skirt, ""); // worn, no asset + var results = new SkiaBakeBackend().Bake(new BakeRequest(new[] { shape, skirt }, new Dictionary(), new Dictionary(), 64)); + + var s = results.Single(r => r.Channel == BakeChannel.Skirt); + Assert.True(s.NothingDrawn, "an all-skipped skirt channel must report NothingDrawn: " + string.Join(" | ", s.Fidelity.Notes)); + Assert.All(s.Fidelity.Notes, n => Assert.Contains("skipped", n)); + // it still encodes a full image - that is the hazard: an undrawn channel is not an empty bake, and how + // opaque it comes out depends on which mask layers the outfit skipped (on a real outfit S1d measured 96.5% + // opaque near-black). The orchestrator must decide on this flag, never on the pixels. + Assert.NotEmpty(s.J2kBytes); + Assert.Equal(64, J2kCodec.Decode(s.J2kBytes).W); + // and it is per channel, not per outfit: the head of the same outfit did draw + Assert.False(results.Single(r => r.Channel == BakeChannel.Head).NothingDrawn); + } + + /// + /// Drawn-but-transparent is NOT undrawn. Truly Bazar's hair wearable carries a 4x4 fully transparent + /// texture: the base layer draws it, the bake is legitimately all-transparent, and it must still be stored. + /// Asserted on the real truly-stock fixtures, and on a synthetic equivalent so the rule is covered when the + /// fixtures are not fetched. + /// + [Fact] + public void a_channel_that_drew_a_fully_transparent_texture_is_not_nothing_drawn() + { + // synthetic: a bald hair — a hair wearable whose texture is entirely transparent + var shape = new WearableInput(UUID.Random(), (int)WearableKind.Shape, WearableText(WearableKind.Shape, "S", new Dictionary { [80] = 0f }, new Dictionary())); + var bald = UUID.Random(); + var hair = new WearableInput(UUID.Random(), (int)WearableKind.Hair, WearableText(WearableKind.Hair, "bald", new Dictionary { [114] = 0.5f }, new Dictionary { [TextureSlot.Hair] = bald })); + var textures = new Dictionary { [bald] = new TextureInput(bald, J2kCodec.Encode(CompositorTests.Flat(4, 4, 255, 255, 255, 0))) }; + var synthetic = new SkiaBakeBackend().Bake(new BakeRequest(new[] { shape, hair }, new Dictionary(), textures, 64)) + .Single(r => r.Channel == BakeChannel.Hair); + Assert.False(synthetic.NothingDrawn, "a drawn but transparent layer has drawn: " + string.Join(" | ", synthetic.Fidelity.Notes)); + Assert.Contains(synthetic.Fidelity.Notes, n => n.StartsWith("base drawn")); + var img = J2kCodec.Decode(synthetic.J2kBytes); + Assert.True(img.A.All(a => a <= 2), "the bald hair bake is legitimately all-transparent and must still be stored"); + + // the real thing: Truly Bazar's stock outfit + var fx = TrulyFixtures(); + if (!File.Exists(Path.Combine(fx, "avatar.json"))) { Console.WriteLine("SKIPPED (truly-stock fixtures not fetched): synthetic case asserted above"); return; } + using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(fx, "avatar.json"))); + var worn = new List(); + foreach (var w in doc.RootElement.GetProperty("wearables").EnumerateArray()) + { + var id = w.GetProperty("assetId").GetString()!; + var type = w.GetProperty("type").GetInt32(); + if (id == "00000000-0000-0000-0000-000000000000") { worn.Add(new WearableInput(UUID.Zero, type, "")); continue; } + var f = Directory.GetFiles(fx, id + ".*").First(x => !x.EndsWith(".j2c")); + worn.Add(new WearableInput(new UUID(id), type, File.ReadAllText(f))); + } + var tex = new Dictionary(); + foreach (var w in worn.Where(x => x.RawText.Length > 0)) + foreach (var (_, id) in WearableParser.Parse(w.RawText).Textures) + { + var f = Path.Combine(fx, id + ".j2c"); + if (File.Exists(f) && !tex.ContainsKey(id)) tex[id] = new TextureInput(id, File.ReadAllBytes(f)); + } + var real = new SkiaBakeBackend().Bake(new BakeRequest(worn, new Dictionary(), tex, 128)); + var realHair = real.Single(r => r.Channel == BakeChannel.Hair); + Assert.False(realHair.NothingDrawn, "Truly's bald hair drew: " + string.Join(" | ", realHair.Fidelity.Notes)); + // and no channel of a normal outfit reports nothing drawn + Assert.All(real, r => Assert.False(r.NothingDrawn, $"{r.Channel}: " + string.Join(" | ", r.Fidelity.Notes))); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/CompositorTests.cs b/Source/OpenSimNGC.Appearance.Baking.Tests/CompositorTests.cs new file mode 100644 index 00000000000..225beb003db --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/CompositorTests.cs @@ -0,0 +1,356 @@ +using OpenMetaverse; +using Xunit; + +namespace OpenSimNGC.Appearance.Baking.Tests; + +/// +/// The compositor against the embedded avatar_lad.xml and mask files, with synthetic wearables whose textures +/// are flat colours, so every assertion is about semantics: scaling instead of tiling, layer order, +/// parameter-driven masks, tints, alpha wearables masking the body, and the fidelity gate. +/// Ported from the web-viewer gateway's CompositorTests (sessions 11–13) in S0b. +/// +public class CompositorTests +{ + private static AvatarLad Lad => AvatarLad.Embedded; + private static TexLayerCompositor NewCompositor() => new(Lad, ResourceImages.Embedded); + + internal static RgbaPlanes Flat(int w, int h, byte r, byte g, byte b, byte a = 255) + { + var p = new RgbaPlanes(w, h, hasAlpha: a != 255); + Array.Fill(p.R, r); Array.Fill(p.G, g); Array.Fill(p.B, b); Array.Fill(p.A, a); + return p; + } + + private static WornWearable Wear(WearableKind kind, Dictionary? prms = null, params (TextureSlot Slot, RgbaPlanes Tex)[] textures) + { + var ids = textures.ToDictionary(t => t.Slot, _ => UUID.Random()); + return new WornWearable { Kind = kind, Label = kind.ToString(), Params = prms ?? new(), TextureIds = ids, Textures = textures.ToDictionary(t => t.Slot, t => t.Tex) }; + } + + /// Shape, skin (flat tan, no makeup), hair, eyes: the minimum a system avatar needs. + private static List BaseOutfit(bool male = false) => new() + { + Wear(WearableKind.Shape, new() { [80] = male ? 1f : 0f }), + Wear(WearableKind.Skin, new() { [111] = 0.5f }, (TextureSlot.HeadBodypaint, Flat(64, 64, 200, 150, 120)), (TextureSlot.UpperBodypaint, Flat(64, 64, 200, 150, 120)), (TextureSlot.LowerBodypaint, Flat(64, 64, 200, 150, 120))), + Wear(WearableKind.Hair, new() { [114] = 0.5f }, (TextureSlot.Hair, Flat(32, 32, 255, 255, 255, 0))), + Wear(WearableKind.Eyes, new() { [99] = 0f }, (TextureSlot.EyesIris, Flat(32, 32, 255, 255, 255))), + }; + + private static (byte R, byte G, byte B, byte A) Px(RgbaPlanes img, int x, int y) { var i = y * img.W + x; return (img.R[i], img.G[i], img.B[i], img.A[i]); } + + /// A pixel of the given mask file (at bake resolution) with the requested grey value, searched from the centre outwards. + private static (int X, int Y) FindMaskPixel(string file, int size, Func want, params string[] alsoWhite) + { + var res = ResourceImages.Embedded; + var r = Raster.Resample(res.Mask(file)!, size, size); + var white = alsoWhite.Select(f => Raster.Resample(res.Mask(f)!, size, size)).ToList(); + for (var y = 8; y < size - 8; y += 4) + for (var x = 8; x < size - 8; x += 4) + if (want(r.Data[y * size + x]) && want(r.Data[y * size + x + 3]) && want(r.Data[(y + 3) * size + x]) && white.All(w => w.Data[y * size + x] > 250 && w.Data[y * size + x + 3] > 250 && w.Data[(y + 3) * size + x] > 250)) return (x, y); + throw new InvalidOperationException($"no pixel in {file} matches"); + } + + [Fact] + public void every_resource_the_bake_layers_name_is_embedded() + { + // avatar_lad.xml also names head_wrinkles_highlights_alpha.tga, but only from a bump-pass layer, which is never baked. + var c = NewCompositor(); + var files = Enum.GetValues().SelectMany(c.ResourceFilesOf).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + var missing = files.Where(f => !ResourceImages.Embedded.Exists(f)).ToList(); + Assert.True(files.Count > 40, $"expected the layer sets to name dozens of files, got {files.Count}"); + Assert.Empty(missing); + Assert.False(ResourceImages.Embedded.Exists("head_wrinkles_highlights_alpha.tga")); + } + + [Fact] + public void avatar_lad_loads_the_bake_layer_sets_and_driver_parameters() + { + var lad = Lad; + Assert.True(lad.LayerSets.ContainsKey("head") && lad.LayerSets.ContainsKey("upper_body") && lad.LayerSets.ContainsKey("lower_body") && lad.LayerSets.ContainsKey("eyes") && lad.LayerSets.ContainsKey("hair") && lad.LayerSets.ContainsKey("skirt")); + Assert.Equal(new[] { "base", "nipples", "shadow", "highlight", "upper_bodypaint" }, lad.LayerSets["upper_body"].Layers.Where(l => !l.Bump).Take(5).Select(l => l.Name).ToArray()); + var sleeve = lad.Params[800]; // the shirt's stored "Sleeve Length" drives the layer's mask parameter 600 + Assert.Contains(sleeve.Driven, d => d.Id == 600); + Assert.Equal("shirt_sleeve_alpha.tga", lad.Params[600].Alpha!.TgaFile); + Assert.False(lad.Params[600].Alpha!.MultiplyBlend); + Assert.True(lad.Params[601].Alpha!.MultiplyBlend); + Assert.Equal(ParamSex.Male, lad.Params[1005].Sex); // sideburns: men only + Assert.Equal(new[] { 111, 110, 108 }, lad.GlobalColors["skin_color"]); + Assert.Equal(ColorOp.Blend, lad.Params[110].Color!.Op); + } + + [Fact] + public void a_small_texture_is_scaled_over_the_whole_bake_not_tiled() + { + // Left half red, right half blue, 64 px wide: on a faithful bake the halves land at 25% and 75% of the width. + var tex = new RgbaPlanes(64, 64, hasAlpha: false); + for (var y = 0; y < 64; y++) for (var x = 0; x < 64; x++) { var i = y * 64 + x; if (x < 32) tex.R[i] = 255; else tex.B[i] = 255; } + var outfit = BaseOutfit(); + outfit[1] = Wear(WearableKind.Skin, new() { [111] = 0.5f }, (TextureSlot.UpperBodypaint, tex), (TextureSlot.HeadBodypaint, tex), (TextureSlot.LowerBodypaint, tex)); + var bake = NewCompositor().Bake(BakeChannel.Upper, outfit, 256).Image; + var left = Px(bake, 64, 128); + var right = Px(bake, 192, 128); + Assert.True(left.R > 200 && left.B < 40, $"left quarter should be red, got {left}"); + Assert.True(right.B > 200 && right.R < 40, $"right quarter should be blue, got {right}"); + var farRight = Px(bake, 250, 250); + Assert.True(farRight.B > 200, $"a tiled bake would repeat red here, got {farRight}"); + } + + [Fact] + public void the_shirt_covers_the_arms_only_when_its_sleeve_length_says_so() + { + const int size = 256; + var shirt = (TextureSlot.UpperShirt, Flat(64, 64, 0, 0, 255)); + var others = new[] { "shirt_bottom_alpha.tga", "shirt_collar_alpha.tga", "shirt_collar_back_alpha.tga" }; // the shirt's multiply masks must be open there + var torso = FindMaskPixel("shirt_sleeve_alpha.tga", size, v => v == 255, others); // always covered by the sleeve mask + var wrist = FindMaskPixel("shirt_sleeve_alpha.tga", size, v => v is > 60 and < 120, others); // covered only by long sleeves (below ~38 is past the wrist even at full length) + var c = NewCompositor(); + + var longSleeves = BaseOutfit(); longSleeves.Add(Wear(WearableKind.Shirt, new() { [800] = 1f, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 0f, [804] = 0f, [805] = 1f }, shirt)); + var bakeLong = c.Bake(BakeChannel.Upper, longSleeves, size).Image; + Assert.True(Px(bakeLong, torso.X, torso.Y).B > 200, "torso is shirt-blue with long sleeves"); + Assert.True(Px(bakeLong, wrist.X, wrist.Y).B > 200, $"wrist is shirt-blue with long sleeves, got {Px(bakeLong, wrist.X, wrist.Y)}"); + + var shortSleeves = BaseOutfit(); shortSleeves.Add(Wear(WearableKind.Shirt, new() { [800] = 0f, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 0f, [804] = 0f, [805] = 1f }, shirt)); + var bakeShort = c.Bake(BakeChannel.Upper, shortSleeves, size).Image; + Assert.True(Px(bakeShort, torso.X, torso.Y).B > 200, "torso is still shirt-blue with short sleeves"); + var w = Px(bakeShort, wrist.X, wrist.Y); + Assert.True(w.R > 150 && w.B < 150, $"wrist shows skin with short sleeves, got {w}"); + Assert.Equal(255, Px(bakeShort, wrist.X, wrist.Y).A); // clothing masks never make the body transparent + } + + [Fact] + public void clothing_is_tinted_by_its_colour_parameters() + { + var outfit = BaseOutfit(); + outfit.Add(Wear(WearableKind.Shirt, new() { [800] = 1f, [803] = 0f, [804] = 1f, [805] = 0f }, (TextureSlot.UpperShirt, Flat(16, 16, 255, 255, 255)))); + var bake = NewCompositor().Bake(BakeChannel.Upper, outfit, 128).Image; + var torso = FindMaskPixel("shirt_sleeve_alpha.tga", 128, v => v == 255); + var p = Px(bake, torso.X, torso.Y); + Assert.True(p.G > 200 && p.R < 40 && p.B < 40, $"white shirt tinted green, got {p}"); + } + + [Fact] + public void layers_stack_in_avatar_lad_order_undershirt_below_shirt_below_jacket() + { + var outfit = BaseOutfit(); + outfit.Add(Wear(WearableKind.Undershirt, new() { [603] = 1f, [604] = 1f, [605] = 1f, [779] = 1f, [821] = 1f, [822] = 0f, [823] = 0f }, (TextureSlot.UpperUndershirt, Flat(16, 16, 255, 255, 255)))); + outfit.Add(Wear(WearableKind.Shirt, new() { [800] = 1f, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 0f, [804] = 1f, [805] = 0f }, (TextureSlot.UpperShirt, Flat(16, 16, 255, 255, 255)))); + var torso = FindMaskPixel("shirt_sleeve_alpha.tga", 128, v => v == 255); + var c = NewCompositor(); + var result = c.Bake(BakeChannel.Upper, outfit, 128); + Assert.True(Px(result.Image, torso.X, torso.Y).G > 200, "the shirt (green) is above the undershirt (red)"); + var drawn = result.Layers.Where(l => l.Status == "drawn").Select(l => l.Layer).ToList(); + Assert.True(drawn.IndexOf("upper_undershirt") < drawn.IndexOf("upper_clothes"), string.Join(",", drawn)); + Assert.Contains(result.Layers, l => l.Layer == "upper_jacket" && l.Status == "skipped" && l.Detail.Contains("no Jacket worn")); + } + + [Fact] + public void an_alpha_wearable_masks_the_body_and_nothing_else_touches_the_alpha() + { + var alphaTex = new RgbaPlanes(64, 64, hasAlpha: true); + Array.Fill(alphaTex.R, (byte)255); Array.Fill(alphaTex.G, (byte)255); Array.Fill(alphaTex.B, (byte)255); + for (var i = 0; i < 64 * 64; i++) alphaTex.A[i] = (byte)(i % 64 < 32 ? 0 : 255); // hide the left half of the upper body + var outfit = BaseOutfit(); + outfit.Add(Wear(WearableKind.Shirt, new() { [800] = 1f }, (TextureSlot.UpperShirt, Flat(16, 16, 0, 0, 255)))); + var without = NewCompositor().Bake(BakeChannel.Upper, outfit, 128).Image; + Assert.All(without.A, a => Assert.Equal(255, a)); + outfit.Add(Wear(WearableKind.Alpha, null, (TextureSlot.UpperAlpha, alphaTex))); + var with = NewCompositor().Bake(BakeChannel.Upper, outfit, 128).Image; + Assert.Equal(0, Px(with, 20, 64).A); + Assert.Equal(255, Px(with, 100, 64).A); + Assert.True(Px(with, 20, 64).B > 200 || Px(with, 20, 64).R > 100, "the colour channels are left intact under the mask"); + } + + [Fact] + public void the_iris_takes_the_eye_colour_and_the_head_bake_carries_the_eyelash_mask() + { + var c = NewCompositor(); + var eyes = c.Bake(BakeChannel.Eyes, BaseOutfit(), 128).Image; + var centre = Px(eyes, 64, 64); + Assert.True(centre.R < 120 && centre.G < 100 && centre.B < 60, $"a white iris with eye colour 0 is dark brown, got {centre}"); + Assert.Equal(255, centre.A); + var head = c.Bake(BakeChannel.Head, BaseOutfit(), 256).Image; + Assert.Contains(head.A, a => a == 0); // head_alpha.tga (eyelashes) is a visibility mask + Assert.Contains(head.A, a => a == 255); + var hair = c.Bake(BakeChannel.Hair, BaseOutfit(), 64).Image; + Assert.All(hair.A, a => Assert.Equal(0, a)); // a fully transparent hair texture gives a transparent hair bake + } + + [Fact] + public void makeup_parameters_on_the_skin_paint_over_the_skin_texture() + { + // Makeup layers sit below head_bodypaint in avatar_lad.xml: an opaque skin texture covers them (as in the viewers), a transparent one shows them. + var covered = BaseOutfit(); + covered[1] = Wear(WearableKind.Skin, new() { [111] = 0.5f, [700] = 0.15f, [701] = 1f }, (TextureSlot.HeadBodypaint, Flat(64, 64, 200, 150, 120))); + var lips = FindMaskPixel("lipstick_alpha.tga", 256, v => v > 200); + var plainOpaque = NewCompositor().Bake(BakeChannel.Head, BaseOutfit(), 256).Image; + Assert.Equal(Px(plainOpaque, lips.X, lips.Y), Px(NewCompositor().Bake(BakeChannel.Head, covered, 256).Image, lips.X, lips.Y)); + var bare = BaseOutfit(); + bare[1] = Wear(WearableKind.Skin, new() { [111] = 0.5f }, (TextureSlot.HeadBodypaint, Flat(64, 64, 200, 150, 120, 0))); + var plain = NewCompositor().Bake(BakeChannel.Head, bare, 256).Image; + var outfit = BaseOutfit(); + outfit[1] = Wear(WearableKind.Skin, new() { [111] = 0.5f, [700] = 0.15f, [701] = 1f }, (TextureSlot.HeadBodypaint, Flat(64, 64, 200, 150, 120, 0))); + var result = NewCompositor().Bake(BakeChannel.Head, outfit, 256); + Assert.NotEqual(Px(plain, lips.X, lips.Y), Px(result.Image, lips.X, lips.Y)); + Assert.Contains(result.Layers, l => l.Layer == "lipstick" && l.Status == "drawn"); + Assert.Contains(result.Layers, l => l.Layer == "facialhair" && l.Detail.Contains("skip")); // male-only masks fall back to their defaults on a female shape + } + + [Fact] + public void the_fidelity_gate_names_what_it_cannot_bake_and_passes_a_classic_outfit() + { + var c = NewCompositor(); + var bakes = new[] { BakeChannel.Head, BakeChannel.Upper, BakeChannel.Lower, BakeChannel.Eyes, BakeChannel.Hair }; + var classic = new List + { + new(WearableKind.Shape, "Shape", new Dictionary()), + new(WearableKind.Skin, "Skin", new Dictionary { [TextureSlot.HeadBodypaint] = UUID.Random(), [TextureSlot.UpperBodypaint] = UUID.Random(), [TextureSlot.LowerBodypaint] = UUID.Random() }), + new(WearableKind.Hair, "Hair", new Dictionary { [TextureSlot.Hair] = UUID.Random() }), + new(WearableKind.Eyes, "Eyes", new Dictionary { [TextureSlot.EyesIris] = UUID.Random() }), + new(WearableKind.Shirt, "Shirt", new Dictionary { [TextureSlot.UpperShirt] = UUID.Random() }), + new(WearableKind.Tattoo, "Tattoo", new Dictionary { [TextureSlot.UpperTattoo] = UUID.Random() }), + new(WearableKind.Alpha, "Alpha", new Dictionary { [TextureSlot.LowerAlpha] = UUID.Random() }), + new(WearableKind.Physics, "Physics", new Dictionary()), + }; + Assert.Empty(FidelityCheck.Check(classic, c, bakes)); + var only = Array.Empty(); + + // a universal painting a Bakes-on-Mesh extra slot is fine once that extra bake is being made, and refused otherwise + var bom = classic.Append(new FidelityCheck.WornSummary(WearableKind.Universal, "Universal 1234", new Dictionary { [TextureSlot.LeftArmTattoo] = UUID.Random() })).ToList(); + var reasons = FidelityCheck.Check(bom, c, bakes).Except(only).ToList(); + Assert.Single(reasons); + Assert.Contains("LeftArmTattoo", reasons[0]); + Assert.Empty(FidelityCheck.Check(bom, c, bakes.Append(BakeChannel.LeftArm)).Except(only)); + + var universalClassicOnly = classic.Append(new FidelityCheck.WornSummary(WearableKind.Universal, "Universal", new Dictionary { [TextureSlot.UpperUniversalTattoo] = UUID.Random() })).ToList(); + Assert.Empty(FidelityCheck.Check(universalClassicOnly, c, bakes).Except(only)); + + var unknown = classic.Append(new FidelityCheck.WornSummary((WearableKind)99, "Mystery", new Dictionary())).ToList(); + Assert.Contains(FidelityCheck.Check(unknown, c, bakes), r => r.Contains("not composited")); + + var twoShirts = classic.Append(new FidelityCheck.WornSummary(WearableKind.Shirt, "Shirt 2", new Dictionary())).ToList(); + Assert.Empty(FidelityCheck.Check(twoShirts, c, bakes).Except(only)); // multi-wearables are layered + var twoSkins = classic.Append(new FidelityCheck.WornSummary(WearableKind.Skin, "Skin 2", new Dictionary())).ToList(); + Assert.Contains(FidelityCheck.Check(twoSkins, c, bakes), r => r.Contains("2 Skin")); + } + + [Fact] + public void the_morph_mask_follows_gatherMorphMaskAlpha() + { + // Docs/MORPH-MASK-PASS.md: 255 everywhere, times the mask of each worn instance of the set's morph-mask layers + // (head: facialhair per hair; upper_body: upper_clothes per shirt; lower_body: lower_pants per pants). + var lad = Lad; + Assert.Equal(new[] { "facialhair" }, lad.MorphMaskLayers["head"].ToArray()); + Assert.Contains("upper_clothes", lad.MorphMaskLayers["upper_body"]); + Assert.Contains("lower_pants", lad.MorphMaskLayers["lower_body"]); + Assert.False(lad.MorphMaskLayers.ContainsKey("eyes")); + var c = NewCompositor(); + + // female shape: every facialhair parameter is male-only and skip_if_zero, so the mask is 0 and the head's morph mask is 0 + var head = c.Bake(BakeChannel.Head, BaseOutfit(male: false), 64); + Assert.All(head.MorphMask, v => Assert.Equal(0, v)); + Assert.Contains(head.Layers, l => l.Layer == "facialhair" && l.Status == "morph"); + // no morph-mask layers at all: 255 + Assert.All(c.Bake(BakeChannel.Eyes, BaseOutfit(), 64).MorphMask, v => Assert.Equal(255, v)); + Assert.All(c.Bake(BakeChannel.Hair, BaseOutfit(), 64).MorphMask, v => Assert.Equal(255, v)); + // upper body with no shirt worn: the upper_clothes layer has no instance, mask stays 255 + Assert.All(c.Bake(BakeChannel.Upper, BaseOutfit(), 64).MorphMask, v => Assert.Equal(255, v)); + // with a short-sleeved shirt: the morph mask is the shirt's mask (torso 255, wrist 0) + var others = new[] { "shirt_bottom_alpha.tga", "shirt_collar_alpha.tga", "shirt_collar_back_alpha.tga" }; + var torso = FindMaskPixel("shirt_sleeve_alpha.tga", 256, v => v == 255, others); + var wrist = FindMaskPixel("shirt_sleeve_alpha.tga", 256, v => v is > 60 and < 120, others); + var outfit = BaseOutfit(); + outfit.Add(Wear(WearableKind.Shirt, new() { [800] = 0f, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 0f, [804] = 0f, [805] = 1f }, (TextureSlot.UpperShirt, Flat(16, 16, 0, 0, 255)))); + var upper = c.Bake(BakeChannel.Upper, outfit, 256); + Assert.Equal(255, upper.MorphMask[torso.Y * 256 + torso.X]); + Assert.Equal(0, upper.MorphMask[wrist.Y * 256 + wrist.X]); + Assert.Contains(upper.Layers, l => l.Layer == "upper_clothes" && l.Status == "morph"); + } + + [Fact] + public void a_no_texture_layer_renders_once_with_the_last_worn_wearables_parameters() + { + // lltexlayer.cpp:64 isUserSettable() is mLocalTexture != -1; :290-297 only such layers become LLTexLayerTemplate (rendered + // once per worn wearable, :1659-1689). facialhair has no local texture, so it is a plain LLTexLayer: rendered ONCE, from the + // avatar's parameters, which each worn wearable's writeToAvatar overwrites in wear order — the last hair's values apply. + // Predicted: two hairs (moustache 0 then 1) == a single hair with moustache 1, != a single hair with moustache 0, and the + // facialhair layer appears exactly once in the report and once in the morph-mask gather. + var c = NewCompositor(); + WornWearable Hair(float moustache) => Wear(WearableKind.Hair, new() { [114] = 0.5f, [1007] = moustache }, (TextureSlot.Hair, Flat(32, 32, 255, 255, 255, 0))); + List Outfit(params WornWearable[] hairs) + { + var o = BaseOutfit(male: true); + // facialhair sits below head_bodypaint in avatar_lad.xml; a transparent skin texture lets it show (as in the makeup test) + o[1] = Wear(WearableKind.Skin, new() { [111] = 0.5f }, (TextureSlot.HeadBodypaint, Flat(64, 64, 200, 150, 120, 0))); + o.RemoveAt(2); // the base hair + o.AddRange(hairs); + return o; + } + var first = c.Bake(BakeChannel.Head, Outfit(Hair(0f)), 128); + var second = c.Bake(BakeChannel.Head, Outfit(Hair(1f)), 128); + var both = c.Bake(BakeChannel.Head, Outfit(Hair(0f), Hair(1f)), 128); + + string Diag(string tag, CompositeResult r) => $"{tag}: " + string.Join(" | ", r.Layers.Where(l => l.Layer == "facialhair").Select(l => $"{l.Status}: {l.Detail}")) + $" | mask max {r.MorphMask.Max()}"; + var diag = Diag("first", first) + "\n" + Diag("second", second) + "\n" + Diag("both", both); + Assert.True(1 == both.Layers.Count(l => l.Layer == "facialhair" && l.Status is "drawn" or "skipped"), diag); + Assert.True(1 == both.Layers.Count(l => l.Layer == "facialhair" && l.Status == "morph"), diag); + Assert.True(both.Layers.Any(l => l.Layer == "facialhair" && l.Status == "morph" && l.Detail.StartsWith("once (plain layer)")), diag); + + // colour pass: the moustache is drawn from the last hair's parameter + Assert.True(second.Layers.Any(l => l.Layer == "facialhair" && l.Status == "drawn"), diag); + Assert.True(first.Layers.Any(l => l.Layer == "facialhair" && l.Status == "skipped"), diag); + Assert.Equal(second.Image.R, both.Image.R); Assert.Equal(second.Image.G, both.Image.G); Assert.Equal(second.Image.B, both.Image.B); + Assert.NotEqual(first.Image.R, both.Image.R); + // morph mask (5th component): once, with the last hair's parameter — not the product of both hairs' masks (which would be 0) + Assert.Equal(second.MorphMask, both.MorphMask); + Assert.All(first.MorphMask, v => Assert.Equal(0, v)); + Assert.True(both.MorphMask.Max() > 32, diag); // non-zero (the mask is box-averaged 16:1 at this size, so its peak is well below 255) + } + + [Fact] + public void the_male_parameter_is_read_from_the_shape() + { + var c = NewCompositor(); + Assert.False(c.IsMale(BaseOutfit(male: false))); + Assert.True(c.IsMale(BaseOutfit(male: true))); + } + + // ---------------- Bakes-on-Mesh extra bakes and multi-wearables ---------------- + + [Fact] + public void the_extra_bakes_composite_the_aux_base_and_the_universal_tattoo() + { + var c = NewCompositor(); + var plain = c.Bake(BakeChannel.LeftArm, BaseOutfit(), 64).Image; + var mid = Px(plain, 32, 32); + Assert.True(Math.Abs(mid.R - 128) < 6 && Math.Abs(mid.G - 128) < 6 && Math.Abs(mid.B - 128) < 6, $"aux_base.tga x fixed grey, got {mid}"); + Assert.True(mid.A < 8, $"an unpainted extra bake is (almost) transparent, as aux_base.tga's alpha says; got {mid.A}"); + var outfit = BaseOutfit(); + outfit.Add(Wear(WearableKind.Universal, new() { [1238] = 1f, [1239] = 0f, [1240] = 0f }, (TextureSlot.LeftArmTattoo, Flat(16, 16, 255, 255, 255)))); + var result = c.Bake(BakeChannel.LeftArm, outfit, 64); + var p = Px(result.Image, 32, 32); + Assert.True(p.R > 200 && p.G < 40 && p.B < 40, $"white tattoo tinted red by tattoo_universal_red, got {p}"); + Assert.Equal(255, p.A); + Assert.Contains(result.Layers, l => l.Layer == "leftarm_tattoo" && l.Status == "drawn"); + foreach (var bt in new[] { BakeChannel.LeftLeg, BakeChannel.Aux1, BakeChannel.Aux2, BakeChannel.Aux3 }) + Assert.Equal(64, c.Bake(bt, outfit, 64).Image.W); + } + + [Fact] + public void two_shirts_layer_in_wear_order_each_with_its_own_parameters() + { + // first shirt long-sleeved red, second short-sleeved blue: the torso is blue (on top), the wrist stays red (only the first covers it) + var others = new[] { "shirt_bottom_alpha.tga", "shirt_collar_alpha.tga", "shirt_collar_back_alpha.tga" }; + var torso = FindMaskPixel("shirt_sleeve_alpha.tga", 256, v => v == 255, others); + var wrist = FindMaskPixel("shirt_sleeve_alpha.tga", 256, v => v is > 60 and < 120, others); + var outfit = BaseOutfit(); + outfit.Add(Wear(WearableKind.Shirt, new() { [800] = 1f, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 1f, [804] = 0f, [805] = 0f }, (TextureSlot.UpperShirt, Flat(16, 16, 255, 255, 255)))); + outfit.Add(Wear(WearableKind.Shirt, new() { [800] = 0f, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 0f, [804] = 0f, [805] = 1f }, (TextureSlot.UpperShirt, Flat(16, 16, 255, 255, 255)))); + var result = NewCompositor().Bake(BakeChannel.Upper, outfit, 256); + var t = Px(result.Image, torso.X, torso.Y); + var w = Px(result.Image, wrist.X, wrist.Y); + Assert.True(t.B > 200 && t.R < 40, $"torso shows the second (blue) shirt, got {t}"); + Assert.True(w.R > 200 && w.B < 40, $"wrist shows the first (red, long-sleeved) shirt, got {w}"); + Assert.Equal(2, result.Layers.Count(l => l.Layer == "upper_clothes" && l.Status == "drawn")); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/.gitignore b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/.gitignore new file mode 100644 index 00000000000..bd67685778a --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/.gitignore @@ -0,0 +1,5 @@ +# Fetched by fetch-fixtures.sh: resident content and binaries, never committed. +# One subdirectory per reference set (truly-stock, aleric-max, ...); manifests are committed, fixtures are not. +*/fixtures/ +# Written by GoldenTests on every run, one per set. +last-run-*.txt diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenTests.cs b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenTests.cs new file mode 100644 index 00000000000..f530489c09b --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenTests.cs @@ -0,0 +1,318 @@ +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using OpenMetaverse; +using Xunit; +using Xunit.Abstractions; + +namespace OpenSimNGC.Appearance.Baking.Tests.Golden; + +/// +/// The golden harness. Each subdirectory of Golden/ that holds a manifest.json is one **reference set**: +/// an avatar's worn outfit plus the reference bakes (LL compositor output, captured via the client-bake path +/// named in that manifest). The authority is the LL compositor, never the capturing client (Ledger P-1). +/// +/// truly-stock/ — Truly Bazar, stock Library outfit (S0b). +/// aleric-max/ — Aleric Fenwood, a richer outfit: socks, jacket and a tattoo (S1b, Ledger Q-11). +/// +/// Fixtures are fetched per set by fetch-fixtures.sh <set> into <set>/fixtures/ (gitignored); +/// when they are absent the test reports that and returns without asserting anything. +/// +/// bakes at the manifest's bakeSize — which is the +/// shipped [Appearance] BakeSize (ADR-008: 1024), not the reference's own size — and asserts, per channel: +/// RGB (mean |d| <= 4, at most 5% of pixels with |d| > 8 — both skipped when the reference alpha is entirely +/// zero, as for a bald hair), alpha (mean |d| <= 2) and the 5th component, the morph mask (mean |d| <= 4 and, +/// unless the reference's mask is uniform, at most 5% of pixels with |d| > 8). It writes the table and the full +/// per-layer decision log to Golden/last-run-<set>.txt. The numbers came first (S0b), the thresholds after. +/// +/// (S1b Part 2) repeats the comparison at 512, 1024 and 2048 and reports the +/// encoded byte size per channel per size. It asserts only that every channel encodes and decodes at the size asked +/// for: it is the evidence for ADR-008's default, not a gate on it. +/// +public class GoldenTests +{ + private readonly ITestOutputHelper _out; + public GoldenTests(ITestOutputHelper output) { _out = output; } + + private static string GoldenDir([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!; + + /// Every set present: a subdirectory holding a manifest.json. + public static IEnumerable Sets() + => Directory.EnumerateDirectories(GoldenDir()) + .Where(d => File.Exists(Path.Combine(d, "manifest.json"))) + .Select(d => Path.GetFileName(d)!) + .OrderBy(n => n, StringComparer.Ordinal) + .Select(n => new object[] { n }); + + private sealed record Manifest(string Avatar, string Outfit, string Captured, + [property: System.Text.Json.Serialization.JsonPropertyName("captured_via")] string? CapturedVia, + int BakeSize, Dictionary Goldens); + private sealed record AvatarRow(int Type, int Index, string ItemId, string AssetId); + private sealed record AvatarJson(string PrincipalId, List Wearables, List VisualParams); + + private static readonly (string Key, BakeChannel Channel)[] ChannelKeys = + { + ("head", BakeChannel.Head), ("upper", BakeChannel.Upper), ("lower", BakeChannel.Lower), ("eyes", BakeChannel.Eyes), ("hair", BakeChannel.Hair), + ("skirt", BakeChannel.Skirt), ("leftarm", BakeChannel.LeftArm), ("leftleg", BakeChannel.LeftLeg), ("aux1", BakeChannel.Aux1), ("aux2", BakeChannel.Aux2), ("aux3", BakeChannel.Aux3), + }; + + private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true }; + + /// One channel's comparison against its reference. + private sealed record Row(string Key, double MeanRgb, double MeanA, double PctRgb, double MeanM, double PctM, + string MRef, bool RefAlphaAllZero, int OurW, int OurH, int RefW, int RefH, bool RefAlpha, int Bytes, string GoldenId); + + private sealed class SetContext + { + public required string Name; + public required string Dir; + public required string Fixtures; + public required Manifest Manifest; + public required AvatarJson Avatar; + public required List Wearables; + public required List Parsed; + public required Dictionary Textures; + public required List NullSlots; + public required Dictionary VisualParams; + } + + /// Loads a set, or returns null with a reason when its fixtures are not there. + private static SetContext? Load(string set, out string? reason) + { + reason = null; + var dir = Path.Combine(GoldenDir(), set); + var fixtures = Path.Combine(dir, "fixtures"); + var manifest = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(dir, "manifest.json")), JsonOpts)!; + if (!Directory.Exists(fixtures) || !File.Exists(Path.Combine(fixtures, "avatar.json"))) + { + reason = $"SKIPPED [{set}]: no fixtures at {fixtures}. Run Golden/fetch-fixtures.sh {set} (needs the Legion grid DB and Robust) to populate them; nothing is asserted without them."; + return null; + } + + var avatar = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(fixtures, "avatar.json")), JsonOpts)!; + string Fixture(string uuid, params string[] exts) + { + foreach (var e in exts) { var p = Path.Combine(fixtures, $"{uuid}.{e}"); if (File.Exists(p)) return p; } + throw new FileNotFoundException($"fixture {uuid} ({string.Join("/", exts)}) missing from {fixtures}; re-run fetch-fixtures.sh {set}"); + } + + // the request: every worn wearable's text, every texture they reference in a drawn slot. + // A worn slot carrying the null asset id is still a worn wearable — the viewer counts wearables, not + // textures (Docs/MORPH-MASK-PASS.md §2.4) — so it goes in with empty text and is named in the report. + var wearables = new List(); + var nullSlots = new List(); + foreach (var row in avatar.Wearables.OrderBy(w => w.Type).ThenBy(w => w.Index)) + { + if (!UUID.TryParse(row.AssetId, out var assetId) || assetId.IsZero()) + { + nullSlots.Add($"{(WearableKind)row.Type}:{row.Index}"); + wearables.Add(new WearableInput(UUID.Zero, row.Type, "")); + continue; + } + wearables.Add(new WearableInput(assetId, row.Type, File.ReadAllText(Fixture(row.AssetId, "bodypart", "clothing"), Encoding.UTF8))); + } + var parsed = wearables.Where(w => w.RawText.Length > 0).Select(w => WearableParser.Parse(w.RawText)).ToList(); + var textures = new Dictionary(); + foreach (var pw in parsed) + foreach (var (_, id) in pw.Textures) + { + if (id == UUID.Zero || id == BakeConstants.DefaultAvatarTexture || textures.ContainsKey(id)) continue; + var p = Path.Combine(fixtures, $"{id}.j2c"); + if (File.Exists(p)) textures[id] = new TextureInput(id, File.ReadAllBytes(p)); + } + + return new SetContext + { + Name = set, Dir = dir, Fixtures = fixtures, Manifest = manifest, Avatar = avatar, + Wearables = wearables, Parsed = parsed, Textures = textures, NullSlots = nullSlots, + VisualParams = DecodeVisualParams(avatar.VisualParams), + }; + } + + /// + /// The avatar's VisualParams as the simulator sends them, decoded through the parameter table exactly as + /// BakeOrchestrator.Resolve does. Wearables' own stored values still win; this fills in what none of them + /// carries — which for a worn-but-assetless slot is everything (Docs/MORPH-MASK-PASS.md §2.4). + /// + private static Dictionary DecodeVisualParams(List? bytes) + { + var overlay = new Dictionary(); + if (bytes is null || bytes.Count == 0) return overlay; + var list = VisualParamEncoder.SendList(new TexLayerCompositor().Lad); + if (bytes.Count != list.Count) return overlay; + for (var i = 0; i < list.Count; i++) + overlay[list[i].Id] = list[i].Min + bytes[i] / 255f * (list[i].Max - list[i].Min); + return overlay; + } + + /// Bakes the set at one size and compares every channel the manifest has a reference for. + private static (IReadOnlyList Results, List Rows, List Failures) Compare(SetContext c, int size) + { + var request = new BakeRequest(c.Wearables, c.VisualParams, c.Textures, size); + var results = new SkiaBakeBackend().Bake(request); + var rows = new List(); + var failures = new List(); + + foreach (var (key, ch) in ChannelKeys) + { + if (!c.Manifest.Goldens.TryGetValue(key, out var goldenId)) continue; + var ours = results.SingleOrDefault(r => r.Channel == ch) ?? throw new InvalidOperationException($"the library produced no {ch} bake"); + var mine = J2kCodec.Decode(ours.J2kBytes); + var golden = J2kCodec.Decode(File.ReadAllBytes(Path.Combine(c.Fixtures, $"{goldenId}.j2c"))); + var a = mine.Resample(size, size); + var b = golden.Resample(size, size); + + long sumRgb = 0, sumA = 0, over8 = 0; + var n = size * size; + for (var i = 0; i < n; i++) + { + int dr = Math.Abs(a.R[i] - b.R[i]), dg = Math.Abs(a.G[i] - b.G[i]), db = Math.Abs(a.B[i] - b.B[i]); + sumRgb += dr + dg + db; + sumA += Math.Abs(a.A[i] - b.A[i]); + if (dr > 8 || dg > 8 || db > 8) over8++; + } + var meanRgb = sumRgb / (3.0 * n); + var meanA = sumA / (double)n; + var pct = 100.0 * over8 / n; + + // A channel whose reference alpha is entirely zero (a fully transparent bake, e.g. a bald hair) has no + // visible RGB, so its two RGB assertions are skipped and the row says so; alpha is still asserted. + var refAlphaAllZero = true; + for (var i = 0; i < n && refAlphaAllZero; i++) if (b.A[i] > 2) refAlphaAllZero = false; + if (!refAlphaAllZero && meanRgb > 4.0) failures.Add($"{key}: mean |dRGB| {meanRgb:F2} > 4.0"); + if (!refAlphaAllZero && pct > 5.0) failures.Add($"{key}: {pct:F2}% pixels |dRGB| > 8 exceeds 5%"); + if (meanA > 2.0) failures.Add($"{key}: mean |dA| {meanA:F2} > 2.0"); + + // the 5th component: ours (always present) against the reference's (present on every viewer bake) + if (a.Mask is null) throw new InvalidOperationException($"our {ch} bake has no 5th component"); + if (b.Mask is null) throw new InvalidOperationException($"reference {ch} bake {goldenId} has no 5th component"); + long sumM = 0, overM = 0; int refMin = 255, refMax = 0; + for (var i = 0; i < n; i++) + { + var d = Math.Abs(a.Mask[i] - b.Mask[i]); + sumM += d; if (d > 8) overM++; + if (b.Mask[i] < refMin) refMin = b.Mask[i]; if (b.Mask[i] > refMax) refMax = b.Mask[i]; + } + var meanM = sumM / (double)n; + var pctM = 100.0 * overM / n; + var uniform = refMax - refMin <= 2; // a flat reference mask (no morph-mask layer worn): lossy coding jitters it by a level or two + if (meanM > 4.0) failures.Add($"{key}: mean |dM| {meanM:F2} > 4.0"); + if (!uniform && pctM > 5.0) failures.Add($"{key}: {pctM:F2}% pixels |dM| > 8 exceeds 5%"); + + rows.Add(new Row(key, meanRgb, meanA, pct, meanM, pctM, uniform ? $"uniform({refMin})" : $"{refMin}..{refMax}", + refAlphaAllZero, mine.W, mine.H, golden.W, golden.H, golden.HasAlpha, ours.J2kBytes.Length, goldenId)); + } + return (results, rows, failures); + } + + private static void Header(StringBuilder report, SetContext c, int size) + { + var m = c.Manifest; + report.AppendLine($"reference-bake run {DateTimeOffset.Now:O} set={c.Name} avatar={m.Avatar} outfit={m.Outfit} reference=LL compositor captured={m.Captured} via {m.CapturedVia ?? "?"} size={size}"); + report.AppendLine($"wearables: {string.Join(", ", c.Parsed.Select(p => $"{p.Kind}('{p.Name}', {p.Params.Count}p, {p.Textures.Count(t => t.Value != UUID.Zero && t.Value != BakeConstants.DefaultAvatarTexture)}t)"))}"); + if (c.NullSlots.Count > 0) report.AppendLine($"worn but assetless (contributes as an instance, no textures): {string.Join(", ", c.NullSlots)}"); + report.AppendLine($"visual params overlaid: {c.VisualParams.Count}"); + report.AppendLine($"textures supplied: {c.Textures.Count}"); + } + + private static void Table(StringBuilder report, List rows) + { + report.AppendLine("channel meanAbsRGB meanAbsA pctRGB>8 meanAbsM pctM>8 M-ref ours(WxH) reference(WxH,alpha) bytes reference-uuid"); + foreach (var r in rows) + report.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0,-8} {1,10:F2} {2,9:F2} {3,9:F2}% {4,10:F2} {5,7:F2}% {6,-12} {7,-11} {8,-22} {9,6} {10}{11}", + r.Key, r.MeanRgb, r.MeanA, r.PctRgb, r.MeanM, r.PctM, r.MRef, $"{r.OurW}x{r.OurH}", + $"{r.RefW}x{r.RefH},{(r.RefAlpha ? "a" : "-")}", r.Bytes, r.GoldenId, + r.RefAlphaAllZero ? " [RGB assertions skipped: reference alpha is entirely zero]" : "")); + } + + // ------------------------------------------------------------------ the threshold gate + + [Theory] + [MemberData(nameof(Sets))] + public void reference_set_versus_library_bakes(string set) + { + var c = Load(set, out var reason); + if (c is null) { _out.WriteLine(reason!); return; } + + var size = c.Manifest.BakeSize; + var (results, rows, failures) = Compare(c, size); + + var report = new StringBuilder(); + Header(report, c, size); + report.AppendLine($"channels baked: {string.Join(", ", results.Select(r => r.Channel))}"); + var produced = results.Select(r => r.Channel).ToHashSet(); + report.AppendLine($"channels not produced (nothing worn feeds them): {string.Join(", ", Enum.GetValues().Where(ch => !produced.Contains(ch)))}"); + var refusals = results.FirstOrDefault()?.Fidelity.Refusals ?? Array.Empty(); + report.AppendLine($"fidelity refusals: {(refusals.Count == 0 ? "none" : string.Join("; ", refusals))}"); + report.AppendLine(); + Table(report, rows); + report.AppendLine(); + + // the full per-layer decision log, every channel: this is the fidelity surface (S1b Part 3) + foreach (var r in results) + { + report.AppendLine($"[{r.Channel}] hash={r.InputHash[..16]} bytes={r.J2kBytes.Length} missingTextures=[{string.Join(", ", r.Fidelity.MissingTextures)}] unsupportedLayers=[{string.Join(" | ", r.Fidelity.UnsupportedLayers)}]"); + foreach (var line in r.Fidelity.Notes) report.AppendLine($" {line}"); + } + + if (failures.Count > 0) report.AppendLine($"threshold assertions FAILED: {string.Join("; ", failures)}"); + var text = report.ToString(); + File.WriteAllText(Path.Combine(GoldenDir(), $"last-run-{set}.txt"), text); + _out.WriteLine(text); + + Assert.Equal(c.Manifest.Goldens.Count, rows.Count); + Assert.True(failures.Count == 0, string.Join("; ", failures)); + } + + // ------------------------------------------------------------------ S1b Part 2: bake size + + [Theory] + [MemberData(nameof(Sets))] + public void bake_size_sweep(string set) + { + var c = Load(set, out var reason); + if (c is null) { _out.WriteLine(reason!); return; } + + var report = new StringBuilder(); + Header(report, c, 0); + report.AppendLine("Reported per size: the same comparison, both images resampled to that size before differencing,"); + report.AppendLine("plus the encoded byte size of our bake. Evidence for ADR-008's default; asserts nothing about it."); + + var bytesBySize = new Dictionary>(); + foreach (var size in new[] { 512, 1024, 2048 }) + { + var (results, rows, failures) = Compare(c, size); + report.AppendLine(); + report.AppendLine($"--- {c.Name} at {size} ---"); + Table(report, rows); + report.AppendLine(failures.Count == 0 + ? "thresholds: all pass at this size" + : $"thresholds at this size: {string.Join("; ", failures)}"); + bytesBySize[size] = rows.ToDictionary(r => r.Key, r => r.Bytes); + + foreach (var r in results) + { + var mine = J2kCodec.Decode(r.J2kBytes); + Assert.Equal(size, mine.W); + Assert.Equal(size, mine.H); + } + } + + report.AppendLine(); + report.AppendLine("encoded bytes per channel per size (our bake)"); + report.AppendLine("channel 512 1024 2048 1024/512 2048/1024"); + foreach (var key in bytesBySize[512].Keys) + { + double b512 = bytesBySize[512][key], b1024 = bytesBySize[1024][key], b2048 = bytesBySize[2048][key]; + report.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0,-8} {1,10} {2,10} {3,10} {4,10:F2}x {5,9:F2}x", + key, (int)b512, (int)b1024, (int)b2048, b1024 / b512, b2048 / b1024)); + } + var totals = new[] { 512, 1024, 2048 }.Select(s => bytesBySize[s].Values.Sum()).ToArray(); + report.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0,-8} {1,10} {2,10} {3,10}", "TOTAL", totals[0], totals[1], totals[2])); + + var text = report.ToString(); + File.WriteAllText(Path.Combine(GoldenDir(), $"last-run-{set}-sizes.txt"), text); + _out.WriteLine(text); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/aleric-max/manifest.json b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/aleric-max/manifest.json new file mode 100644 index 00000000000..56821754e8b --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/aleric-max/manifest.json @@ -0,0 +1,24 @@ +{ + "avatar": "Aleric Fenwood", + "outfit": "Professional Male 1: Adam Skin (shaved), hair base, pants, shoe base, mens black socks, Black Suede Blazer with blue shirt (jacket), head tattoo; Shirt slot worn but empty (null asset id). 10 worn slots, none doubled; no Universal, Alpha, Gloves or Skirt.", + "captured": "2026-09-03", + "captured_via": "Firestorm client-bake path", + "authority": "LL viewer compositor (lltexlayer.cpp / llavatarappearance.cpp); the reference bakes are its output, captured through a client; nothing is keyed on the capturing client", + "grid": "Legion Grid (legiongrid_mysql / Robust localhost:8003)", + "region": "Ebony", + "bakeSize": 1024, + "goldens": { + "head": "6e572118-ceb1-4da2-8db2-aa8882b4720f", + "upper": "1acde2f6-2b68-475d-b48e-8b0c2c1f64c1", + "lower": "36087dee-d890-4cf9-a086-9edf9de59509", + "eyes": "1df4c391-cbe0-499e-b703-df08647bbb37", + "hair": "dff4be04-3213-4c63-b4bb-bbb3b88b0e01" + }, + "notSet": ["skirt", "leftarm", "leftleg", "aux1", "aux2", "aux3"], + "notes": [ + "Second reference set (S1b), added to close Ledger Q-11: Truly's stock-Library outfit exercised no unsupported layer, so the compositor's behaviour on a richer outfit was unproven.", + "Goldens are reference bakes (LL compositor output) captured on 2026-09-03 via the Firestorm client-bake path.", + "Bakes are temporary assets: Robust answers 404 for them; fetch-fixtures.sh falls back to the region's Flotsam asset cache, where the sim keeps them as the same AssetBase XML.", + "Fixtures are not committed (binaries, resident content). Run fetch-fixtures.sh aleric-max to populate fixtures/." + ] +} diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh new file mode 100644 index 00000000000..30fcda37c6d --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# Populate /fixtures/ (gitignored) with one avatar's current wearables, the textures they +# reference, and the reference bakes (LL compositor output, captured via the client-bake path +# named in that set's manifest.json). +# +# Usage: ./fetch-fixtures.sh [set-name] (default: truly-stock) +# +# A "set" is a subdirectory here holding manifest.json (committed) and fixtures/ (not committed): +# truly-stock/ Truly Bazar, stock Library outfit (S0b) +# aleric-max/ Aleric Fenwood, richer outfit (S1b, Ledger Q-11) +# +# The avatar's name comes from the manifest's "avatar" field; the reference bake UUIDs from its +# "goldens" map. Steps: +# +# 1. The avatar's PrincipalID from the live grid DB (container legiongrid_mysql, database legiongrid). +# The root password is read from D:\legiongrid-runtime\.env (key LEGIONGRID_DB_ROOT_PW); it is +# never written anywhere. +# 2. Their Avatars rows ('Wearable :' = itemID:assetID, and VisualParams) -> fixtures/avatar.json +# 3. Every wearable asset, every texture those wearables reference, and the reference bakes, from Robust +# (http://localhost:8003/assets/, AssetBase XML with base64 Data) -> fixtures/.. +# Bakes are temporary assets and Robust does not hold them; for those the region's Flotsam asset +# cache (same AssetBase XML on disk) is read instead, and the source column says so. +# +# Nothing is fabricated: any UUID that cannot be fetched from either source stops the script (exit 1). +# +# Requires: bash, docker, curl, python 3 ("python3" or "python" on PATH) for XML + base64. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SET="${1:-truly-stock}" +SET_DIR="$HERE/$SET" +MANIFEST="$SET_DIR/manifest.json" +OUT="$SET_DIR/fixtures" +ENV_FILE="${LEGIONGRID_ENV:-D:/legiongrid-runtime/.env}" +ROBUST="${ROBUST_ASSETS:-http://localhost:8003/assets}" +REGION_CACHE="${LEGIONGRID_REGION_CACHE:-D:/legiongrid/regionserver/assetcache}" +DB_CONTAINER="${LEGIONGRID_DB_CONTAINER:-legiongrid_mysql}" +DB_NAME="${LEGIONGRID_DB_NAME:-legiongrid}" + +die() { echo "FETCH FAILED: $*" >&2; exit 1; } + +if command -v python3 >/dev/null 2>&1 && python3 -c "" >/dev/null 2>&1; then PY=python3 +elif command -v python >/dev/null 2>&1; then PY=python +else die "python 3 not found on PATH"; fi + +[ -d "$SET_DIR" ] || die "no such set '$SET' ($SET_DIR). Sets here: $(ls -d "$HERE"/*/ 2>/dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' ')" +[ -f "$MANIFEST" ] || die "$MANIFEST not found" + +# the avatar's name from the manifest +read -r FIRST LAST </dev/null | tr -d '\r'; } + +mkdir -p "$OUT" + +# ---------------------------------------------------------------- 1. principal +PID="$(sql "SELECT PrincipalID FROM UserAccounts WHERE FirstName='$FIRST' AND LastName='$LAST'" | head -1)" +[ -n "$PID" ] || die "no UserAccounts row for $FIRST $LAST (is $DB_CONTAINER up?)" +echo "principal $PID" + +# ---------------------------------------------------------------- 2. avatar rows -> avatar.json +sql "SELECT Name, Value FROM Avatars WHERE PrincipalID='$PID' AND (Name LIKE 'Wearable %' OR Name='VisualParams') ORDER BY Name" > "$OUT/avatar.tsv" +[ -s "$OUT/avatar.tsv" ] || die "no Avatars rows for $PID" +"$PY" - "$OUT/avatar.tsv" "$OUT/avatar.json" "$PID" <<'PY' +import json, sys +rows = [l.rstrip('\r\n').split('\t', 1) for l in open(sys.argv[1], encoding='utf-8') if l.strip()] +wearables, vp = [], [] +for name, value in rows: + if name == 'VisualParams': + vp = [int(x) for x in value.split(',') if x.strip() != ''] + elif name.startswith('Wearable '): + t, i = name[len('Wearable '):].split(':') + item, asset = value.split(':') + wearables.append({'name': name, 'type': int(t), 'index': int(i), 'itemId': item, 'assetId': asset}) +wearables.sort(key=lambda w: (w['type'], w['index'])) +json.dump({'principalId': sys.argv[3], 'wearables': wearables, 'visualParams': vp}, open(sys.argv[2], 'w'), indent=2) +print(f"avatar.json: {len(wearables)} wearables, {len(vp)} visual-param bytes") +PY +rm -f "$OUT/avatar.tsv" + +# ---------------------------------------------------------------- helpers (python on Windows writes CRLF: strip it) +list_wearables() { + "$PY" - "$OUT/avatar.json" <<'PY' | tr -d '\r' +import json, sys +for w in json.load(open(sys.argv[1]))['wearables']: + print(w['type'], w['assetId']) +PY +} + +list_goldens() { + "$PY" - "$MANIFEST" <<'PY' | tr -d '\r' +import json, sys +for k, v in json.load(open(sys.argv[1]))['goldens'].items(): + print(k, v) +PY +} + +# texture ids named by a wearable file: the lines after "textures N" +list_textures() { + "$PY" - "$1" <<'PY' | tr -d '\r' +import sys, re +lines = open(sys.argv[1], encoding='utf-8', errors='replace').read().replace('\r\n', '\n').split('\n') +i = 0 +while i < len(lines): + p = lines[i].split() + if len(p) >= 2 and p[0] == 'textures' and p[1].isdigit(): + n = int(p[1]); i += 1 + while n > 0 and i < len(lines): + q = lines[i].split(); i += 1 + if len(q) >= 2 and re.fullmatch(r'[0-9a-fA-F-]{36}', q[1]): + print(q[1]); n -= 1 + break + i += 1 +PY +} + +# unwrap an AssetBase XML into fixtures/.; prints the table row +unwrap() { + "$PY" - "$1" "$OUT" "$2" "$3" "$4" <<'PY' +import base64, sys, xml.etree.ElementTree as ET +xml, out, uuid, kind, source = sys.argv[1:6] +root = ET.parse(xml).getroot() +data = root.findtext('Data') or '' +atype = root.findtext('Type') or '?' +raw = base64.b64decode(data) if data.strip() else b'' +if not raw: + print(f"FETCH FAILED: {kind} {uuid}: empty Data in AssetBase from {source}", file=sys.stderr); sys.exit(1) +ext = {'0': 'j2c', '5': 'clothing', '13': 'bodypart'}.get(atype, 'type' + atype) +open(f"{out}/{uuid}.{ext}", 'wb').write(raw) +print(f"{uuid} {kind:<10} {source:<12} type={atype:<3} {len(raw):>8} bytes -> {uuid}.{ext}") +PY +} + +# fetch one asset: Robust, else the region cache +fetch() { + local uuid="$1" kind="$2" + local source="robust" + local xml="$OUT/$uuid.xml" + if ! curl -sf -o "$xml" "$ROBUST/$uuid" || [ ! -s "$xml" ]; then + rm -f "$xml" + local cached="$REGION_CACHE/${uuid:0:3}/$uuid" + if [ -s "$cached" ]; then cp "$cached" "$xml"; source="region-cache" + else die "$kind $uuid: not on Robust ($ROBUST) and not in $REGION_CACHE"; fi + fi + unwrap "$xml" "$uuid" "$kind" "$source" + rm -f "$xml" +} + +# ---------------------------------------------------------------- 3. assets +echo +echo "UUID kind source type bytes" +TEXTURES=() +while read -r t a; do + [ -n "$a" ] || continue + # A worn slot may carry the null asset id (the row exists, nothing is in it). The orchestrator skips + # those (BakeOrchestrator.Resolve: assetId.IsZero()); so does this, and says so. + if [ "$a" = "00000000-0000-0000-0000-000000000000" ]; then + printf '%s %-10s %-12s %s\n' "$a" "wearable:$t" "skipped" "null asset id: slot worn but empty" + continue + fi + fetch "$a" "wearable:$t" + f="$(ls "$OUT/$a".* | head -1)" + while read -r id; do + case "$id" in ""|00000000-0000-0000-0000-000000000000|c228d1cf-4b5d-4ba8-84f4-899a0796aa97) continue;; esac + TEXTURES+=("$id") + done < <(list_textures "$f") +done < <(list_wearables) + +if [ "${#TEXTURES[@]}" -gt 0 ]; then + while read -r id; do + [ -n "$id" ] && fetch "$id" "texture" + done < <(printf '%s\n' "${TEXTURES[@]}" | sort -u) +fi + +while read -r k v; do + [ -n "$v" ] && fetch "$v" "bake:$k" +done < <(list_goldens) + +echo +echo "fixtures in $OUT: $(ls "$OUT" | wc -l) files" diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/manifest.json b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/manifest.json new file mode 100644 index 00000000000..70d944ad3d8 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/manifest.json @@ -0,0 +1,23 @@ +{ + "avatar": "Truly Bazar", + "outfit": "stock Library", + "captured": "2026-09-03", + "captured_via": "Firestorm client-bake path", + "authority": "LL viewer compositor (lltexlayer.cpp / llavatarappearance.cpp); the reference bakes are its output, captured through a client; nothing is keyed on the capturing client", + "grid": "Legion Grid (legiongrid_mysql / Robust localhost:8003)", + "bakeSize": 1024, + "goldens": { + "head": "e7b03937-957c-435d-9a3b-3b516b489ed5", + "upper": "569a9a6d-913a-487f-a524-3502751638e9", + "lower": "952302a7-fede-4e92-91b6-5d23f5706ddf", + "eyes": "c66c39e9-f87f-4b61-81b1-001130945eef", + "hair": "120e197c-3bf6-43a8-bd72-5f8095052f59" + }, + "notSet": ["skirt", "leftarm", "leftleg", "aux1", "aux2", "aux3"], + "notes": [ + "Goldens are reference bakes (LL compositor output) captured on the wire on 2026-09-03 via the Firestorm client-bake path.", + "Bakes are temporary assets: Robust answers 404 for them; fetch-fixtures.sh falls back to the region's Flotsam asset cache, where the sim keeps them as the same AssetBase XML.", + "Fixtures are not committed (binaries, resident content). Run fetch-fixtures.sh to populate fixtures/.", + "bakeSize is the size the gate bakes at, not the reference's: it is the shipped [Appearance] BakeSize (ADR-008, 1024). It was 512 through S1; raised to 1024 in S2 Part 0 so the threshold gate diffs what the live sim actually stores. The references stay as captured (2048 for head/upper/lower/hair, 512 for eyes) and both images are resampled to the compared size." + ] +} diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/OpenSimNGC.Appearance.Baking.Tests.csproj b/Source/OpenSimNGC.Appearance.Baking.Tests/OpenSimNGC.Appearance.Baking.Tests.csproj new file mode 100644 index 00000000000..0bc2d479536 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/OpenSimNGC.Appearance.Baking.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + true + false + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/VisualParamEncoderTests.cs b/Source/OpenSimNGC.Appearance.Baking.Tests/VisualParamEncoderTests.cs new file mode 100644 index 00000000000..af1ea050a02 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/VisualParamEncoderTests.cs @@ -0,0 +1,67 @@ +using Xunit; + +namespace OpenSimNGC.Appearance.Baking.Tests; + +/// +/// The VisualParams block must be the one a viewer sends: 253 parameters (group 0 and 3) in id order, +/// float32-truncated bytes, worn wearable values on top of the sim's stored bytes for unworn types. +/// Ported from the web-viewer gateway (session 14) in S0b. +/// +public class VisualParamEncoderTests +{ + private static AvatarLad Lad => AvatarLad.Embedded; + + [Fact] + public void the_send_list_is_the_253_transmitted_parameters_in_id_order() + { + var list = VisualParamEncoder.SendList(Lad); + Assert.Equal(253, list.Count); + Assert.Equal(249, list.Count(p => p.Group == 0)); + Assert.Equal(new[] { 163, 868, 869, 877 }, list.Where(p => p.Group == 3).Select(p => p.Id).ToArray()); + Assert.True(list.Zip(list.Skip(1)).All(pair => pair.First.Id < pair.Second.Id)); + Assert.Equal(31, VisualParamEncoder.IndexOf(Lad, 80)); // `male`, what the client picks the body by + Assert.Equal(252, VisualParamEncoder.IndexOf(Lad, 11001)); // hover, the last one + } + + [Fact] + public void bytes_are_float32_truncated_like_the_viewer() + { + Assert.Equal(74, VisualParamEncoder.F32ToU8(0f, -0.5f, 1.2f)); // 0.5/1.7*255 = 74.999 in float32: 74, not 75 + Assert.Equal(255, VisualParamEncoder.F32ToU8(1f, 0f, 1f)); + Assert.Equal(0, VisualParamEncoder.F32ToU8(-3f, 0f, 1f)); // clamped + Assert.Equal(51, VisualParamEncoder.F32ToU8(0.2f, 0f, 1f)); + Assert.Equal(204, VisualParamEncoder.F32ToU8(0.8f, 0f, 1f)); + } + + [Fact] + public void worn_wearables_override_carried_bytes_which_override_defaults() + { + var lad = Lad; + var carried = new byte[253]; + for (var i = 0; i < carried.Length; i++) carried[i] = (byte)(i % 256); + var idx800 = VisualParamEncoder.IndexOf(lad, 800); // shirt sleeve length + var idx608 = VisualParamEncoder.IndexOf(lad, 608); // jacket bottom length: not worn here + var idx33 = VisualParamEncoder.IndexOf(lad, 33); // shape height + var worn = new List<(WearableKind, IReadOnlyDictionary)> + { + (WearableKind.Shape, new Dictionary { [33] = 1f }), + (WearableKind.Shirt, new Dictionary { [800] = 0.5f }), + (WearableKind.Shirt, new Dictionary { [800] = 1f }), // topmost shirt wins + }; + var r = VisualParamEncoder.Encode(lad, worn, carried); + Assert.Equal(253, r.Bytes.Length); + Assert.Equal(255, r.Bytes[idx800]); + Assert.Equal(VisualParamEncoder.F32ToU8(1f, lad.Params[33].Min, lad.Params[33].Max), r.Bytes[idx33]); + Assert.Equal(carried[idx608], r.Bytes[idx608]); + Assert.Equal(2, r.FromWearables); + Assert.Equal(251, r.Carried); + Assert.Equal(0, r.Defaults); + + var noSim = VisualParamEncoder.Encode(lad, worn, null); + Assert.Equal(VisualParamEncoder.F32ToU8(lad.Params[608].Default, lad.Params[608].Min, lad.Params[608].Max), noSim.Bytes[idx608]); + Assert.Equal(251, noSim.Defaults); + + var wrongLength = VisualParamEncoder.Encode(lad, worn, new byte[218]); // a stale blob is not carried + Assert.Equal(251, wrongLength.Defaults); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/AvatarLad.cs b/Source/OpenSimNGC.Appearance.Baking/AvatarLad.cs new file mode 100644 index 00000000000..7717cd3454e --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/AvatarLad.cs @@ -0,0 +1,236 @@ +using System.Globalization; +using System.Reflection; +using System.Xml.Linq; + +namespace OpenSimNGC.Appearance.Baking; + +/// Which avatars a parameter applies to (the `sex` attribute); the other sex sees the default weight. +public enum ParamSex { Both, Male, Female } + +/// How a colour parameter combines into a layer's net colour (`operation` attribute; add is the default). +public enum ColorOp { Add, Multiply, Blend } + +/// A colour with components in 0..1. +public readonly record struct Rgba(float R, float G, float B, float A) +{ + public static readonly Rgba White = new(1, 1, 1, 1); + public static readonly Rgba Transparent = new(0, 0, 0, 0); + + public Rgba Clamp() => new(Math.Clamp(R, 0, 1), Math.Clamp(G, 0, 1), Math.Clamp(B, 0, 1), Math.Clamp(A, 0, 1)); + public static Rgba operator +(Rgba a, Rgba b) => new(a.R + b.R, a.G + b.G, a.B + b.B, a.A + b.A); + public static Rgba operator *(Rgba a, Rgba b) => new(a.R * b.R, a.G * b.G, a.B * b.B, a.A * b.A); + public static Rgba Lerp(Rgba a, Rgba b, float t) => new(a.R + (b.R - a.R) * t, a.G + (b.G - a.G) * t, a.B + (b.B - a.B) * t, a.A + (b.A - a.A) * t); + + /// "r, g, b, a" with byte components, as avatar_lad.xml writes colours. + public static Rgba ParseBytes(string s) + { + var p = s.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (p.Length < 3) throw new FormatException($"colour '{s}'"); + float B(int i) => i < p.Length ? float.Parse(p[i], CultureInfo.InvariantCulture) / 255f : 1f; + return new Rgba(B(0), B(1), B(2), B(3)); + } + + public override string ToString() => $"({R:F2},{G:F2},{B:F2},{A:F2})"; +} + +/// `param_alpha`: a greyscale mask file, the width of its soft edge, and how it combines with the layer's other masks. +public sealed record AlphaParamInfo(string TgaFile, float Domain, bool SkipIfZero, bool MultiplyBlend); + +/// `param_color`: colour stops interpolated by the parameter's weight, and the operation that folds it into the layer colour. +public sealed record ColorParamInfo(ColorOp Op, IReadOnlyList Colors); + +/// `driven`: how a driver parameter's weight maps onto one driven parameter (trapezoid min1/max1/max2/min2). +public sealed record DrivenInfo(int Id, float Min1, float Max1, float Max2, float Min2); + +/// One visual parameter as avatar_lad.xml defines it (every occurrence of the id merged). +public sealed class ParamDef +{ + public int Id; + public string Name = ""; + /// The wearable type that owns the parameter ("shirt", "skin", ...); null for parameters no wearable stores. + public string? Wearable; + public ParamSex Sex = ParamSex.Both; + public int Group; + public float Min; + public float Max = 1f; + public float Default; + public AlphaParamInfo? Alpha; + public ColorParamInfo? Color; + public readonly List Driven = new(); +} + +/// One `layer` of a `layer_set`, in file order. +public sealed class LayerDef +{ + public string Name = ""; + /// `local_texture`: the wearable texture slot this layer draws (upper_shirt, head_bodypaint, ...). + public string? LocalTexture; + public bool LocalTextureAlphaOnly; + /// `tga_file`: a bundled image; with file_is_mask it is an alpha mask for the layer colour. + public string? StaticImage; + public bool StaticIsMask; + public Rgba FixedColor = Rgba.Transparent; + public string? GlobalColor; + public bool Bump; + public bool WriteAllChannels; + public bool VisibilityMask; + public readonly List ColorParams = new(); + public readonly List AlphaParams = new(); +} + +public sealed class LayerSetDef +{ + public string BodyRegion = ""; + public int Width = 512; + public int Height = 512; + public bool ClearAlpha = true; + public string? StaticAlphaFile; + public readonly List Layers = new(); +} + +/// +/// The parts of avatar_lad.xml the compositor needs: parameters (alpha masks, colour stops, drivers), the +/// six-plus-five bake layer sets in their exact layer order, and the three global colours. The file is embedded +/// in this assembly (ADR-007; see THIRD-PARTY-NOTICES.md) and is the same data the viewers bake from. +/// +public sealed class AvatarLad +{ + /// Manifest resource name of the embedded avatar_lad.xml. + public const string EmbeddedResourceName = "OpenSimNGC.Appearance.Baking.Data.avatar_lad.xml"; + + private static readonly Lazy s_embedded = new(LoadEmbedded, LazyThreadSafetyMode.ExecutionAndPublication); + + /// The avatar_lad.xml that ships with the library, parsed once. + public static AvatarLad Embedded => s_embedded.Value; + + public readonly Dictionary Params = new(); + public readonly Dictionary LayerSets = new(StringComparer.OrdinalIgnoreCase); + public readonly Dictionary> GlobalColors = new(StringComparer.OrdinalIgnoreCase); + /// + /// The <morph_masks> block: per body_region, the layer names whose alpha mask also drives a mesh + /// morph. Only these layers contribute to a bake's 5th component (see Docs/MORPH-MASK-PASS.md §2.1). + /// + public readonly Dictionary> MorphMaskLayers = new(StringComparer.OrdinalIgnoreCase); + + public static AvatarLad Load(string path) => Parse(XDocument.Load(path)); + + public static AvatarLad Load(Stream stream) => Parse(XDocument.Load(stream)); + + private static AvatarLad LoadEmbedded() + { + using var s = typeof(AvatarLad).Assembly.GetManifestResourceStream(EmbeddedResourceName) + ?? throw new InvalidOperationException($"embedded resource {EmbeddedResourceName} is missing"); + return Load(s); + } + + public static AvatarLad Parse(XDocument doc) + { + var lad = new AvatarLad(); + var root = doc.Root ?? throw new FormatException("avatar_lad.xml has no root"); + foreach (var p in root.Descendants("param")) lad.MergeParam(p); + foreach (var g in root.Descendants("global_color")) + { + var name = (string?)g.Attribute("name") ?? ""; + lad.GlobalColors[name] = g.Elements("param").Select(p => (int)p.Attribute("id")!).ToList(); + } + foreach (var ls in root.Descendants("layer_set")) + { + var set = new LayerSetDef + { + BodyRegion = (string?)ls.Attribute("body_region") ?? "", + Width = (int?)ls.Attribute("width") ?? 512, + Height = (int?)ls.Attribute("height") ?? 512, + ClearAlpha = Bool(ls.Attribute("clear_alpha"), true), + StaticAlphaFile = (string?)ls.Attribute("alpha_tga_file"), + }; + foreach (var l in ls.Elements("layer")) + { + var layer = new LayerDef + { + Name = (string?)l.Attribute("name") ?? "", + GlobalColor = (string?)l.Attribute("global_color"), + Bump = string.Equals((string?)l.Attribute("render_pass"), "bump", StringComparison.OrdinalIgnoreCase), + WriteAllChannels = Bool(l.Attribute("write_all_channels"), false), + VisibilityMask = Bool(l.Attribute("visibility_mask"), false), + }; + if (l.Attribute("fixed_color") is { } fc) layer.FixedColor = Rgba.ParseBytes(fc.Value); + foreach (var t in l.Elements("texture")) + { + if (t.Attribute("tga_file") is { } tga) + { + layer.StaticImage = tga.Value; + layer.StaticIsMask = Bool(t.Attribute("file_is_mask"), false); + } + else if (t.Attribute("local_texture") is { } lt) + { + layer.LocalTexture = lt.Value; + layer.LocalTextureAlphaOnly = Bool(t.Attribute("local_texture_alpha_only"), false); + } + } + foreach (var p in l.Elements("param")) + { + var id = (int)p.Attribute("id")!; + if (p.Element("param_alpha") is not null) layer.AlphaParams.Add(id); + else if (p.Element("param_color") is not null) layer.ColorParams.Add(id); + } + set.Layers.Add(layer); + } + lad.LayerSets[set.BodyRegion] = set; + } + foreach (var m in root.Descendants("morph_masks").Elements("mask")) + { + var region = (string?)m.Attribute("body_region") ?? ""; + var layer = (string?)m.Attribute("layer") ?? ""; + if (region.Length == 0 || layer.Length == 0) continue; + if (!lad.MorphMaskLayers.TryGetValue(region, out var names)) lad.MorphMaskLayers[region] = names = new HashSet(StringComparer.OrdinalIgnoreCase); + names.Add(layer); + } + return lad; + } + + private void MergeParam(XElement p) + { + if (p.Attribute("id") is null) return; + var id = (int)p.Attribute("id")!; + if (!Params.TryGetValue(id, out var def)) + { + def = new ParamDef + { + Id = id, + Name = (string?)p.Attribute("name") ?? "", + Wearable = (string?)p.Attribute("wearable"), + Group = (int?)p.Attribute("group") ?? 0, + Min = Float(p.Attribute("value_min"), 0f), + Max = Float(p.Attribute("value_max"), 1f), + Default = Float(p.Attribute("value_default"), 0f), + Sex = ((string?)p.Attribute("sex"))?.ToLowerInvariant() switch { "male" => ParamSex.Male, "female" => ParamSex.Female, _ => ParamSex.Both }, + }; + Params[id] = def; + } + else + { + def.Wearable ??= (string?)p.Attribute("wearable"); + if (p.Attribute("sex") is { } sx && def.Sex == ParamSex.Both) def.Sex = sx.Value.ToLowerInvariant() switch { "male" => ParamSex.Male, "female" => ParamSex.Female, _ => ParamSex.Both }; + } + if (p.Element("param_alpha") is { } pa) + def.Alpha = new AlphaParamInfo((string?)pa.Attribute("tga_file") ?? "", Float(pa.Attribute("domain"), 0f), Bool(pa.Attribute("skip_if_zero"), false), Bool(pa.Attribute("multiply_blend"), false)); + if (p.Element("param_color") is { } pc) + { + var op = ((string?)pc.Attribute("operation"))?.ToLowerInvariant() switch { "multiply" => ColorOp.Multiply, "blend" => ColorOp.Blend, _ => ColorOp.Add }; + def.Color = new ColorParamInfo(op, pc.Elements("value").Select(v => Rgba.ParseBytes((string?)v.Attribute("color") ?? "0,0,0,0")).ToList()); + } + if (p.Element("param_driver") is { } pd) + { + foreach (var d in pd.Elements("driven")) + { + var did = (int)d.Attribute("id")!; + if (def.Driven.Any(x => x.Id == did)) continue; + var max1 = Float(d.Attribute("max1"), def.Max); + def.Driven.Add(new DrivenInfo(did, Float(d.Attribute("min1"), def.Min), max1, Float(d.Attribute("max2"), max1), Float(d.Attribute("min2"), max1))); + } + } + } + + private static bool Bool(XAttribute? a, bool dflt) => a is null ? dflt : a.Value.Trim().ToLowerInvariant() is "true" or "1" or "yes"; + private static float Float(XAttribute? a, float dflt) => a is null ? dflt : float.Parse(a.Value.Trim(), CultureInfo.InvariantCulture); +} diff --git a/Source/OpenSimNGC.Appearance.Baking/BakeChannel.cs b/Source/OpenSimNGC.Appearance.Baking/BakeChannel.cs new file mode 100644 index 00000000000..11d19478d3c --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/BakeChannel.cs @@ -0,0 +1,32 @@ +namespace OpenSimNGC.Appearance.Baking; + +/// +/// The eleven baked-texture output channels an avatar can carry. +/// The first six are the legacy (pre-Bakes-on-Mesh) slots; the last five are the +/// extended slots signalled by RegionProtocols bit 63. +/// +public enum BakeChannel +{ + /// Head bake (TextureEntry face 8, TEX_HEAD_BAKED). + Head, + /// Upper-body bake (face 9, TEX_UPPER_BAKED). + Upper, + /// Lower-body bake (face 10, TEX_LOWER_BAKED). + Lower, + /// Eyes bake (face 11, TEX_EYES_BAKED). + Eyes, + /// Skirt bake (face 19, TEX_SKIRT_BAKED). + Skirt, + /// Hair bake (face 20, TEX_HAIR_BAKED). + Hair, + /// Left-arm bake (face 21, TEX_LEFT_ARM_BAKED). + LeftArm, + /// Left-leg bake (face 22, TEX_LEFT_LEG_BAKED). + LeftLeg, + /// Auxiliary bake 1 (face 23, TEX_AUX1_BAKED). + Aux1, + /// Auxiliary bake 2 (face 24, TEX_AUX2_BAKED). + Aux2, + /// Auxiliary bake 3 (face 25, TEX_AUX3_BAKED). + Aux3, +} diff --git a/Source/OpenSimNGC.Appearance.Baking/BakeHash.cs b/Source/OpenSimNGC.Appearance.Baking/BakeHash.cs new file mode 100644 index 00000000000..66537a926d2 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/BakeHash.cs @@ -0,0 +1,69 @@ +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using OpenMetaverse; + +namespace OpenSimNGC.Appearance.Baking; + +/// +/// Deterministic input hashing for bake reuse (ADR-004 BakeHash:<channel>). +/// The hash covers only the inputs that can influence the named channel, so a change to, say, a shirt does +/// not invalidate the lower-body bake. +/// +public static class BakeHash +{ + /// + /// SHA-256 (lower-case hex) over: the channel, the bake size, the sorted asset ids of the wearables that feed + /// the channel's layer set (the types that own one of its texture slots or one of its parameters, plus the + /// shape, whose `male` gates every layer), the sorted ids of the textures those wearables reference in the + /// channel's slots, and the values of the parameters the channel's layers read (colour, alpha, global colour + /// and their drivers) in parameter-id order, taken from or, failing + /// that, from the topmost wearable that stores them. Independent of dictionary and list ordering. + /// + public static string Compute(BakeChannel ch, BakeRequest r) => Compute(ch, r, new TexLayerCompositor()); + + public static string Compute(BakeChannel ch, BakeRequest r, TexLayerCompositor compositor) + { + ArgumentNullException.ThrowIfNull(r); + var lad = compositor.Lad; + var slots = compositor.SlotsOf(ch).ToHashSet(); + var paramIds = compositor.ParamsOf(ch).OrderBy(i => i).ToList(); + + var feedingKinds = new HashSet { WearableKind.Shape }; + foreach (var s in slots) { var k = TexLayerCompositor.WearableOf(s); if (k != WearableKind.Invalid) feedingKinds.Add(k); } + foreach (var id in paramIds) + if (lad.Params.TryGetValue(id, out var def) && def.Wearable is { Length: > 0 } owner && WearableKinds.FromName(owner) is { } k) feedingKinds.Add(k); + + var wearableIds = new List(); + var textureIds = new List(); + var stored = new Dictionary(); // topmost stored value per parameter among the feeding wearables + foreach (var w in r.Wearables) + { + ParsedWearable? pw = null; + try { pw = WearableParser.Parse(w.RawText); } catch (FormatException) { } + var kind = pw?.Kind ?? (WearableKind)w.WearableType; + if (!feedingKinds.Contains(kind)) continue; + wearableIds.Add(w.AssetId.ToString()); + if (pw is null) { textureIds.Add($"corrupt:{w.AssetId}"); continue; } + foreach (var (slot, id) in pw.Textures) + if (slots.Contains(slot) && id != UUID.Zero) textureIds.Add(id.ToString()); + foreach (var (id, v) in pw.Params) stored[id] = v; + } + wearableIds.Sort(StringComparer.Ordinal); + textureIds = textureIds.Distinct().OrderBy(s => s, StringComparer.Ordinal).ToList(); + + var sb = new StringBuilder(); + sb.Append("ch=").Append((int)ch).Append('|'); + sb.Append("size=").Append(r.BakeSize).Append('|'); + sb.Append("w=").Append(string.Join(",", wearableIds)).Append('|'); + sb.Append("t=").Append(string.Join(",", textureIds)).Append('|'); + sb.Append("p="); + foreach (var id in paramIds) + { + if (r.VisualParams.TryGetValue(id, out var v) || stored.TryGetValue(id, out v)) + sb.Append(id).Append('=').Append(v.ToString("R", CultureInfo.InvariantCulture)).Append(','); + } + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString())); + return Convert.ToHexString(hash).ToLowerInvariant(); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/BakeTimings.cs b/Source/OpenSimNGC.Appearance.Baking/BakeTimings.cs new file mode 100644 index 00000000000..68bd66fc862 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/BakeTimings.cs @@ -0,0 +1,101 @@ +using System.Diagnostics; + +namespace OpenSimNGC.Appearance.Baking; + +/// +/// Where a bake's time goes (Ledger Q-10). A caller that wants the breakdown creates one, hands it to the run +/// through , and reads it afterwards; a caller that does not pass one costs two +/// null checks per phase. +/// +/// +/// Five phases, which between them account for a bake end to end: +/// +/// — reading the wearable and texture assets out of the asset service. Measured by +/// the orchestrator, not the library: the library performs no I/O. +/// — turning the fetched JPEG 2000 texture bytes into planes. +/// — the layer-set render, per channel. +/// — the five-component JPEG 2000 encode, per channel. +/// — writing the finished bakes back (and the supersede deletes). +/// +/// Everything else — parsing wearables, hashing, reading the bake index, writing the faces — is the remainder, and +/// on any measurement so far it is noise. +/// +/// +/// +/// It is a sink, never an input: nothing here reaches a pixel, a hash or a byte of output. Counters are updated +/// with so a backend that composites channels in parallel stays correct. +/// +/// +public sealed class BakeTimings +{ + private long _fetchTicks, _decodeTicks, _compositeTicks, _encodeTicks, _storeTicks; + private long _assetsFetched, _fetchedBytes, _texturesDecoded, _decodedPixels; + private long _channelsComposited, _channelsEncoded, _assetsStored, _storedBytes, _assetsDeleted; + + /// A timestamp to pass to . Cheap; no allocation. + public static long Now => Stopwatch.GetTimestamp(); + + /// Elapsed time since a timestamp. + public static TimeSpan Since(long start) => Stopwatch.GetElapsedTime(start); + + public void AddAssetFetch(long start, long bytes) + { + Interlocked.Add(ref _fetchTicks, Since(start).Ticks); + Interlocked.Increment(ref _assetsFetched); + Interlocked.Add(ref _fetchedBytes, bytes); + } + + public void AddDecode(long start, long pixels) + { + Interlocked.Add(ref _decodeTicks, Since(start).Ticks); + Interlocked.Increment(ref _texturesDecoded); + Interlocked.Add(ref _decodedPixels, pixels); + } + + public void AddComposite(long start) + { + Interlocked.Add(ref _compositeTicks, Since(start).Ticks); + Interlocked.Increment(ref _channelsComposited); + } + + public void AddEncode(long start, long bytes) + { + Interlocked.Add(ref _encodeTicks, Since(start).Ticks); + Interlocked.Increment(ref _channelsEncoded); + Interlocked.Add(ref _storedBytes, bytes); + } + + public void AddAssetStore(long start, bool deleted = false) + { + Interlocked.Add(ref _storeTicks, Since(start).Ticks); + if (deleted) Interlocked.Increment(ref _assetsDeleted); + else Interlocked.Increment(ref _assetsStored); + } + + public TimeSpan AssetFetch => TimeSpan.FromTicks(Interlocked.Read(ref _fetchTicks)); + public TimeSpan Decode => TimeSpan.FromTicks(Interlocked.Read(ref _decodeTicks)); + public TimeSpan Composite => TimeSpan.FromTicks(Interlocked.Read(ref _compositeTicks)); + public TimeSpan Encode => TimeSpan.FromTicks(Interlocked.Read(ref _encodeTicks)); + public TimeSpan AssetStore => TimeSpan.FromTicks(Interlocked.Read(ref _storeTicks)); + + /// The five phases added together. Always less than the run's wall clock; the difference is the remainder. + public TimeSpan Accounted => AssetFetch + Decode + Composite + Encode + AssetStore; + + public int AssetsFetched => (int)Interlocked.Read(ref _assetsFetched); + public long FetchedBytes => Interlocked.Read(ref _fetchedBytes); + public int TexturesDecoded => (int)Interlocked.Read(ref _texturesDecoded); + public long DecodedPixels => Interlocked.Read(ref _decodedPixels); + public int ChannelsComposited => (int)Interlocked.Read(ref _channelsComposited); + public int ChannelsEncoded => (int)Interlocked.Read(ref _channelsEncoded); + public int AssetsStored => (int)Interlocked.Read(ref _assetsStored); + public int AssetsDeleted => (int)Interlocked.Read(ref _assetsDeleted); + public long StoredBytes => Interlocked.Read(ref _storedBytes); + + /// The one-line split, milliseconds: fetch=… decode=… composite=… encode=… store=…. + public string Summary => + $"fetch={AssetFetch.TotalMilliseconds:F0} ({AssetsFetched} assets, {FetchedBytes / 1024} KiB), " + + $"decode={Decode.TotalMilliseconds:F0} ({TexturesDecoded} textures), " + + $"composite={Composite.TotalMilliseconds:F0} ({ChannelsComposited} channels), " + + $"encode={Encode.TotalMilliseconds:F0} ({ChannelsEncoded} channels, {StoredBytes / 1024} KiB), " + + $"store={AssetStore.TotalMilliseconds:F0} ({AssetsStored} stored, {AssetsDeleted} superseded)"; +} diff --git a/Source/OpenSimNGC.Appearance.Baking/Contracts.cs b/Source/OpenSimNGC.Appearance.Baking/Contracts.cs new file mode 100644 index 00000000000..747a6286316 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/Contracts.cs @@ -0,0 +1,130 @@ +using OpenMetaverse; + +namespace OpenSimNGC.Appearance.Baking; + +/// +/// One worn wearable, as the compositor needs it: the asset it came from, its +/// wearable type, and the raw LLWearable text (the asset body) that names +/// the textures and parameter values for that wearable. +/// +/// Asset id of the wearable asset. +/// +/// Wearable type index as used on the wire and in avatar_lad.xml +/// (0 = shape, 1 = skin, 2 = hair, 3 = eyes, 4 = shirt, 5 = pants, ...; see ). +/// +/// Full text of the wearable asset, unparsed. +public sealed record WearableInput(UUID AssetId, int WearableType, string RawText); + +/// +/// One source texture referenced by a wearable, already fetched from the asset +/// service and still in its JPEG 2000 encoding. +/// +/// Texture asset id. +/// JPEG 2000 codestream or JP2 file bytes. +public sealed record TextureInput(UUID TextureId, byte[] J2kBytes); + +/// +/// Everything one bake run needs. The request is complete and self-contained: +/// the compositor performs no I/O and no asset fetches of its own. +/// +/// The wearables currently worn, in wear order (a later wearable of the same type is on top). +/// +/// Visual parameter values keyed by parameter id, in the parameter's native range from +/// avatar_lad.xml (not the 0..255 wire encoding). A worn wearable's own stored value always +/// wins for the parameters its type owns; these fill in only what no worn wearable stores, and take +/// part in . May be empty. +/// +/// +/// Source textures keyed by texture id. Any texture a wearable references that +/// is absent from this map is reported in +/// and its layer is skipped. +/// +/// Output edge size in pixels for every channel (ADR-008: 1024 by default). +public sealed record BakeRequest( + IReadOnlyList Wearables, + IReadOnlyDictionary VisualParams, + IReadOnlyDictionary Textures, + int BakeSize) +{ + /// + /// The channels to composite, or null (the default) for every channel the outfit needs. A caller that has + /// recognised a channel's inputs as unchanged and is reusing its stored bake (ADR-004) leaves that channel + /// out, and then nothing is decoded or composited for it. Naming a channel the outfit does not need does not + /// add it: the set is intersected with what the wearables actually feed. + /// + /// It is a request-shaping field only. It is deliberately **not** part of : the bake of + /// a channel must not depend on which of its siblings were asked for in the same call. + /// + /// + public IReadOnlyCollection? Channels { get; init; } + + /// + /// Optional sink for the phase split (Ledger Q-10). Purely an output: nothing in it reaches a pixel, a hash or + /// a byte of the bake, and a null one (the default) costs a null check per phase. The library fills in decode, + /// composite and encode; the caller fills in the two I/O phases around them. + /// + public BakeTimings? Timings { get; init; } +} + +/// +/// What the compositor could not reproduce faithfully for one output channel +/// (ADR-005: best-effort with a structured report; refusal only for corrupt input). +/// +/// +/// avatar_lad.xml layers of this channel that were skipped for a reason other than "nothing worn": +/// a bundled resource or mask file missing, or an unknown local texture. Each entry is layer: detail. +/// +/// Texture ids a wearable referenced in this channel's slots that were not supplied in the request. +/// One line per layer of the channel (layer status: detail) — the coverage evidence, for logs; never parsed. +/// +/// The fidelity gate's reasons for the whole outfit (the same list on every channel): wearable types the +/// compositor does not composite, texture slots no requested channel draws, duplicate body parts, missing +/// bundled resources. Empty means the outfit is one the compositor reproduces faithfully. The web-viewer +/// gateway refuses to send an appearance when this is non-empty; the simulator decides per ADR-005. +/// +public sealed record FidelityReport( + IReadOnlyList UnsupportedLayers, + IReadOnlyList MissingTextures, + IReadOnlyList Notes, + IReadOnlyList Refusals) +{ + /// True when the gate found nothing to refuse and every referenced texture was supplied. + public bool IsFaithful => Refusals.Count == 0 && MissingTextures.Count == 0 && UnsupportedLayers.Count == 0; +} + +/// +/// One finished bake. +/// +/// Which output channel this is. +/// The composited texture, single-tile JPEG 2000 codestream, ready to store as a texture asset. +/// +/// Deterministic hash of the inputs that produced this bake, as computed by +/// . Stored alongside the asset so an unchanged +/// input set can be recognised without re-baking (ADR-004 BakeHash:<channel>). +/// +/// What was and was not reproduced. +public sealed record BakeResult( + BakeChannel Channel, + byte[] J2kBytes, + string InputHash, + FidelityReport Fidelity) +{ + /// + /// True when **no layer of this channel drew anything** — every colour layer was skipped, so nothing ever + /// reached the canvas. Set from the compositor's own per-layer decisions, never by inspecting pixels, and it + /// is per channel: an outfit can have one undrawn channel and ten drawn ones. + /// + /// **Drawn-but-transparent is not undrawn.** A layer that drew a fully transparent texture has drawn: Truly + /// Bazar's bald hair draws a 4x4 transparent hair texture, and its all-transparent bake is the correct bake + /// and must be stored. So is the case where an alpha wearable hides a whole region (IMG_INVISIBLE). Only the + /// "every layer skipped" case sets this. + /// + /// + /// It matters because such a bake is not blank: the layer set's alpha starts opaque + /// (LLTexLayerSet::render clears to opaque black) and only a mask layer would have carved it, so an undrawn + /// channel encodes as a solid near-black image. A caller that stored it would paint that over the avatar — + /// the defect S1d found on an assetless skirt slot. Callers should not store or apply such a bake. + /// + /// + public bool NothingDrawn { get; init; } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/avatar_lad.xml b/Source/OpenSimNGC.Appearance.Baking/Data/avatar_lad.xml new file mode 100644 index 00000000000..90dc361a5cd --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/Data/avatar_lad.xml @@ -0,0 +1,17504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/aux_base.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/aux_base.tga new file mode 100644 index 0000000000000000000000000000000000000000..dbcaaaf2b192d7dcc184e46e83c8815a26cd80ea GIT binary patch literal 10284 zcmeI&%L#x$38Aqh!HLK2dYgd`*(2}wvo5|WUFBqSjTe71 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/blush_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/blush_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..05be7e7e3d7f37dc76b90bee270e5597e8b2e322 GIT binary patch literal 17352 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 F-veKtt3?0+ literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/body_skingrain.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/body_skingrain.tga new file mode 100644 index 0000000000000000000000000000000000000000..7264baac144fd5d9fe8b4bbb47f97b0c4d5ff430 GIT binary patch literal 262188 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 OxBo_ literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/bump_face_wrinkles.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/bump_face_wrinkles.tga new file mode 100644 index 0000000000000000000000000000000000000000..54bf7a55be3fe90f306b454b18a7d7df18acac8c GIT binary patch literal 25243 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/bump_head_base.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/bump_head_base.tga new file mode 100644 index 0000000000000000000000000000000000000000..fa3568573a4694b28dfce80e28ab6b4bca3a5d48 GIT binary patch literal 105525 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/bump_shirt_wrinkles.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/bump_shirt_wrinkles.tga new file mode 100644 index 0000000000000000000000000000000000000000..9e0d757a48ac8f35985f27d04a66bcc42d585a1d GIT binary patch literal 81501 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 zⅆ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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/bump_upperbody_base.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/bump_upperbody_base.tga new file mode 100644 index 0000000000000000000000000000000000000000..e57d6352e6ad449d80690b82c920a9a33c37f32a GIT binary patch literal 147581 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⋙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

A8rC5&4Z+%clA{*V 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 K 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 QjROHzFYk+}FsNhx4{2mXZU6uP literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/eyebrows_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/eyebrows_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..c363e482e13e1c9437fdb7c11444e4b894aa34b4 GIT binary patch literal 9469 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 N{5uW_garOSfj^K!`tJY$ literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/eyeshadow_inner_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/eyeshadow_inner_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..37d791939578dbacbe6a8768a46431406c5a0854 GIT binary patch literal 5466 zcmeH`%THQq7{)&k5jtMdxSIX}Xn_NAza5I8jr9+(wm25NfM@}0wMn<%LE51x$gPNg zfLKkO)-IY_L@f5SF8H4PuuJ+SvmE8r(kC+)RAfDhTzQ=>}I+i9#e?5?I3gMQ!qoYwFGpesz51 zO%oiZnMa6Gmc=?g^&}8k&cZxvFPBax;(;#Btg>F-ArNAG53hwU5DJ7Mh32|1y7a<7 zEI9@(l41XgrKs<^Mj=8o0aDfTF`=MCKC5JDx&rY;GM(E)yzFu$@MLP-Y9U5xBW5^U zXS%86*HVN4n=exJ83+11H3}$iRN#gmERN~gvj$pjH^PHh)U#l7v^n^a(Gv=6Jka(Sv~aLm zr2BKh*z)RbiP&#Acz^Qg>$fi#@SOOQdH(tAuv9F-9Z-Npb|IIHhCO55YQZUB3)J0X zo^UjoLlZRJVHS#|!?Vw53!XPzeEIhE)5-e-V!x%dySf|;&fTxY!8nfrXd`$8I*TC6 zk_FAR3}&linhcSGX=Y}vaCr3LBRnO3W}TiKz1uGq@>!U=w7e7z%uHBKogMAJfJ^Ni zohIwVOdz_n3{$LZzEIqMcXWbw;3@Othoi&7S_Y<@Q>6K{!)hkWATK0~T%Um^15P-pk&|*BjvR)_?2d(A8S~?zEjD`Y!?~HS7uuH4t{R&!nN^RHRm~+PK4}_wN zv3PooR5z3h>nrK_Vh9GC0t?=%l>t{(y`CYjb~b_X zH*;^&X}4J|wR~;Afy-^xcVf0!ZFc7*k_M5o9%VsN=xlV@ZAfd-s>nMf=@qbn?wBu@ zsqkwC;~i*Xn1?4EPWNP;!)6(RCiWd;mrh#^^g9xgKU=HoB87EB7MsI8$#6O*hRs!> z;To=ziTQ}Tfw<2Blf%Wt5}6v}P)kQ}w~0}u5jayLmr6u@&L2p_7fGaY4S_SNT(^mS z$7q0Bh%?nPi5TM|+H*vE2^b_zO2}w(&10OdQOI2~hE&Ywaz2wIzF6v#(d7y>qiN&> qIjZiyM7ytmivy%T%Al0s{0#^WMBjnbg~+ealzONJs0RM4fqwweISbnW literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/eyeshadow_outer_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/eyeshadow_outer_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..00eef9d9f7cf40ab48ad7e941a99b9f03f0c7eb6 GIT binary patch literal 7382 zcmeI1X;jl!8pdz2j@F*e*k#&jE26cwqg}0ITkTX!tJKz|+7{Qgs8x$%#a)!D+A%bb&W?>~-b=FHdmGUr5(UpUEofA_i1 z`@Anlgq}o?{zRdu#V8c{8&M#lKtzFv0ucow3Pco$C=gK~qCiA}|G5IiXrZ;O!`5YI zbdz5?2H{^JT4XYZf5?O%HXy1Ux8c?(tEDA`O5g)|=t)(5gWlNE+HULWb})OeGlVn< zbz&Rt!0ij$TCFW+QwSBoXLHc+MYT#zL$lf1-r3#bbh#P5lpDE1NQJNibz?i#wWQP5 z-ezqvH8*POLMR`;br&toE2GpWB5BVwIV_pyL<)OzbSmkAg3W8%7r?2&@+tNImJ*(t!r+9LN1Tb-;V<*hyzjmeSYl2 zUN*wq1srxI3zwl%KYF~Rw2W23LzMfmUJui|)YI#BI=VWnCVfMlT*NLd2%&pW`8N9F zg_N|+oZ>2htgg}6YJ<}L{@~ycHN0qOa3C0W~g`1$(h48g%OEws+Y&+FC7UQ*)EHUagSVN`Qs~G(PkcVc~H3 z0+CoMQz#2jR4;kdMSdU}9D*y2qA@)BJlrVY_qgmGmS(L|TvL8On@YWybTWi80gHi_ z?f&LK0&wB1yfTiM=5n}wfuXU<>6zI%YJSPw?DXU~VG9Br?nCg#(OwU*IcPSk#cb3! z0-KU%<5pKz_)v5?knsdUv9wmM1Zy`n8_kw*2ROj7xEFaC-Y1E8!GWRS(ea5%YU-uQ ziSetsP)h+7h#K^Xb2>eJ~HDrr^0oM~45^o8Dd**tH83CV^4^keBU8%L%SXB9klC z8eNkCq$RN3G%V>J99%L$>y1}*@xu}K}7Wc@3y*uJQBUT8Z zG=x815gWHF{)dyvDYr826|y)&5*^kKy9*3FG7b)&n`c6~*XL$|m-q{S!N?%62XMg< z9jz@!nzmLVh88#jScEr z0lO?en*l~Rvj3|uwtf1Db<@f%TX%f(-SIP*eqr3nEv~2$%G3>zp?1hQNTAUPD2ith zv>cv8vv_7DO-z7;{e8d-sY&x1Ak>v|saU|}^rP3H&AaG1%vEk|b|X188MQes=neD?gw3 zK7QBdv3T<%(haLOZr--@+wYDiT}Zjf$jT`wtKy1eDy`l`rlhmi8$N@C`~+ii`9tdt z1o}uc(85~H%?%p00(`*d4WeZ=JP0qjvQFD%vb1%8UCG1*QW_LbqA5I0%}@Zjini4- zl=3)1P*ameB^7Wgiu1D5Z>F3}JbLKsFF)IiKYB#^?%MSmx5Rz5|A!Mv=dYyVjJtV7 z<<&f3)xZO`!bwgyIR{=3dV(>!jP5WJ3E)_WT+r0q+^A90n(~IxOI)6SfY$5u;pgvh z^?KlG28KrH%Q7d?1Rh@v#vd9C2Kv129y@{5s%2tsRarq!7URa%^QVp-{&v^4Eg#_z z!UDJrsSt!K%DbD1f4Oo#=|sYzZ+69fvT^-8Z-qOZiPo%oZPkkSgWn%J znUs9-%C#G5%*;D?bMA-HfhQ5QC$fIp3%=kbN& zTKY03l0=;_kBH_BdOuhLGmVT05@FC~%32YxnpIMelbu2Ra`pU~pN<~hx97{PAAhj! z%{Ad{&p|19t*GV- z=`_~r8k_X#hCejxn;ILmn)*6e&16!E7z-buXMqSpFgyM0G@V$EVgp;X9awOAS@5sUtYo+d8_ajaC=)q|A! zW`mJ3VIwxI2JQx(27(*rOm$^h@%>-3((#Ryi)T-tNH`e3cjxD^AAj&3ScvS3_s}aV zR=vLVt#unV#eR7p;m5?E>7=-mQ&3V~S}o zs8FxbVjZJ_(yaoa$X+IfGg+lYaPE=x#^ebGh0xdnAeCc{xMs^OAuVB~k_yR0M zB1%k&kPr(npL94DEri`sCRb2OtinpHco|d%1M+Lw#N+p}GH~jpvq?W4KXQ2g*SkXK gK2#1NGkW|#zO?kez7#^0^d}?#9~4pG|4o5^0>Ekef&c&j literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/eyewhite.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/eyewhite.tga new file mode 100644 index 0000000000000000000000000000000000000000..a720496988e33e69e4cedc05744dc0b3fb035b5a GIT binary patch literal 42353 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/facehair_chincurtains_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/facehair_chincurtains_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..b10397063c08f084891baae79a24dfacc06f5964 GIT binary patch literal 34610 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ðI&|3gDWO*W)O|_gQh@mf$Evidr59e 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/facehair_soulpatch_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/facehair_soulpatch_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..687091a29f6a83761d1886828a26be70d52b108a GIT binary patch literal 11277 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/freckles_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/freckles_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..a9a4ec073531c19b3588bb7ad9531caf070d2694 GIT binary patch literal 140558 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 z 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/glove_length_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/glove_length_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..db89ad57e7c52a73e8801bd3b0597a596bb59a16 GIT binary patch literal 49745 zcmeHwd5~Roo$v3QbI-lEyYKDW-PuSai6lUvKrEnKKp&0{YAK>6P=HWCX{iN*r6Tj* zl#2R2O%h}Y27#CGSi>fZ22j>YATfE#1Q>Qjc?3-u$RxxhGGa82p7&jT+ga}I+nt8t z)nn$Ljwqea`R8l$+5ZbwSXZgKK|!Qqs6#I_s?FBG;LwDLFd3$(Jz#1yZO`d5 zU?%LPX2IUQ`|LYszx@w<*Fo<-_z*a>`kwc`@BN1z{y{jR`k{aL@JIgf$dAIu)hFPS zaFqHK9Nquv&wTcnV~;!jgcCn^(#fZsdK!E_{K6N%1YZtLKLfrJo%PkPeI340JNujG z{P+Ly&)@nEoEv@jd*4rBxH)hB`4?Px(Zvfcx%5YuU48{D+Gp{VS6zM0wbxz0 zy7|Yq-gd`Ne!BF|yYBwkJ@?*s|AWhxKlJd5m8({-fwizsJpzw5AA9_XC!c!ynP-3b ztLJ|Go9E#LSg(Ey8+tds^ztj4Hg9=#>$dI5Yw-H;&WFc!T&;sg!bcx_9G>X+Wd6eX7k`_065qZ9UQ1qwov=$} z-VO2?k?+0FzA&e1kq;1ZRQLb)fF}ri87li2f!8!}Yv*sK$bXJj{(xuYqZ9dPkXKmb zuy5TcJ9@ZQM&w7n0l;DL?Dp<04?IF$e*&KD@%@`x4;lYvv_09iB7Lw+p^psx9|^kn z_D*(QJN=BWoGGq-H_iNk2Yt_8d87n+h<5IKc8B~I>HVWb-eZM+D7+^YJBK48@^EmZ zLhSGfg>j9sx3WI&^Iw23DvWDA2KzV8hHv(r^G_Jo+Pk=5@s)7ZUS(m86@IMX6LfO_ zJH0>+r#FxC1N6BM@SgX=`yz?!qxt~7?gM-i>#^0Z{L+yvpLg#f5djzf6bqWNl>~ z$sV5Q^*lVhP%e9d|9pC#h7Pf+cZzNi89eOMz?4R!+ac)aEObt3*y26W*No`lq)2j_ z64}R`(tJCg(zy7>*ss1ur!+E z2Xu%lU}w1#b~q_G`4l+SHuIAmT|9GD5kZH~Ry94D#U7%MhqFQ=D9^5alTunN2Z$^!cGBA!yae@`#5>P7bV!u{}IRnr)6 z81|U4&w|;lBKG+GhY?!||4{j`CwwC3`+Ra9PKhML3BUB^)8UMLiNa}vUZ{DK*j1UE zzwUZiQb%qc3t86u0hYWtJt^bI1pd|Zl5)Wh#QFk78N^HGI50_NOggu+*{OURK_rZE}_!aKT zz?KNRGECAt=p_lBmV07aQsrWAi@fElyhM40QlE`d7tXRh@CC3i=H5g*p|^aYyGphu z{7p8}1?!QugpEDakm7z+4*3!;qsmDm!ZrJz|yj;s;|9yIKS)7Zc70F#G z-H9y6j)Wzba#rK14JoBuLl-pJHe?~gp(bQZ*oWuByaCgN)Cx}-PDH#_*4UdFxjzgm zLvBR|>vAa$cOu(^-vW{CV@?{C?llbAs3J zv$gE#hqCB(A6CFW=JGpf!@d}`k^J`bLM!=%ThQE&95wh4aR=J?IuU&{BYMp1!!Ldb z1N=bF;&pmwu+Pk4_Zl=D<9-552|9P6xtKL>J`Y_=x`>n>HmaB4m1@r7cOX}9h&yS& z7qC~T|Bl&h$L!8(y(jqoEOsQVf)ebU+SV;^0~a*8`R=KGPVQZSuRkNy%?s<{MQXnX zFTLCjJT)an>!G2V(5mR^12*|FfY+&g(;qz`_C|7>R>swvyk1DFGNsQ|H3od4q|=gr zoLqffb9NT|#uD&t*{5*Xp`4wMQ;ilLeIs#A+IW!+F@C9X48gv?oYwF zRV{m%-^%7UB}H9Z_(a)OuuVhodo6sTt5NS6{briHu#xVy^!yC^q>&SR%vz5rJwo57 z*(Rs2Y3w@EdDx|vH`uF;{mirAt9?4X(b%!18;$5EkkMmq=QA0^)wh8jVs?XCFGD95 z`hTSJ+2m#7Cg}|t?UX*mYNjZ6IIPc1AYeU*(37Jya5%Q2+cTQPsH3x!i1L)0PRn6Y?d<+?g(gn>Au<52x+PlSVK*pwWxIELq!hqZb04%GikL z8|Lk@BJVNCY09Y1h1{5sSGCAk$@fy27WhL1AycsK0 zcN(&SU0LXq+T?_8=F4VdfPH4kK9?hCNH{D_;Td79oxuBy%hQD>0B#x zitVZa&DhM=`3Ws|WpYv<P(!vxBd>5a17{B!2F2fUu!r=J!3ffJsBf}WYOP@h z^P0b!3C_02YbbCnXIPeblqO|e@6y++vR$l;)nw*orDuJAMo zd@kml0AysUx=4YuVj-X5oR&nf-SXu;T%q>@y2d@c>rbrXH{0+{Nn1^lwkdlgQ^RSr z3qhzcbaRsKqjotd&@*JzXxk0gfDx=J5scRX0)pM=!9EDy9UgoL z&dN6ZRL#P!aBCTk364DuPEaSpNfnxw%t(7myH-P4;O_TF25-Luc?ii^9PEen=-dQD zxsPDl!|2eQuqAFoqi*6C%bKduF@TPUdx6hY^wx_PzQzS}pA|LIjxsUV%7fOkf+e|m z7B$r+y~d$&(3To}45)i`FI~oB#~XCN51P$-Adgv7jXBK8>*U0JNslAu5HZ7vv1u%L zpqp9kM#iO6Lf%fjTRGHd)C0N5wf;pK|1Sl-GfgmFlP?o~H6lU7U`1!+W0tg_R(49N z6Kwci+`a@W!tT{{3w2LndDZTA4Hbk7Fxti{Vt(Z&a)E9k1P5z3HuuswO_9eNwsF^_s8>*qPwnW8&CXzXo5gnQZVfPzh+e{5R!P=vbsN_^_Q`YihouPWN9t-MbW}Mk(ES*5tzH56UbvyF` zmxY&KQ8LYKi_T&#;y`=RyBNmEr{zIU_sg%Tqtx#^#pp@o|-RuI%h2?-JY#yTaYb$vZ@91s?Bt!pwA7+&red8MdgcR^SOba;dp*)m-uJ z8Msw+*|4_Js}#7-Ok)u^%!_4M6$L&5@FChp(#5Ci?ZYDt?`Vz;XJhb!jdeSs3_BE_ zP|1RZIIQ;~Jw6rb@i93ygSo=Ob{uh6n0vhe-A=Zx?u5Gs@|clFYtlgpGrnUntBvqd z&CF#x&_W-Qq<0&%&CM!$saDTwjXUU8aXhtUR%yGZ z9(&vRit>?Bz;#U72(^_&CmK<>O^4(?rM4PVaPky71Gh}P2Cg&VywBQ~HpOy2i;vh9 zrYkw=Y+`8gMhr_jJiu+OKXbR5;07_0{_h};NQSM(slebJa5y8x#W47q(PjhJlWw0c zU05{7hl?lsuwyGX`xve}+J|wt-#KU-l9CNIGfL_uT*)p2S9XC#y5kBr#KIX)k8nGr z1a5oj)(l*@aXOzU%;5ENYgjjE@$#hu2}lvag-w;sTX0C;q#XvY@z1-X&G}VtopcCZ zbq1Un_>@r5dUNvi03UqQEqMX^fK34I!gh^xWiWm`(c;Y-yVDP6M6?3p_^%?^8NM=G zS4Gz+8R}2qr~UQ-rCE}~9`u4}Sl6QioleiYt*H~nM4WVKI+~g@p&pXNRr`iNtDtd`W#OlC8 zJRM6xnw=R_tnGxgDpJjDaTu_(?X4lZq>8@S23mu)pZ9hQR!Wp7tTCR+X)xMqkBK{5 zx50MB)DGR8*~O#$WUUVA&{%l9BGS;F7V3<2RvA*8QiuzNrTg$Gfw?zg8|u2l>d7?F zOSn=5QOF^gs$GAzNa@swcEaNdm6s4>TENV&<`|)tK(*PExL~@mK&F~cCrq`+Emd9Y ziS}0b_OHz*)Xu+`0UeX}796m@l5s|*1JJ{YGSxtJtntz&O-<6Y0C^;Myy2NCg_(f1 z4ADq8S9qyhM5=cFjn~bxsyil_`4qw|*NrUCN#!u%`giL<96$7N_PYgp|RFE)*v2$VYUr8cF7@feA6GhUq838JFwbKN%a$;@(^i z<$8G*;L)tk9W-feq3;KvorAC0ZZJ$PiD z#V9Gl9PzM}<8p$)sgT`~VE?*dBNB&1jUzy{E!m!6BuP^kKY#`AD${pay0m__In=`w zS(4tu{5}0NhybFVq70>AL0f#MInJer#eIB27P18SZoJ5Ect@HhytCiE^ae?KC5OY| zsCL;vR(SRZQ3l%)9yZMI2ph>x)RT&BK0C&xHaZzX=DAW{Rb_Uc<3DU4M%yD~bcw+r zYl`ie;14sJGxzfR3+m-i5t&}ahNByqYb$W7Fyq!XlrE z^;MOvZ422N(**Fk+6m}RhWUO7(V9tY!XL>^ql;(q!d+&^vgEQk-UHMZQ}|{G(q3>< zh~wOFT8JBW@TEP<@bQucB(a>ogbV*0J>9@Vaf`0J8U^R0L+V!irHG9-$7x6eO^q&O zXsJb1W0-`LpwV3zqmwzwO@|p>VymL)sCI3|DoigNp|}vE6`v1n=__G=1>E8r!3E*oDt@ib^6= zC+})V2&XooVrX=~%80Gu3=T&hV0v{1Yh-r$wIi~e)Ve@cGeH~{9Htd=@s-!0%uI$N zqf5rt2p9SgPEU+7qI0y#mw|PO`Wp_Q5+_FmQc)kPgeA)A4G8y$yS8*l@lh?#2FsifthG?xV{Mc1-Q5QE`?D}VX?pf5APPB9L%kr zZgR}3PEX+5Reeb?Bfyvih)><%CCNMCu9<$1o=SKO-r7}ifWtUY({VuO&57jA=|c5K zL^wYNhQ}=d5g)_rl|zBNoC*6!w0aE(2eiW#(2Fh#(bbR8N0cy($HrmS^woEqNeJ94 zKKT<$c=Y`{RlTtXbF2Dx7%Zr1?L=^ZLpIZ0ix#0_(yaZ-FR3gb3bAIE8^oU^7`)F< zf23Yh!_ja&uZ}aJ(M{aGqb-Li4PMQofhrFi9ug4AF}R3wFZci)(W6!K<8(u!kXAA@ zt(n;)K}fHS2rao6?%T_s9+G^Exg{IjC{GSCx;dr%0V4YYx|3maCG&cNI)l^AoGFba zFW^~hrh%0h0_$`*qsi5s#z^vmb3-nC z)G%P_6VK-~3@(9%6+B@}H(cStWSLqH4^1*uC~*(yRCGYdD5g+@RL5O_5VxbROi+Nd z5UrZSxQh|AddUQ%D6ZpwHlf+Vwb=~J{SYJ^81quN>`;2Q5A-8i6~suVZD@$#gcE54 zCpmSp2B@po3_ksSmYSO&yz?)>G>qZt6SWCK1Dwd7M$%(qg>tV<>V<`PuoX)=uqv!W<3Di8D;5iU^ zGy-KrcgW1TSU`tXt2!wG^o}*tG=?QLO&r|@x5FI)B`SDpm}dCko`@(UcraLofE5D9 z)(q0UbJv8cSFIf8KM&CtK>O74)s;GR95X>9P0z#EG=9&VFZmi=J!l5BKR20Mj_7a- zHNz;r!6a5xE639EH9DVHOe8$uo#l3)K-~d9QT{imFpY)_C-xgC{07OIWNol!HLQwO zu2{+6WibCC*a5qQ&gnU%OVn2HQ^+-!3V#WhI*50hq{Of{%0^O%x>X1`ZK7DM{TSb5 zeV}Z>*M~6sG3eTORrg&p3X%I7!9D1jBXZ5KoEC5tBB!qb_%(2Bd#zdntCE$;Duw?^ zzg7!Oa4{aqna--sC4`}(D)|PrMO#Kd18{ZO4@V1!sd%>xFA?KQc>(1Ra6mvL74wbL zG9&<9u(I}nr`OHpE``-r;d}5^7y2GeYai^SVS&y47Ac%qkA*$P6P3n3jClli;^<6( z*SPNq?!}YT6>eL~#{h<|)+Y3p+PXEX&*P6n`{VFvfX6_dj4_Hm_xx`Gr^I6U7{X*x zXim}_@mt7(DQ)LeI9r4Xa_Odz#@T6UT+V+4?nXf8k5S`vkt@%_x3Id=;al)dQdJLJ zz>Rg?xW%<`v=Y2ajR_Xqqnutult;>Ukk?1x(aK|wu3fWw7{3K7$l9v7izneJ*b#2u zx*1-9mlZbbBf4rQ;XF;hpWcGs!rUo0%eG`9s26A=bfQ{+3(QcgUPPo9aQGCt^+dU| zrx<7@aAJk`i%ayPn{N@3*Mw`Tx+exVs~^Ly6($@K?KSyRglHnLtis$o$XgtI)3BGh z>0hFkB4Z&jPMN)*f^z#Y9A3eWllblR?}-G#Og z`Vxo1;qbv8Q$>6nvzZ>(@`V$tvTBWo1SV3Tw=To^&sNq%7I28CAeixQ5QD6i!vWumuwXQh-KA z0bu`_RDc2}<#G7n7;(FBMBV17a8$1Yj@Jb2y^MCi2lBviSt}CYc%S~NnwdX>o9eoW zMsZU+23&OrIc`>%vWJxcK18(EuGmvytm7mNi-t_EX`%+-R@@P6+e+c`ZiFxTUl4pV z10T~LdmF|>KhfNCbe@q7Ai!5x%!%@v5j#+y-^pF4KMhm$bK(G(RrRnLT+{1yh2gdk z>5XJ*5BmZxN#MZ{p~J(yUegrt0V3-9Q;tj8@7R3q$PlrZr1z?S*VXr=$;MZc*oyJ;VbN_QaWYtJx!jHR}EhYI>8QX69Dl`i32G5LTaR%!o!h z;68Yu!VEF2n9O-0xjCeFgiF;0SwO8Q>MPnckMmytE54mds7cccqP7S9>Xt3GCvo6C z13I7A55}UiE91c{oap+`j}$?-a-h+|drgXnkm079(ep&6gYB^FLzoA)EMVqXpZz5~ z7jtI{<(!!U2zKy($6wNiEo9lig~6EfA2jZ&)^3&x~SkKs0TgNI@G}(3e{U-(C=~2kHEtbDL1Sc@;a+B6F}O{f)0o{cViyI zJO7-nK-^;0u_!nUDRY6pZp|9@33IGhK#%EpJ*y*;6ryKG%w*+pUYOT>o;TQ1uP_0V^T%PRSCWVAX?kAbL<5q<>3-p7;FunFAqGw>0PjlGBf<^WW`I)|1005L z2ra0mOJUW3t#0%J*KgqYkjV*6Qg^tp$M%uXuRHL)W(t#M*xEs-ecYBr zGT+ck#}gfPk$Z@F8w|Lkm1W@FLBDYpytHHJummwvlD)M9Z;mpXu{KUzV6D0O<>uPR zxw~l#+kx`MF3|O6ujEmyzq-SCn>u-qs}K6wgWF1$ivVI!uTtbF=5|w8m@XHf0@O@ipD6U=d1lf3N} z(9^hiQy*X86%K50Q7j}MTs}3w7~-ZG;O?5~`Y?~PVv3i)Y5y*1x}JpdJRB`MIp892 zXmf^IUc8TluyAHRzQCioNYKKCHPb1FD_SO9#{nGna8#k_m!zm21o!s1?gFfyXb4QR zCBm~f=K6R6`mXdLMKTxYwDnYahdMys$|kVxH18XYePSNFen!f_!Oc}E=dKfkqmv?q zH%7QLD}*%>x*#{44Ewjt`x08+-xTEiBy~4KfMIQdad3VX2TR+yHx6nq0{>kLH0uL_)5Kqy_LgmvY9sET~w17LZu)t zNL>cgsXQg@#?&M7aWE8OE(cTW^HF?u9a$&_{zjR45qRAIzX5KnP`O6PDTi%?+#LMl z;RGI)q4fhn4x3~1f|QTk=zG!9VVZ7oGZP8>M#R9ufpAcSr(2PT>ZgXHJ;S}5nH;hb zIfc(-ztv#p?Oj)}A@w%75ZPo*DACY8Cnt5wf3naCI=%W{gs!H+o_xmCz5yN40axr4 zV;h`${8*>I4<8R3KlFKXZ;RftkM{=;V4EW3XMrbq<%=%cNa+p!kYmE`sV1&$TPOQc!=9 zzOj6cbRo_Uzy#uIl=FIT4tkiE-+*l=eL^1*M+NuSZB7eobeN~1B|q3{w?~_j)Oo>9 zwHQC4-{?g0MY)j8wm2_~X0x~gW1oK1i3?L1hN!*5f1;^@@R z;GRaljFSlx$HjVS4s7x_?1pidh0xRRXKAcYrfSbejThKkC%1X@E$jMp4{Ge-3yF>o zzTpokqvdxbF1$R)h`tDY0}Ecp6bfmMb^Dndufp9I@%uflF$*8Ajt+d0wCWu2M+e?r ztmp0Ks9@oQd2d2Q; zW#K0{WV=j>>WkI8c%MjyZ{j`7ig=88j>^2j5u)D1`_{0kfM{wy2V3EE4;~pX4X9DW_bnf$(9;ZD+$ApE$-=K{v*)4JQH$Qwd>Jfj z;gom>^hWfU=R1;~l08O0C6nxCR;tS^-^kuXduZqiC!p#MpJylmA9A8^-XWmC<@m^s-lae71IM$kkLA>h<{ioPc;s%ifIvJDl!~ zgqkOo9Cnnup>nXQZ-c#Nuw#~oi4Kn$!kTv1YdV2L4p&Sozg4scrCzkXWvweQ1 z9dtOl(nXuXys-r5`p|uzz72G%@jQhU5%dnHiaJ4;@=k{FDm-=t!e`hVhr@h`U09gY zR^?;qxa&uU-c<0FG0XjTUTK$!bRMMvd%^6OJJ6``USn>8F?SrS@vU8NGL>|vT5h(6 zq8y!k8MWAHw&Z73s_I$pV~4&$;mp1pL#1crFtN{gI+)3Vx8S{5vo_#EUcR6QN{YR? zfL49`>`SR7@GrbbPo}mvc!pm?_{`kGL%R7sdW&x(c||5~JUw8JgYQil*)x32=?rB! z9jt8F(|hpix;1GpX_UivagK?N?(p{;{4`>RQ4ZGH^Hr`FZKn?u&eZw&y#zWRze{pp za)MQ@6*Rb71o&Fpd#kPCvkVIS;^m9Bh( z9`gwf!$KF1s8Rxy{{%h83(&l!8N%zJ1AV>|fFCK^%~))1qLqXJxTj_ge!z-}nZ|Wq z2PFB;u)A3ukk$rM+e$M%IX~EI(g5VR;GjN-pN36w36Ak|4qz6=>UJ;-PBrT}9pDdI z_}T;(;qF0IU#`gnLEOrrQTrZ)o|ye1udkt*eS2uRBYJtn3L2aOsH_h#R0qFX#;;Fy z4ZdtDtpoT*20Y%7r#00DPSe=ESnOH7r-mM7{$^?cR!%76 zIE?En5&ZDZ|HKwH5nR;96}4BiTt@_FFcGsq%kjX>y77Ei_w@5%eOv6AeFC#uF*5c^ z8M)gVrf_YDH%0Pd&p^-VyqU|Qu!nEAMH_HFsnE|M=o_W#NpMJ~D0MrcgkjI7eI>{# zOS^iZed%6w-eXiM)gNjnuSC9nyv^G+wS?i3t zj{|ZV&)S7@H{I)k%F8;L9@eRftz(3LhwRWeQ_s>HaJ0gEANuK4_e!H|=i_;Wuk>rN zyIFI=UY6wQRYMOv&1?vG{}CKmRi5ItHFlXzTfyKd#EGYamP_(XCue%yY>*-L>7B42 z^(n#5_dxiLi+O)FmbEE36TaFOk<3nssh`7+dXaJ|Y3o}UY8_}H`gy$kLm zVwXWB!Y;m!v72`)-J?%7u)lkf7u!6c+rqwXGrIB@L;QnLZGj`3X?F;9WzQ>9^h`<~ zcZKU3z;(&GeL}p0t&`CIjOzt5m6znho1Q`qrs1|+E4NvTlPdV;u5H$$dxwt&v_+#! zI<9CcOz9ZxnIr6FTT^a-$OxTQD{#<0^sSv0-ao<4eh!5mOHl_;^hmqjRCR5_n*sj{ z=QTcfH+9{w7fXcD!Z97)r_Vq?@}pVkMCZT>Atp3%vc7PfgI?CLGp%|9ok(mstO++x zlctq_y_zc-p|ESZn8m%&T36W|ZCCt4ZQ@qFv2=3r4ma%C=kM0TL59Y1lSvfMWv@a@ zJUj+pq0Pe1ok#Ov)1fVuz;IFIJv^AkyEJgqJLr;_y_Dl6azf&n2Zwn&L&Po5Kb6LG zfh`MTbP7U@90npB(3nVl3z4?DC_t;RIRke|jK<6_*^!c4NoV&W4dUe7h)xQOjF*nW zwa2R$fS-b=tGZ(Cn7RHLr7T;40yn!O-k{{gf`x-7`P0)uzK9tmtw;{5C zX~`EiAN6pdL#(ygJP%KAD*K4}R7(aicH7N~q^4ma7wD%rVeuZErI;CVU!iHZ43_nZ z$L0zg9>rIG^f2)V4qNcP%40UeI#x}VqX$!_;w_LO);<)k3{oQH$WA(uEijUOwxZV@9CyQiYWsUG}Quu7g1oqG`ga2_9S2U9cfUbdqN-nkHQx z3VZ3S(zOcEpGMU1Onjm6el;D*t6;QagEb4@msEJzNX>)u zd+i_w|0N}Nzn)Hl$Z9a&JHQWJWrl(-KEUz_$Bi+GbMF^O5(mEyh@0;a)?Z8i1@Cgr z$G;^+kEp7>;uGTg0`eCM=A920s0-m@S@Ds*ba$l0{f%e}GN$sG{z|dQD+hFb<#rxm zVBKDxzyfba1MIzn-@GViTNrftPBkC2#}Q7JQNxB~zDsi~Jd?+F8SzmTMWvG%P$H zys#c#RPBEMj`R`y{m-l6c@h0$xVXvPTC-SYey=`-OTX8{?S5}rtAcB}Hn7dFP3D0L zaVQ2Z?)QE#<B4%Q7n_s&X3g)a>GTc zs>w%qC^oIXmFzbbJB(g1G5HcTwdQ!M5~Y@h_Gt^v-MOa8E;y`|{T! zo7JjGr#iT)2%GU(I^L<7uePR-^Z)WXMOhBsJ4k#jre6bFE>fG*C-}d0qf+=^5Iy70 zOtLB2yg+SApX7gxclg#1vl8lmc&U0heTx6lohsRMnR+RG+WcwrV<~XMQ9vCPw^F0RRTHBHnyq5ud(PJz ziy&xnZ#I)_)EGyj;AFeTF}aCxG{z(q_l$`VE}9q}_sray`R4a~p67kfIbVO>5T~YU z{xDT06>06~_dff2qNptj|EZ7aOHl>?*;zT@-M8O-{jY!d^DBFH?|S*AKfd_;=bwA_ znWumEn_vC%(MNvv)1N&2(2pPdmmmJ%``>#Y-I?x~djEajz31+`zJ2E%-}>gQU;U?B zwr$xs*x#GF8@}-Q&wcjOpSsRnJ8%8EYd-PuD=z!!1?QZ3+NsM=JZ|aYg-0KC#Nmg{ zKXl%~2hKfU){N=Xrgl%BIAMI}xUub{N42_^q%;seNMim!p=L1t7o#SA&)&xEness% zXH)IRS@U;2&c^tM{&9?)S6|ulL8uW{B^j&cgH=}xt5%=6Y9CnD*4mPmHpV{))V%Z7 z8xR~&v-^VzPBk@GQE-Hs6GU*1JYvC6!RZpg0X5~)=J-DkrRIaYjc_V)L5vUO+mM>` z12xAUJt8#@=!SzG&5ONCJ9K8`2nkfK&~jq89`wLz`6l=|cE z7RY&xyxR40j+`fdxv%8(rMcG;$-(OkC1+kn&J-#+$r(d>l9uhIzW6)C(er{D9iiu6 z_iA+TJRgLfNuZ~*W314Vw%k(M7Jp?ZJ-g+7JUvf5_KTnI13eoC3iM1=Be*8_J_|*9 z4t4W71HYzBHiEz~*Os(y0LyNQ|ItWle~;=VEHjeabDcEIPwo>|p(c)ZU5(>9#IWComI1uiO?#v3SC6ZE9nrfEp=+)Q z79Gf{Q%{!nI;J*VuB(BRx%Ont#?k4hL`drcX4PzmV3OxWLLeM%pD8 z!tt*#(vC9weGnY~%o)>rrt-<$_^8WuM&n$E8#~zE3yvm2UeeMF^0pXxvvcyl>rXHJ z;YD)vnWuh>z(j49k>?&rY6T`+H$(pO^c@)JJNK+w^vzcKAZioFC!o)bD|ZZxO~F~x zNBwl5Z`q70>s>?|&~(-*>XWX$XC8{VWDfYcIJ-={p{Q(=oaCBYo4RLKWP^ z2v3-HrJV!g(vAciCW%y52P<0!i8qe;gft-}QOsHuiTB<;5{bs*wU=IW-r1+GJmn-K zQDeJ<=FXYT;I3y%H)(Ve((!{`<<10b_B2MIF#?S#*u2?joKwwaAAf>IGrgdZV(8ht zFEn1c_R@>bR~nb`8IL@C0pia?4mxm-$svezlah($2?OKHU^c0A@{)D5#Y*J1+C=7K zot!OJBGa9T1j!>4Ia_2=h=ei5LoQpg2>$%=1&4u1nmv0-*X<^kCzU7mPe{OU%A_Z= zL4AA~4x{Ev7KseDxi*nsXCyyFAd5sI=nx_^i3GbvB;AyxyLWOQ$aIj)q$>$zHj1Zp z;P>9Cqz9?C)b^D3eCICa8%ibJT3)T)`q@w$eM&~LCmgqA5%ordQW&me_Dt=YQts{p z*D0GBoe{WRKV}qkCxY&5-P>zE_1j+$wcN>$=ze$K>7OjC8sK#Sxyk0Z(iKL4un} zu;e9aX;W~b)%5zf?RURH;HClPu=L)%ZA*}b>rs3Fz0>);M(-9b)BP7g>xassJHrbqI~byJ{n^%9>r*xD9}Hu=O5;Js1&%u~Pnt%aHoKUACI zuYLJTH7K3}imwh7uRLY>iN_zeY>9{j-GQ55cj!EKh!e7txyhUaWIK@UW;ncbWU|p{ zQA=bG?FD1wa1Z^GKD3YxdnS>4O|ozO+Lyn0^G!n@x?9NpI0fU}vrb1OikF6HDBVZ6 z1$BqbcgS@}d2&#K2PfqYl+$W>%H5PE$_T@)Sc2#=qs_K$2tjItXgr(@&e(9FK~xN^An*w5y}%tcLd7oW*OmB$7Y5& zpqpY+f|!UR!g~zv6WOrIOq6oZy;Q8+x22|5P4;I#^~vk5HL8)0fa=p$o^tZ?6a9N1 zbM#S19znXD(48DYx>dv*=ul{+-Lynh(w^IOmE^g-lnjJyr6se@Zg=;~A?lXr9!mG( z9NqtP3!ZzU=A1Wt{`$`r>Avv1)n}fza)l8N0bjHbO6iWQJK}H$+7n1i0%=K1JsZUr z=1GS&VdCJrT1dKo@cv;a&+-MNroYxo=CChL$~SKulnlO#a>)0E=bdvFp8V8PPCkim zPICg%@#yXd7if3$64TLU_(r;M%6PEcVpCc)QS^`gfk{^%Lt$Y+w0&z8>1woTQr3L* zqVvx^`^?kz-6`kGprJ~=J2DdT6NrshR9ePHkgqb6KxRr)P0%6dO)Lp`2iX^Iv+%tr zF}2ZVSy~6;=iM(W*lA~t;(j9nx1W!i)%?O$U{Z{|>7G}*Q7ejQ; zRsg4-yxgQ`$zr#t9s=Z!jv)SILL|ByiTt~Rn1h-x zauZsuRN^(DcNpa@B48SbHbjQN2iwS0@A%d?=$I{X7lUr7|bctKsC_3$qs)r-UbE3p( zGRKIHpw7YkBedxsK1R@ziv+t`bIfz0C(zFr0mi?I{%hTOx2~Jv*cEFpy95BvI~VFq zHqe-wFh$GU(mLuytVY!XpiPRP(NG6(kdz5c$^_Td%=S?*Nd3b_z#(7|CNQM@$-_B& zL=fm_feGklNx)$G3?_W$)9}0s;%agMC@#M6g7eO)Lg9{&jx$D-%r)duv@k|yX+&*O z(o^Juda|^%A>QZb|Je`$!F3un2P~03=AaOFNDcPZfY<{ZvGl;KG%cQ;et# z9>5AUOCth^;FXJVM6b4=se(pr64fW@3~dkyJ1TSD9|41eZ5{)QW62J1fkyRC?I~7) zVEmJ;uUmJGyLz%`1GRy+Riw#GlfsCg*Dc;fXM7-xX5@lim@?-u3C8t|sM+uwVvtzTsxh4`iXSlaKlIuSw5mB-}TIa6m218(+)~G}kpuj-66YAjWpcyUR zT^|WQQO9h+9oovAHmBB_Z0#rUXcl2WuWyX|awy*Ptptcvh9Vb6|M)@HddUwb)r={u zU=?s^izDE0>!QFCl`JTDjwb>ng2SMW7m(27(P_s51@V%2JL>m-v{1CMUa(l|NKoo1 z8f;sC_;pvTg=(z*JGyVXNT1 z3moxYgG$m&91_NQ_ua;q_`0+gX(f*7C-kMoLw0`&l z_q|AD%Uj$#SaXo1VKcLa42XlxO`sz^YP8l~Myo}AuYv+M9t>|YQheq?KALI~C}MS) zL{Qr)S`tSUMQdB^0+e^O+0TBqik3hbmxtoBHfjk(3swuI$qY%TM|hT`>!u4!t|Ut! zTNgsMY5=J_4mx77tq(@rVRiH21xu{A>3Hu0;*ctkIKj=p zAyXYev{Eo+JiXwRmy;nPTMFd#)7+{O!$ZkQO-W)1moY>G5|gbgH;Q|TDk42=Sgq|b z*|DQiLMAF>DX+Rc@jkF4LS3^5@f5rWafmbD3LKJMphsmYLLB>x@dZN|l~73ph`18B zA~qnfzp-UYg*aU#OLbk-QuCkQZHi39vUHH zeMbT!#ik(!=J-b61_ zGB4sw9VJSZWlULdCaIFF*D&zp_OzH}p|x1$Lm27)@cT4cPE=h*i~bOWC2}NKt?Ny+ zde_xmbG7?K3v}d)wZ@Vb6)wgpZdHpVQOLvH(pX))FC#GX@b=eY#nQBzDv|-|e4mOA zAeq4u92^qJs$*h&(LM7pteVdHn1|6%SUNy!%KN)T2!7=Cx)~I|?~t!HHaeg+Ng>JVRh3bjpa~ z5txR11Pn?70-7oe(gi;BLBS`A3#{UU1ifx$i-f&W)NxB-Z!-jxr3Ni^OEn5TB{wav z70ou2!^u03VO%k=(#juO9h^AH4^e28=!6Y23W#FsH@?PN4%ndfHUrVWZeorNIkzDS z(W(lOVNjouSQ0Ch@7N-e8E$$@P>4d=HN()Fi`ISlQszL0VDOqvPUt+NI6sNlGZ3HE zu&O`!KGSROGEkc`t(Nk)w@>2u2*T?_N5}x74s?4M`?1JS-QG|Le}O2u-94zMvE{_U zhfJqM8MKj`V}rOo3#i1#QkFi;6gF9J2#^C_0ryR>=fjX30yZAZ!sZZ24AQ3BAgyyo zg@nL?9I3@tLuh&bUApeq0Kx6)f`NrTSRo;pCHHXjWA|W_f=G5w+rd&uaNW$CHV=n^ zcFD(t@Z3x|GA`^phOq^WW=Sfi45jyd^xIa$qqA3{qr4c_3evIf93q zdcSc%a&Q1(%)x5D0bo=<07dAMm_rr8VyFKn2wI825b~1 zg4@}X_iQ=;Y)q(uKKAk}dXV7KwwwfVv1uUhxRnf{?^rO)dK?Q~g&`b)D>wzUL zCf1n*5kMGXb-SZo?&Zm*$3)-sy zWm*ueaHp0$2IOu{GQl7*wzeojfGRGTByu_Vxl4s)C{qv+-Ot2HG&GrQJx9a0J3>#4`2r4x-f!= zBtb$70U!>|Qnvneo#X*a`%)TLSHO5;cmOupKs9c*aYEGtsre0!>~6BcOce$S(*A0V zo)cXtfIuBKpFT^Tp!H;?osuVLe}ZnAnL_8)Ec^NLWhO~h&;TEN&Ix z#)hL*0VF`R7~4m8Tdhu31*1D>Q>oDbMU5gr4HPh{5GLp}Q3QQpeJM+v!HcXc0~At9 zrb_FuhC7r_O_;%)a|W4_u$JMQb*uq+6kDutIUEXlHjhF^G`wSwqRc%EQ7vZ^rDlM{ za$c1P00g4lsJ9gj@wfRbwO+CRAX??YC*j zdJ{^P#Yq&ZMp>BACCo6X@*2KUP{eGfk{NDEoB2E95cPHkmr&-@v1jVHPA_R6JHj|| zBZg^M&Twc!YTK&{hlWkV;XqAaVSGnnR8?L*GYG?K$ZULBD^()zGtdwQis%$~@_21f z1a63Kprpjv4ne^#s^~Wd44ZtlQZ(GW_Fz0%iy6a#uvRd=_D5I>01DPKJPI|o?6Gh< zh23C+n7{&Z=%q@B2+8Kdv35jFO6ZHl5orG zbAuA2tXJfLP&qi{AyTE@k7DKdp+GFB-YXDwe`2cgK&ax<6o$do=~U@$OQ#41SG)6= zFi5T;j{U{`Nm{KY_l(p1n1OXuWm2L^8uSQgn?yfEu;9hDQ6z$uF+?|1s2mgSTK-^W6P~p(}v1CT9Z^+=BPT#PZ z8bPw!!l;25tSAT?_4OI@$2CIZH1+Tzc1n*$OWisi-Fim3_G zY}l+2lc<)kRiAvK?y8Twk4=z%kv6{6W?=}2HaF|myT!4`;tp>Z&TQpJ(F%&Pv9{H6 zyCzB1hP5+RJ#@K%ak_RV4%jI!#8N%0*;vgk$} z3^wY?5x1zx4+gTMs?N~6^q}&_6*jr6hKxBtQPTjsT{?+riFcTLRVWWYzGgwbI6onl z3&Ac2q9j|W+;2xh-@Aji1!fbvAT+~2(QYhTfU9I5Qzz*6S5>m_4ySJs0a zZq4M&F1>^tF&xknQF-qP$&qNGJGOzKVqh%tfmLs2X<4RnDFv-dT$<+x7 zWN`)rv&-JWqpSLWImIjq5xD}6PhEQrVIYx(JS7*o3*7~yeGa9(a3?oPc;%KgO3sLA1J6@U`CVDTb%ezNJ!++s(9#WWQs zr9Z1Ull~*5b3yuZxva++|H_n34sJhjY&4HxgmDME}%wgu`-VGzfmapZO# zA+^pIUwG~r*4faoz?>R44g&EQ7!1Vsr8j);)7M>th5qD{MxotZ*m%MD=vHx8s;@hm zoODOalarEeXJk^6cS5!~FSY`()kxe86ptk35e+XK<4>gdSLD|?dN zj6IH*0`2qAV0gJ-{2XJy6c~uTO=*H=b8mx2GgeS8zF_qkC!er%;ZY0b9eBV@bl4mo zybO{zPHyhj%_=u9J)~dC1Ug@hMU*;?l5`y`_7zMqHU4!Agz7X3yK-(<1gm)*O5u22 zk&Q=wW>bJdC}IjpO3loG(Ca^a?KL0292==vCSQL1l4FlN>=2<8TU`!|lUpXo-BYIa z%$PN2E>>j@N)ApDUz;7Fi<&_vrG#>WL)5VFqBmzR8hURalrMUce^H$X_$j0S0g%!u z3aEVsL_U4+P7ngndC_^YlzQCaV~&`A@Z8xmuyxFB*}jQ4PKqYG?ijRApE+yx0oePR zi%pY*B5a&QV1FGZ`ND#a(mR%&!Z6QgmT$XFCPL>}-`@C2J!sCX8Q3wGCC`Bg(Zmh-3k#o<8$t5485lF1H5pF!_#toAMe8@&pvFQ_m4|E zBM^)&%83n=A;GSvVcPT=GqIjFJHiG-B*dmY33GKNHk8=2m-W=1iAI4)g0x$u%_NQk zo9h4ilZW(K)n_1Bm&saf?Fq40%0(S6QZHS2^bv>6J814~7y=|ex+~u+&ewJHk1co9 zjqAi^gz*#j#?4eakemiuQ_$Kdw7T&Poab_5{Svb)-4dSmOqBvTrlL(frImK}p$8$d z_tU>295)lo`WiY&h&Had%4j`x`SD8^9Rpeq=1rs=u?e1*F^?OYw3o*u?a>(5UN^SG zjjQkMf^alXoHV%`RJ-Z^eMz(H))he4Ro9u1my*)WVVLH9IT5sxG%-Se;UD<^ z?^0~7619aVXgC)np#_M(5N8Rv1itu~BNrTc@PTt?&xC)FC$3{IQ$~1G$8!@7=OzNp zCD4qg7U7_haaEW`uOUlIbDOLV)M{S3qjIjae_wrNw{EgqVV13Tk%pc3-+MRhmk0wK zuy(2?F2xI9a^bmWpT6Q`5PR%V)H1d^L9K`{*9{X8>pJS;liX;Y#wd?+qjABb-Hna# z(9NV70=)sQI|8v0#7Hpe$fe)Ar9gOUSMw5*5Q|v{|FEphP|v=%^S*m?(y$33w6+tk zTYvRcSFXL}!t>5PVM{Yud$4xu^?Gp!T)7Eku&OLx)*B*C_;}cJU zT^_{C!amE+IRBxc)cX1S^GNKTU5GM&FL@a1_HvEnAEvd=|D9zX=$sg~ptio|J5=(b0oXz91LiuuoB_k>fO&H1LgEAVXE^gg6e_9(mxZzD zY+%9(6=ZgvF^VROkV-c3u50^cfGOvhg7{@@(vfBiNlNAT8-k@t#=Bm3022KsULasuq=G9D9P)>X)9^#Ya6{=bU47=J zys$eS!i*oib}nK95a_wfdNMPAh zDpZ-;^G1XmIbm+g-f?SgafivGw)xzYEJHwt%oLkP3W$kTj4?zyCdGlUdNB^vm~#Bj zis(Jtzi_WQ%yBa7b&xRkk^>$xCwU>ldQ9}JdnN*;20;3?d-6a0>X(l{N)KWh1WU%4 zT>{}?jTuyuRf=m(FlWTYqmSH50x&Dq>ckx_1NSY+FV-Y#Y+Ue22X%?al2K*oQi@A) zYPVgf*W>aNRYav+a({||N<}Vu!O7B39r+0nfjzq?`S_O zCT}KA2nRoWb)8kvx_R0h%xnfZ)-Ep97Xmc<%Qn!W^%4Lh9Q0(lmnGVr|(OZ3{ z-Y-G>+_si zt|kHk%rgNz`U|69YIazi>G5TE)3#-nJrh-1D(EVg&G0{;jGB+ zQ54r-)f{je*{V$1x8cdDTjH(oM+ER*r4*`SDo|uY?5EP*)16O2FMjpJV1 z!ydW}^6}aO>9-M?b63l`6KwAq0QL!zs!I$bY-y z-fd<4WlPAkyYA(+GF8ucXkHkgi$@F(*CDG{KlpwipNdTK*$})L^mEAs^)#sRXE>7~ z^mD!4hDtESAop5C6UYLGeofXXh^(PGD(5tn3mjxE$`Qoq?)cjZ;GGB|aQud59>H_0 z@lqt~8IqaO`FRFFf%X0G_||RTfCi8O`Q~gMfwui+^z9t9o-l_ICP1YlOAz&;l}YQQ ze&62~P+b_U9e)O9nPIvS!+ZZ2-z7kzx0AO@Z?e>;lH_)sAZ1tTvoPYxCu|l&P@v~T zGlW@6&i9BE85?Y*6+&$w9 z0)rDign^wUZaHX%h>;S`wW>UE&4kn40R~T?*sz%I_q&D7x^-nDRpdufgn_%m+EXTs zw6ZO}2Qc2Mq;I-6nt{Tr_3>baZ3!T+_2mPEQ$eGHK#57U?NG&FgNPjPnXXzr1ckN8 z$qpBq{kVbRTjZnkN}9y{tTt1s`~jC1I}9w%rvxziDx2c_1V#cF?)65&K^sf`UMlS~ znZMvnrBs+CfZ~v`7pgn&fKbslh-$Md3ub79D8t?nMtpj#VeY45B?c$sxx=)jiv!6-Oy4 zPo~=(^yW6Z=mq787eufG&H*JZ{)k8k0&Z+bk6appZNE9bw z2tuZ;bXZ|O&hzBX3Wz-AhI5ceqCnBea;qs?Cgf<7Rn2*!zl20b-lKpl9qpU#eBlrx1BHb25$K2Gk1Sp|P#%`Hu2 zlBY>6lv>)>j2DtkQJjW3@h}+wq85r~qJTKL=ZEpmobBB&9RVjzBWC<=$lI*yH%z~BT2F4&hjM6qh z5Ga5_z9^W$WEC!Z5>xd+i4XfppYMP{V@HV2O!HJi_KYesXPCjcp~yH@id)7UN_z50pEP^4`0s;H)_1Igz)uxfq0s(I{DxyL>F|l*WVJgO3a& zOZw!#hG|B7A#e3QBZD!f3Wj@rk%UrNdXbx3tp#HP?G)Dsbmt7-yA;&It!>Ylsr(wR z=6sApGS8RDu%wvV*eizCtEB`IB6Ae0^~A<4^6evv)P7YcU~Ka)LlZr-QdulT^;X9R zE5#uUpiu1=V>=lFrB@Ni39l7;W)WLwO&IY;aL$-;7!f|b36OZCuaXCc_%rJ-HMJo2Y`Fr8U z-+?1TNxKZOR zZ95ZhOO-U^tJaLPV5O>2WLwIG+*@re=YYIwtQZ-F)HXGe@?NiiLef?%e`xJM<;n8y zOu4;06|`bsF~nZGF>~XSjJ<>K+AYbR4{I%H9To($Bp*dprL6hA*w~i{lchxvoUIpk z;ngDJL>`6II;AC+v0`OSRuoWFxeF~tS&=-?3Zl>igH;?l?#yr7QC`G?zLrA+RW68A z6^d+1Ccoz(BQlckd0xC(>3!q!S}1~r$|c@eHHsF>ue>AOQ{A>?&lk%zgrbeLjSat{ z+{~>etAp$V2pyQoStv})(02#jCWb2t7+7c)pJ&C*;W22UWGO3`Fg0PdhJ}@)SZEdu zww%nnSiv@xr3TW#;(~HBGP}MdJj*F32Pj5nSn~iF zZ%A=gcMR${{@mc@Xq#krL@+w93+T!}zuM;$zQ~{L!I23C1ly&oSwO&$NrDZz$f@N_ z1h1&?yHjHifL66%5pX_)Qd9#$qKEE^OA@-jyG>H;Dh@EML-;yXI6!T-h$+(psgJum z56_DHLjh<9_)09pL5Tc??V9vwsSuZb913XlD39m2+mmT6awATXYd>Zx2K6Q>b<_sH zIUI}m&bKC!-7`X*3Bc( zaBpk;kfjhgB`_99=*>h(Y!Q2?3|id-%X|uv@3W8v{@8<-Vtv>xO<)XlFhB|`h|V9> z9?VpLdwWX$DD6+^z)H~$$uNDk47w#$&4$uX4z#n_ly9-%>hUb;V{2_+IN)8g0Z?Ol zty?cwP`Ocw_6nyt3;+)b)H4Z{1a1(%0Idn>o=Psw85Nx;uyr@h8gg1}n?imM^xH?43Y-65gS;^)cS`szE->WO@0? z%9cE};mfZ>8Q`8h!#sg46^CM9<_Yp+D6EpP(TEyZ*7W5N1gEwz!y#WOI8j6=f%PQq z>|HnM+bi*f6P2>4rOk+vkFYURn~l_NjD3r)7WMyeB0k=^O&XD*pt0h;AWW0`*n8L!UijKvk8$)|;DD@Pb)1}fXU+uyS zNAdBEd|v$4+lW$X<>QM7vuYA1YA?nIj`Tx8N__iprKoZYdvPxb2x zp)lzyFj@1VHtj1_?1SKhD2S$0lYVKTNNbUbi*eIjFe(?MFiXa%W;8=jI#C7QA(`o4 zoMv|c0_ApxPgPbglyj-8W^y?Y|JM;!c3joF3+--aGje^}eRt^${_s0fl@x@B7r#oz!fj{eBn(MnG>nHXI9BB_-j*EMi=dL0F~Nt7!Z9vC zT}4TexzF5tYh2kIKe-pOHC-P{w)N659nC7WYNwGM-kW5rgs(c)&lI|akr3^7_xnI} zD#FdfJiKS)Z@>(BYhSS{$G;Y$9abWON!%?730~Vhd$g57B^)k3cA#2Lp%0-NbrM7} zp6s*unP8hpEiG>5Q_w=NZXI?F-2@A6g4k zAQ`p<8i^@dkj!CV&Q5TMwqC~UqeE}R4U|N_GS(Olx+uM4FgfU_o3l6mjl8iDtaC-V zH$GAc*00_Ud1LSKhe`&DoAQw~{a7@1%4HLXL%%ZX;f`qFaU&UBP7W>~v;nK+a#6s# zcfDB1RsBie6j$C`+eIi$8B}D)9YStt%dE6eSPioebK_IZSgg`L3VmGeylloId73SfJE4{H0o1kPMkHcs2vHPu7vYz!}9rFe7M_=IC8j3!XkcZIGNs$djqMh!Ru28n%g|hnida;F^1xS z=KVmkXx^@u^~+!G*)}2BzS!;_YR`K<3-R(K^W1<}JtcXD%0^H<7>Xbo^3t>n2}bk! zrl%lUSBMyoa&N5;A9J5j4_940i7;q1b1~4pDK3QakrB-(!^$ZSyN3=e5!Z0$$s~G*DTD8tiiT(VxKYpZ5yXOB%roir9zi< zbG2x7b?%{3Al2E2$*SA>{(YslArirbSZtglB2q)4n^_PDjucr{WD`iGF{|3kvSnA@ z8D%5^I^RuX+I+N(X}`K>mzRXm5&`KTEgXh>utD3bJm9Y1P}>Wsd{&qVM4cI_s&p#4 zTu5-!2C=5Up@(M$vJEn+1!n85oBWevE!PRL(=putmsgBbmeUZcWwsZQJ^R6dP(SY> ztCbv;*uaA>h%@a?rv*a!5og^BGeYG_)i?C@+>90djK~P-wtOR9`My~KO6b-533hPX zxPb82S9kvj2JE@y*-oV}SX-e`zLH{JsM4t#ArR#zHd68g^0Q8O$<=2e@yH0Id}6La zuWeH4)2Z8`L*Y`Tujg4xeWMWgP9^!2)F&&81SZ|dGPn!h}24{*|N!P9IPZd+fI#S5O;!+vEZ=$ zt0)zEm1J=IfGiQ~9j?Nz^Hx#GO68js{>l#dm2|?E&7lc(V+EgrWXHyS9lWkzzlMbP zfQ$QW^7VGka7JDIo#n1%d}_>22fwgzLRmi9qG7U+oX44%a&KJXcsG$J1V0>FX2|~Z`@qzz6yPVwPJa(OAc3z zU%^8J0tS|hPuIc#M?>9YEEjkOIubck(xACMZ-6ngs8sH+L|^`5WiofUP*Fe@Q219$ z_{>I|o7yTdlcujcc;ZHv$85s?MmTvkf+4Y<*Lqmt1BW3t^Agw5bC23l%jYG{(aBcPqq8+;&c&= zXUvA!mA(|eC_nRWmC+!$IAO5-Gx|%po z%^7|E!RGjH@&~U~8gqYlWBl*-ci}{o2A+NW-|{zKui(@G``QC>rE-J(tv4#X3Bg)X wrJ^51{O=Y1|6ct6vi!t{|MlSueE0$%zQBhs@Zk%5_yQlkz=tpJ!C&D203!d)R{#J2 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/head_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/head_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..8164525353a764ff53aab784a2c6e1d8d0056607 GIT binary patch literal 6066 zcmeH~ZBH6q8pr?hz|1T&<1iA22O$(#ZM@f8ZLrnWXho5fYN0_ZN2^ z7ErAnH3e!4)D);GP*b3$Kuv*~0yPC{3e*&+DNs}3|6YNARH;q-pDzF8{=G_N8$W`w z(|^1i`L#-&J=leA&O?`UFnta1>45zYm-b((RMA@ih}{vng+T=D_m!Xp890J0T>Jy( zQ61cD9_ZE$dtloIu=F?4eyO67pd30}twO83;KUHdoa5?%hG}Ra8c};nd#s|l0J?+q z=0V*ug2;#Eo2m4sY&y51!D>yF^vj$kiHe=W<ihOH6F7Zc{ij2z_p~r;5|dVqQ`(3!Z(fv_EH53{A^`a+00LmA-H3A| zmJBx#qvKq_(4z9gzLr@|SmiRU&uz?vfeV^q@()P6mdrC&Y{=;QE+GMMvo6G$s5ZsL z7fc&U%+N#QX-oU83ENVl^FZS#MJBAqgeYbkX3SYXvQ~uMR}t)>xbOnqYO#r(!W??^_sZ6AiV_Tv@(3XsR|8hqK6spbBuq2>O} z!zk6Id-k#e$e80O=I6XVrsTe+;6kk?OuKl@KZf<+6xUTQA zpHC0o7IrG)#Yyq}v~*a$I6u44E4a|V;9{pDU3@NoI@mwZ->)22a#w%?m~2;hBTuI7 zRP?e^JlHFiD__c|rR`6pPp9P%#6XNEdJ*S^3f^4%=}-JG#m!Q?P-=go*ROtlwzgO| zP??VGYRQjHePw@BIi-4!i?{9Pl{?rzayz!tcykfG26+0%(qi)mE6U!%)5gG_^X@!) zEB$>1gg1mc1x71}bx^sa4tb^0fbFu&cifLBIp+;T(*A%oR=m`C>tso2d|U6H#O;c! zZ41#X-~EcFinBV@wTy(B6EEJL^QiQrBj>w8SZ#XvU6T=V>OrBLD z1h_FT9_C0!>e1J58sO<97*i0+(v4MVosfn}B$7F2YkUnR0pS_=4HKW$S!tmG9TRXY za^OFBNhrsi(E132AB7wjuT}A;gf1BKs%_n3UZl~Bx;VJ3j92wY-AD790WUn{@h)cTDeOh$id$)q1#1iah`a!n@Pr`Dl%;@JRtqZ_v%xC5Mulif;32xXH9KH zH68WC99lVpSA^>0;OvwTwGcNeDWk?#8(O+)i-L#WOHcs@3MvN|xr>+w(+HL{8LkNe zDbgw#Z70`eLTK=ihM)WxVz1+9Qr55X*#}HyCQbY~a5mDllCDPPYUC=AFAaf=k*|#_ Hhri&zrlFBy literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/head_color.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/head_color.tga new file mode 100644 index 0000000000000000000000000000000000000000..74b1b3078b3b60df9854086b839f8383cf4dea88 GIT binary patch literal 70715 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/head_hair.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/head_hair.tga new file mode 100644 index 0000000000000000000000000000000000000000..5321f35204b09610a332c40486f47f19060561ff GIT binary patch literal 75600 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/head_highlights_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/head_highlights_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..8dc5239f977da36f8f36ce3bdb62b23f12aed758 GIT binary patch literal 20503 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/head_shading_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/head_shading_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..e8ea490109d8c5401633ea33ad296aa507d8c9b3 GIT binary patch literal 35304 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 LNEZO^>hu2r{C-d+ literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_length_lower_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_length_lower_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..722bc192a832dd887ef7c90eee4055a6447cbcac GIT binary patch literal 9768 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 ZH_z;z-90Ov{SJ4YtKGA=&i>!7{u>lGL_Poj literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_length_upper_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_length_upper_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..e9db7e7b1fa8cc81c046200c2a497cf852d2cd5e GIT binary patch literal 14617 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 W#5O`)1w`3~C*?&LA-?^c5dR1Dq+i(p literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_open_upper_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/jacket_open_upper_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..71b8a0b805b0fcbb3cd17077a9624aee944d6228 GIT binary patch literal 41606 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/lips_mask.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/lips_mask.tga new file mode 100644 index 0000000000000000000000000000000000000000..ae1401c006adda76985ed7eeaa5336c471c40f87 GIT binary patch literal 6110 zcmeHJiCa|H6~FJj*%z2a7={r95hAF#5lKvNi#2{|UGZx))S_r=(l4JS##Gvbdf%W1 zqp8-YXh9`fW2)8`K@%6OqP8e70y2ZhA~LcG!|dCerRSmMkH|OkzW2TN=Kjv_ob$Wq zo_i4zqn~~xA_XWIx#v~-CPEUIsr-`Rcojfr)F+O&*I&zd&ftCCSm3nk+TtXuK8Yd|s zE;feNpcp!qr%}lz0xrdJwF6fh3L{94AWSuO@~q62tJi(Fna-1L{`hyBHs-v)YT2R% zbKXkFGZNC@cr#<}g2l_S-pgM5o3+{Rty;chQRd>M%U3L4G&e1Ij4E8f0lqfiYen${ z#T6-HCe6yy9Z{zb>3Vd*;-MlEeFow&!ofTg0Dk-S#^E>DqdSEH*psz;Orh2TkCB;)#{9sqbXx ze{=q}-e|JBybSHbjMhtd+}MSke23j)8Zr%=X$t~}q}gi6PTGasS`Tt#kCst*-45%J zvFTdrpVrJ89~H*ukPv?p&^NI0^CYUo^u-&tmt4C4xX051u@?y1c#}ejLDl+DG_VdoOkqP6sGvcS69V+X?DX@b1B0g7L994EP;> zxsUPK2RmvjkAAW4-6^p$0Xq*5z(+k&5R^cX@Yd@5!{z@q7>Dd`#^>J+E?&3+9#$*L z?FBl9@WMOk_CWa9hXA7W7XehpxNXlnbyvULwQ125O&E_AsUFDdkc{An)l-*mExuIW zIba2a{MrBx(g8YH6rf*fuU;Ta2#`(#SBDT)!fU_2b z6Ws9lS-E@5>z|q&pgDwbFj&aafiZ?>A$;RNxZme>S^Hb7PyPA*nQ`Gf5VaQU?;{D= zCoSA^>~_bHlZ~eYyzPLSeMDyRjiwu5jR3ol9=`NEu+H+P_A`vN2%~*e^`l*e90D#}zdhv^2j265E)FCk`h%U!z(8#Sqa%xbx1-AB2t%aJASKe2$*UB z#kfoz)umf9lSfdjY7YR#SB#szzUYFk$Ib-q2BiU?+uW@y-UlASua}s3$M^ zwDe}X$?Xr^3WfpBVr=~3$Y(32s>QD+z7a_YiZ37gcFyj9H}*N0Kov}Kn9ptPd3Y6; z=iB2HBSp>D-9vzK#8H!%{-Nw{C!6b!L3sem!D;Skt~ych>)GQ~VlE`ZE`VmX5A)>- zb3WKt+1O+C_yQF{C4fB+bAM;U^)j3@k8LY#NjCu80MuMb^t7z}V>b+aHjfVn&O;nZ z)?m1&O?Tt`;ewp`6V%d?9?Eu#7AROspiG>dqy78MmOhJ{fkNE_wMIkLYhfL+OYKYG zi10HWm)+F!xbEtA2MRW=oRtzCHp0>jEUj!Wkfu0E^Lx1necl-0#FU?4ek9-l70q7pH-H{)rNnMbOODq4U z?;0@MoGuUTjqa;(+t&Yb zW=d>Cn2^i1l1ISV&h9{haLvSdYqorKy!=+3{&Cl{zJdPU?#@RIcP^hjSz5e*H{L1y zYTu!f)0MXy+q!YD(gE6VnKmb+@<8!0iPUTu+&fv^*eVS`jBr7v8w8SgJSec^|y zoBz31dH&>~U0XjG*(LEiqZec@U6qr!v-nKqwX2n9%MR|y%U+nC5+ALUK(}Uh+YX@W zLNV-apwJ{upR*`y?MGWb-(HZP_uK4c^V27ei;K~yu}ZDh#KezJo4+#mlRs|z{f5;` z=A|WR6cPdN1xpvObfak4wD@9Kq$VyYb;jGXGG?YtN*oiVkisE?N5lC+$rp&_QSlSf zX1+NsB_T#77rzYR8JPFHJbwtpVTw^Wf~S;&9gl;_B7#d&JfTz$rw5rt$cG~b#N7`j z{fH*u%z`Oe6iEYY5MXA6;4O&u!X3Io3WO90DG*ZN=cvH{0j8qB A9RL6T literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/lipstick_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/lipstick_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..2795f1bd40f076eded229e25ecfdeba83a662a3d GIT binary patch literal 7966 zcmeI1Pe`M88pofgN#3B8w&RpCmoXtFkV6jdA(tF-$tj0ia>*f=lt4%bh=?NNpZL5< zkQmp=Ms$j+tW-QmE6xlK4uVC{iye1noKDM3yPa-ja4V$?3%zH*&+mPcvAav^r3+=F z1WevvzMtRoJiq7pJr5G~QtPLSx-@EZemfrMc%b8fjt4p(=y;&xf#016ex^meK|f>P zKT=WubFheaKjPiri2|$}K_f)rKSd~Pgp45T6p$wkdQFU{UBOV8BSv&JW<)vC6%Jtq zW5sK%_#3fb6dYI+iN@j^2}e8@ji@#F+%GE{^jBi9&FANUrU%usM6#UfP1isE?DYAI zZ(g09Uljk*dvSjD>YEqOPe1#(o-U`fWI_oAfwK91>=l&;eNE&wd``cvcyUfpl2hI3 z`low*Ku3J6oxD-hnu;fUjAsYkd!N?RoWeH}&bZp6?l*h{<*%Xqf()O_AAqpfk>DiN zdDFSIeSddv|DcVs^XOoIZ}-rn4^Em`1o-q1F; zw(mW7*j8PG`ojnJwzoEUgV${Z?A>ne9g*=92w#ElC8E%yw$4$GI}(NEmDM#x_~goj zPe5?2udS{u7ZM!jXir3pkO+PW!WSUC1A-a7bZuer&hi_?&J~QDEXxx3RI)DL@hckaSz8xj?Z!?Ri?BPf_8q^BS~ zA7=yAj7)P;})8{-g0~VurNM8F*#LW14dqhjfkM~sY!gW zx6ene<4e`e)v9jCd~|Jp?}16Qvg`KTETZ>T(cr?=+xlU^Hm$_Ot zos5MgUz^;|sTUdRL7_;bGud&TcpsI*N?Ef`TnYRBC2X@!sFa=JiQe&SCY?Z;@U(LF z5%GXm)d?7zlQLlxo3#lynpuHycebm*Ua7!4Rs~kwhN?|PL&`)>843rpRhdrc162b#u-(Br7vg!+uboAT)IJ3$*-XR&nPSN!iz_&N+=coG4*-n4pq3pp%Rz-TQ`i^ z8{E56Zy*g7dIRf9lX~A*8XB~4x*vV5+@Md$W8T;>ES?PN zZ+6a4(K&lyn$=gSLa(~2*^%L)!Mr#!5>j`y^aiIi5Kyf_Dw`9~X!TNCARMdH{qLLs z>2%G;%4`wqwd$x~a@mx$JG#|ZJ)=J9(PN2JCYJ|=N8cW+s(x2lWA)QLCqm&fn~WB= zi*r8f!E7P+ylr*VR$sMT9plkHaPql~;sh}nNuT(fT+&lXkn)2(Y#SNnvA*gredbgY z%r6WIUpUPPMksxky;kKh+vo@nyOhgt7l(tQFTHyo8tOPqF+;;(XjQYpkmIKGU%om! ztgJZ?&tAPWEs&W&WioWly+aq9m$|^42VRh-hL{x2Gh_1vFY#D+6_bo{etK7An?E+^O~n@v@TDAYfX218n4OFWjCS`HC60#RAkzw zKp8LA)dU%nsTbeps8XPy0R@@Gr!>5#w}ug3IZphCrfjPC|Udq@ZM60+zVYk!~; zW>Q-K+wQ`s%Dz7k`?@fGanL4HUkUy94_J3W>|rnA)#LLAWDdWLS-$zVe9q{cFR=0_ zVmEtwygskT{YUC?d+-;&i!S#kJpCti%aaA_me1vV=YJ|44|F`x@j%A|zb_B`3nX=% A!vFvP literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/lowerbody_color.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/lowerbody_color.tga new file mode 100644 index 0000000000000000000000000000000000000000..a63aa12fca04bb16b23f6f8bdc168371b6a7960b GIT binary patch literal 135395 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 z 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

QIl1V^OjRFqjN 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 K{~yl5@c#p~|KVo< literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/nailpolish_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/nailpolish_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..91af762902820acca0e0630438f7b19aef5136ed GIT binary patch literal 4656 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/pants_waist_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/pants_waist_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..35658c08968a9202710a82ba7d11b4f6c792f8cc GIT binary patch literal 10487 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/rosyface_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/rosyface_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..a0c8513da2f860da84825a17b980bdc2f963fc82 GIT binary patch literal 44382 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

H^{ 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 Z22Ee*)1R?1{hx36diCd(1^&Ml_#5R~-;V$Q literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_bottom_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_bottom_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..7cce03dbe040bc7cbe25a2b4f4be579327d12961 GIT binary patch literal 32242 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_collar_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_collar_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..f55f635473413cacd722c5e2b04c81e78f9a2c72 GIT binary patch literal 14417 zcmeI2cW_ikw#NHTl2~Jx>-kk})dD7Gk&(zzc-QNUjZMzc1cM1)Y#cb<_iB(pfWWLV z4!g!+uQNI4oHK|df;8AveaNvOh|8EZbj~gvw*ggC&Zqd!`BDyJV z@h3OkJ8t0i&)#Q&TR-3CPYc~!F5NMoPeWV&obK?QYwiu#x_6iEShs(sTk__#B6*W; zS$x~NdyiXOa<9AA-8*!fbb}n-0RO&i-MP#6FS=LV;Jv$dy!*4f`@!3HO#dI`osu3pK!9Jq2d-bJ8G;OE;~ z?|JvfgdT(e#C5u6UAat`GB464>+%)4hEHw~hDz*r4w1qMZzL+TZlGJ>%H>ORk$#H) zNI%iVz$NZ%T~Fw1JUZ*f;1QxnW)*^p+_-)XJ<*YV%)CJ89Jz?T*RCT#g#bgyu<_#z zdIk}n?#N<8=*b<$kMcrhOkNP^9J+W3eOWSh4x+P;^o(2J2JRWfxKWsdQLmyi2Bix! z7A=r5!r)0iq5D;g&D~AJX6!ul_r{j!p3L1L`X}_Jm@JU_bYAAm0sTEqvkY|j1%OOj2IE40xKUN zI!k9L57${bXPwWd0^HF&wg=&%U$HXgg*YV;R;rM7F%6ML3i8jNJ9{RNPSYtmiR*ME z@66eA=kp=2)CCTdX%H8>$!X#_aN0fZM`^UdC<6-vcJ_=Roe;<62_}Y5pE;{>NsNPS z#oA1)r@#wwtT!14;|&gRh9NEQ^hr8F$DLd{PA9@APv=1#6(q+D%mk{n=_+6rW(X2M zMUQcumoFjA7zcUg^r@34j#I8YMn`ewK6d=X$y2A#aHtHsbeZF1(*n0lMKLRgWNKMS zH`p?us31wRP8`prW9ld!!F4Pt_xK6T!d<}FG#zCSrfJVkx#xIJXc+7ALM&huROLbw z9dQoR5jy(VF~Cl;Fs%!YX-E3g?^9)`RC3;SMIT+M-Cr4c!2h+eYF4g2M!)OeB|h{T;R^0(>NU{ zOhcT;Gp3c{Fj<)wBx)k>6tjTZPy1-E+CzJ3Uzz>DG3`_y>LZ@d!Xn?ZlkQo!>^-gv zz~H_g<*5_LnRQ^_UfM&u)h^mid&=(JcYtX~!x_U_w%kcmh|vAo1snOHU+t1ga${Ihu{kLMmea%lg)J-b1p z9cnx6pq*uR?cTF*|DhvCbB~|QJL`GJiH%2l!ad`b*A4zvSI+qq*qZL_v+ zp{=y7{PrC?8F_$_h}qOiJY-WS$APS@Cst7*W&+RASpK=Zlex!y;`VJ@w`|_Dkv3J_ zyk+aQ?LP8Y?#aA!UP`WAHzw-o#YE4B3{Ng7ff@k$ZRV+Od7xmQ5QssP&I+ z*tluSw(UE1?dH@RfsfEg7s8kvh&!QVm|5Dh(PWuo|6PzD4|~V(+1BxOpp6z1Vr3D$Uc^&$ga!$K5>l;kcRJ>O=*Pas|RlI0+^r)#^vrtX;QWGk0=| zP?H4Y!+Lq~3dp#b%S_)*Q`$22Xu$!l9OPyj6W6X;O{-`nt$K9znzc;bhBR>{9l-Wu zm{$}1WG4Ll8Iv#Wr;5>@M|8H~3Jw={q585TV=}l)Q zKE%^mPe9BW-u$?xOCvKn=ryZXu2@b>X$dVYy?n*W)oXP4$du&T2lOGh>6NCL)S7w6 zFm;BQskT3i;@PwTF|S&&eCd+KYSF`smn>brVih8X%&pthcAw_u#%v!Jf`)4!HpFG{Z{dO;Kvdt-cMpI6!-9qIa+#shT1Ng$ zRx~kj-p~cKTP0S~ic*Lg9s>9seJj3EUyE<(+fv^#mOWigE6{3{Pc1QXj=87eRer;) z8Fox8r)9L%TD*uBMi~0-H(%3N;ve)SuCF3rfAcM)X`!`fG3siWSZ=-SkMPeZ>2xzh4Z@&KOA76f<=7+!d@*iJ)4cP&(X#rX+qQ!PB+1vID z_2I@lHXZ@_A%C_rn0^%SLVT}~BmoN|hB}|-sn2O%WWLYFJ+wi)n84BSA+y!$69qxQh5YH{y=F-Je%A2Rm zHk){ozuq(4*0$0Y0$K&A^cnq~{)P*5yq|~9zhK+#uNaHdqgnTgw{zwOd9G4XlD})J zSYYlvbd-P+pu#{urBCQnhU5KQd_E8NzX;5qH&?~@sHVwgo!0Zhko5^B$=^;?%=&Du z`ds=tKNX+Q$KoTJgX?4bTjS|7i8j{U&kPsy_qy3e&C%$Hd%`X4Z}Z-sqGAp-%D(~d zN#Ns;=Fo>~HhmbF^U=qw8K3{%0gZjtyGg<}f0${0&b-A6>x;*tkLe?8&WALcK2U$9 z4`{aa;T(cgLzRCw%;L?&R4y|E@KhHrCIz2F?3jW!>tn=9nn!+Xnv2ioSgclk zYKj)eOKkS~aH-v7rgylEyJ^Slk3=GY(EIyb-c@UVM4euTzThnZVQ&a4k? zgv!$7`!q8;gJw!kqCMOCV3wZ{y&)GLq7Q*)4$)kj@swCIXW5!0XUZ8eho;LMylSHT zS8LWxHZRFfNbxcH5Y}k#p{N9oW@fyvW|Ah+G%;09VF7H(1Y}kwO_EW{z%?n9nFTC?E2ld-ev0*+(+`Bk#{;1H zJ|@S{>L{7%9u@{7a>^1wQ&eVH;vIS$*Sj<^KoVh?5x9~( zx?UALx-M@+_n)jIUg@B|kx!{*MGO^Cex4o9H%OXS;*+Odc^318|djoEO}FrHc}YYPQ#E0;BSl@JBCKn>ohttX6(2(Fo0(eR}iF> zSg?w>cXr0-CoEWMBE2WxrFQ~vPk`OY20!}sQEFuJsMklE0qD&DETVVN?macJ!~*s& zd~9_k7ciS^57GrJk7)6uN6|=njYi-a85w29pz*l>7TUdSzvD-cu$;wf(rhl~grc7S zu$xYhx=;jckBl1&?IQtK!)Zk1wULNnEYg5`C?xronh@UwFv-{Oyg~?y>a2OV@7MSVz?ZphRR_A zqQgML69OBp#@pt*H2qc1eaU8Lyv${eS@9PeTpF7+M%yrAI1Qtr^r{#lU&ZUNq~Til z=rIJo4Oei8=8gB?(<7d4Z;ApP0GdVx;RWCVa2lcp(+~`T0fIaLjS8T0XskWXOw-7( z@Jpz-$5_l3s17lLhFilxw}uR+L1G{cz%|GkjB%`?!)Q3}zlL_Btk*}|WBy|lB~A}w zl*Axj2_lBj&{u~H9)tnS{(%1dsITfneUtk29}piwjzFuC;LN^n*a7|e_UTQ4 zsvgvndL{Ml)3@IM2u2kRjSl-2s`u4V;<9TGlc zX2~j19xxHpMxe5jJna}jbf<1s*DloA>Vk3bu18W&FAmHAOfjCrzhcpS#!*9PaPpvm zs6FG4)dOP$yLRdP3cVbBr8AFXb%$TcW}E>75sw=3e;vbns6`YsFr>$z-c~O}66)Tq zYZnY5I?+qGUJkv|rE53DWA*Gsy+fRt0fgC%vGkaG1Zd;^%7p4H!pbo>Fy@@i{GVUz9P?%h5Bz zjL^jk!|BMO@Ce2-9>L+st?Mh{$=YWhiT^lI{L}z=PrH}YMH}H%yxa*6sgCplt`{RO z89G<8L_5*5B%BXC=7_n^1zkT8Cv3QQeCR1L8^YRr&M?Rh$O6K{opcJQyX@igj6Xn> zJliw1cpHyDlu%C_RO%M?^~#Q!FG%PO8@)XB(4v@Ha;w!;T{DSW7M|aGNI$SJW^=_+Ba_q44SA8g_Jl-*qdN?k%+8eD#`z|ZtsAtzkAxtWG`!p>!s{0Fej;#&~nua5?tq5^&DO`8=qsYidST3kE{zh z@#xOvglE+FQ<*X|kn8Jt`J8-KKBJzN&*1er^*njHRi~0vJ=?XzhRXA>P5g%dY+ymEfTmt z)05FZ(USx;i>7CB4~B@2-Z7Wc=KX}J4~l!hweH^L-_iAhod$D7uuytdXrb!Q@=5xW z{G;=P{3BkUR9ZOhY2k^IrT!(^G7m3%+=Kq}dLkRsnN`6A>uD`m0j-{hc2K}8E#7(x zwy<{btUsBJ@Py~~-Av~PM*fL8{saKwS~3jKlI7JqHw|JDCfF{{awxu?*wP-~x zMJw4F{{|3RaX*@F-6*s*!&K;Q_kgzoI7O4w2^L8YKpG%hP;*=Wkq45nkUC6GcZ^qh zw(nw!p6zx~4hYnaAi)qs3)x&Yqo%T%Y%by?+;1n_=cL;Sjx)tLWEV}1Q>@mAgCIjR zQ%#*Fs;Ov3&GEja)e4FXXDT?wz8DUdm@3NlIKeipTWJb4lTGa=D$Q;po0294TefQL zakA5mzs8$g?*4dGlQqX`i=cw7TDEA;7}>;0Q>jjx)Tm(d7A;|t)ka4)xkO|;LGp8- zl}_595G&GQK}eH~w9M3uGy;jk3jt*`QTr@Ulgar`cb~V{+C`I~Nx^ijvwI3<4uiJ^+Q~BsDV-L`7gERx~qA{mG&z zv?RCl5iCTdeUhrQkYpbL6=;`GeLLK}aTOU_MWk&Twilsb@U;1<5+bN+L=)IqYM)U; z!*&pS4NOR44aI=3Jb;d(b(^-jf1-lz?rt}%|JVn`l93*1ulvJGp#^-W`{UouBF$TJ ze?5E{hZFC&o2}^2zqaXObJ7KjL^@hFZ>GC1)ZN2Pn{j7qC$LU%fyMvmXREs_;g5bC z=#R8(>-DCzrKyDIBf2*$=gx_L^0NiK@mq;mOSv-#->OBkrcEeqc51ZoZ1hiR(zIEN zRyuI3QQp{k_1NO>^jBh44B1N z*&Kn3w8^PX>vKvRCDFRK+TeU_xIPE<@jV5N9s+L6(b}KzQyr^tVqlU6Uv_92y>QcRE zec6CC)dn?eBBi!i)Oig|Seck>;m_{$ej9BY!AvnWt5n%oHc}0#fmOers+&?yv2w(}ZR^2)( zC0g656|JpOtU8d6Pa&N!fW!cyCU6&vkJE-%OOs_Y&o}ZIraps3n8^+n(_jld1YroF zhk$mfE9+1S)y}AuT{EK=)ut4@H*ruyfnf;41ThR2UZ+(MGc2)=H@e&0&_DGsse!bn z&7znCm1tE%1!v^*1)g|y{ZCOjzoK-{Bl(i(rpgN(t_3Af39E3OmX-zQEqRj)% znx;`I|5@GLfAVkS8{Dm~gZo@Y4ptV-3e8Q@+&U?M+BH>;tm;%PtGcRT1D;enrB0m8 z(y>Yqa}(Yt8uiEY{;B-?iSZ>FdZd|d&o&7rFN~FHL6#ms#GPjvaBR3W>*juWu?r@ zRE4TWs#UL1vsUdCs*AQL<>?LW28OcPwNmQJdO0W^+=TI^45w<9%9W^MRs~r;`*B%5 zs{&OlRjG27s)j=;Xo=FwsV`A*DNI`HZgvIoG_#IO!E3EjEQTQ!fyjJ3T8_$!a0FZS84_I`C9aWyJ2qgoF7h{{yS zs#t-%Zcdpm|KNDtsJvC7B2_|*Dmhg%s&T(n=x3wXbe%zpccU_k|3L9>umYK#j(&Dk p(`&tJSCN&cSCSP4>^2Ii%+h!tSstHP#Qn;474ESTRsCCg{4WiMBjx}A literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_collar_back_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_collar_back_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..43a645310739931da7f9216cdde577c78a04445b GIT binary patch literal 12789 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_sleeve_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/shirt_sleeve_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..e3b18f4fc69578f65c7dda9b675cc101fa389a60 GIT binary patch literal 72196 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_length_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_length_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..c86799469d5fdadc6e0e614ab45060685b0e1695 GIT binary patch literal 4114 zcmeIuclQr;07vm}zXnAS4QZeu+S*IeLQ5&3rL?v8rllo{P_(o(j3Vu&2qk2M_TGCB za{J?bj&trgpY!brbYIWlRw{KWORA~V)UNPfmZG{E%9f_OWhh&gvgIhNMOkgimZxk5 ztPW*$DO(X+iL!cFeacqG8ek16TLoJcTMb*CvPRe%SYvEWY%OeUY#po#)|9el*t*1e z*!tK8SaWPctOeE*+X&kj+l1H@+YH+r+X8DvY>91!ZH;Y1Y>RD&ZIA6h?1=4z?M$@B zcENVV+7NBA-H6?>J+M8Ay|8vzd!hrjH?a@4FSZ}CKh}{r06P%tL>z=2OmxN$A-Z5) zv2Mhn*kQ!s*b&5$Sa;$m>}a9~b_{VWb{uv*aRPQCaT3;(I2k*II2AjMI2}8KI1@XI zI2${MI2SvQI3K%!xRAIAyBNEKxD@L}^d>IDE+?+Qt|YF)`e0WR*AUlY*AdraeTf^e zenfxlM&c%H05K4|nHYo(CWc_Q5VvAOiDB4n*zLp}*qy{(*l^-*>>lD?>^|atYy|ND z_8|5U@h~=$7=?`{#$bWs9kf|0^B%7x>#UkN^Mx literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_back_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_back_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..0e49688b146a55e7d776a80ddb65ff81bffa3202 GIT binary patch literal 90350 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 UoVGvuw)BtU4?ckZnd-m)2PI|)XaE2J literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_front_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_front_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..888bbf71a1193cbc9ff4a564e6c0837f6affab5a GIT binary patch literal 90350 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_left_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_left_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..210feac1ea0e4014ddd8187e1460ef1781a5c8e0 GIT binary patch literal 82006 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 aJ}-*%lj6c<_}jEMdCKa!{pEA}?f(x2Sfexm literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_right_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/skirt_slit_right_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..ce11c64bf6763e517b16f4c0d70f02065132e4c2 GIT binary patch literal 91410 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 literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/upperbody_color.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/upperbody_color.tga new file mode 100644 index 0000000000000000000000000000000000000000..85fcc41142102114244c28c9743d97b119e2f3e1 GIT binary patch literal 23348 zcmeHP33OG}x!yMegaCqq45Ec9 zf7tu%b8?jmSKhxM71T}L#Gl{cC@)oAU7d2Xb_WhYD9uq?q42sL%AF{kQ99nNt80&P z2S&6-X@hcGU2SbEoLi!V*VNR6*VfduKwFp_4Pgqk=I&Y?f^cY7bEBr&wX0W2-(QU5 zybQ&`W*(0EZ739=14Zk^yLWn6-ww)#bzNPnNb&`nx)#Eahvt#w>LSA%*KY(eI{`|H z#7UA`wn3qm9Z;y{T_~a{NVP+;*|kPpG$oI4@@+xrJlv=x65Y z8z;ZPbX+wMMxer9FzxgPL`q{XmLZ`RoU7 zHZ$@8m|(_Q7;z-Wx-blGz>ye+p!nlRgV`MKmU^9-ja=z9HkP-Mek&eY3QrycO>rCI z<+|A}r(2W?{oNWDC3n!x3QcUJTEu7pirU4JS1J$7w2_n@Y zh#gS~AXTyrW&@xx`mjn zbr5R8h%KPgfEgjARjdi$%aYC@TgKWt3_DMM0K|UPj&*wBRNt+&LdxJkO8mJv;Qsj=r zrXwZ7m9(^TlT8Q?=p*+PQEb z3z;Q4vm;~l9>DP}(7g|Jf4UXCq;wocsfnYF3ggd4gpyNgk|xeFH;`c$j&u_F9NNX0 z>=4ARp@f*Q*uFysBuu!CaZRAxlMb=|HtzXLe4dviN!3Dxtc|iQFOg;l2+BCq{v=6q z1jC=)1zu7vlB4o8AI~Ie6SSu&Z{`CbW<7sG7kSBnUfBRSk)*=nTa>j8U5FvaR6?2j zNqA(bLmL(#yoXLK%%#CRqm9^6E-^jYIPVj~280;SN@31d3>8^T(7+Uc^je@WXHmok zW66vYs|?2^uDD`Rli5mQ#Hzs<5mx%SvFb6LWrWKnspbYeF*`-upXcBpAtlqVAzuvoducnk|+1rsqW#u)sQ6JK?-%itSPuZ`hPu{O*PEFA<(Yd;? z;P|q=ds1~uN{V)(FqKsK)T8oqD}FP7cDkNDBSp`MO3`kT3ZC~4#(sG$ru68k`AJDh zdL8?l84wePOhz2Mtx@5!Gs^m`UXh_c+nl36Tzo(ToIO27N3Y1#+deJQWoIgvP`I_E zAQPmSyGeC>qg1QTSF4Y8uKsj$uHLaNPrn0-vryihm9CQ#ignfb%TY&;9C=8n%2H;B z1Fh+73oATW(S6OzES;Q`tCuWH7YXOSlddzjm>N^uqa>`m-%Zbw$PH-(S62eXo-X%pkRp>(^@)jd<$hC3X4IbwMGXcchl= z!0{c=-J8nEad-2sNSov^c8_OlopPd#n+@FTU)8pTvNVGy0HK=BmFF&nFvRlm^7>R( zRw@QXrh~Mr*(PB{ERU65?3$lf7MYP=Iy3V?>8PyCl3{!Hq)Av^xpGBiG)5tuKDPVw z6MEh1gL?VWZ2dlLIuCxa{-b=InNd1>+qT_0KR^E!`o{qWDy!JW%>d)`(CD{dZo0(zilrHP_pV}n;zarD$BrHA)F>5^hjFreNrqm% z`~VVAs(x>78jwoSA1pXPte2ISR}L#GDtdwmC*1+-Xtsr)Ex)4HuFlu*%`s8@_LLM! zirYUc)+HsUepOgd{QKhK;vTd~qg2FmN}*Z&rXsy;aSjr4fzC)jIqXp>y9YEe8qeqW+tgfzBdmSijU|Y-D z+B!-2N%4pEilw>YYCNx9m5*toMCWFooV)AuG+kI&_`h^}N!u9d?+j9Y(4X%(tP2j7u0k3f*Epr@%<@;(f0V5^BFW4`AX6?V7S=k&(dlUi7bDs9Yg{HKnq51O>*$raNaCqLJOxt& zkX0S2&@)r&J6eF>HC_5gVG|lR)ra&KNuh^X3xH$^+d}DE%$I2?XCe#o z&pmqaLbdo>QDY;T9n7nEbjiYO#Cy8_0CCP-SzL4;nYC6Oau~IkZDAKLURUez>@#l` zpNCQ;rkD#EUl|8ZN0gOSsKUlZw>Xe`CUWEYY*}V8Q!#Tf_wnf}G47yF*`F7E>Cz=t z;H5B!HQ6Oa#iRRF)?~>RvnEf+$08GM$VMhi6^Yy=8nk9rw%)QiMepCgU;C&{vMzwGMUn(rDtDGNsNUg0_T)W zEj&Y*t;l@QyBRt@?h73sAMeN86&t%*J^0{L${ifsMNJL~xm&q@@{?y&QqnfVEkMi3 z5c32@IX+Y zQ&WyE+`4t2{_@K&HRpbjy+$xAFRu{UebimgJoB=us=8n}oMJBtL|KbAtFEq5Rp+mD zJX?PGsmjVL6N`&WMrLJYMbd#J2!w3Vh7Iv5H1r-taU&^s2o5cA)eWT?N^6vM;t?t? zZj0e{g1oSKa8njCnFq@HaAIVbujL(nq7YnFRfQFPjR{x7YvRP2US8s8q2SdX<$K`K z1LZ!?xr5<1ZroJE?>MPKqgu7?(^5PuWXki{s%3ea>4ZqqI8>rgHre3|8q08lx$=niD&;d8`l@=Ey=lh)5dIJ!%ZBz_TJqmG({11 z5^qMcHf`<~%_Me$Nf{*wg&N*tSha5b9Y0nfGz`|MKhW4=+V)wV<|CWg<)L0%l8bd^ z<;4kLWZrm6mkTp42pK$Y z;`vV4hA=rNCludxO+u#0~fiw)PJD_w1kGseW=W@ z=>O!CFZqP%3?b;Lzd#|_528GQ@(7BBa=Q`2>rP_lR`gD6vHgIhxPB^ctcU|jdGtPQsoUA z@#gSrS8G&$Ud1n?k!3iO(X~EUkgmVjTZ)x)QOwSrpTqi<{|AnIHHar69*p)27p^pH znVnmqJb}`Wkvr0b14F2pVY+VJCgX4tsbSUEYGlISxA%DTj;+Od_vb~rpy0%k)YPn) zf`WpH*RNl1E6jYZ<~n`4Om*t?BcHp4gGE=AAEDfb5-J|=>e}^x)R{BohTTaD8+z^9 z4R!i-WsmamiwjSjI2DzdnK=(Lb*QkA(6N*A2sH(iWR?q>%$n-cXSgACnxuj`{a(0m zArwyW8a93n3CSx$!c|puQ9bhZ^WscAB( zTeqM3GO3J|cfe8$%E>v1Ib?*SNr6`Kq>HmZ=VIw3hf6q`%ZaRoakkvG>kHMl?XC`(ejedN35yVVCC8bNfRUttR_Eb!l20yn!=zd z6*OhSrcBtB3IBngXLR* z$sUsao#hJvIq;)Sc}ZD=%?55b)CxmSB#N>?-9+ojwSrhleFSX;I>O>N#@sPYe$;T!c__97E9^7{#%JaBEIoDGya+ z>oq*}re&T`AFfGLTe#O7RJiJb=kA8Ar$iTnKsQ6_%;wZG|FB zL+N;jJ=oKybgWklv6m71aA~EAUZ1JPfo~+4yT?r4tkh^6q&AstuGKn26&9a0s*3RP zsc8@eq@WadI(r5Mi0%Ph-WdrDf|0OxW#ye#vls4BZVn6|GkJ?5>(P@otC5k(YQ)4O zHGD#%8jdsTY#Ysj`y!}|V~L$m6S|eu&_zKh@T9+~jt`rS94Wrw$_H@M=I%6NwKgIp ztC8;Hdm+kWL*7hydhodTrw5IT|M}qY@!f}vkMB10twc2pg<_8Kh$u?ANXJh^@Td?G z+4fDS?o&YC>xqT&LA9W+N{^pc^FG*X6lp_6M!J*khoHS7e9wVn;;aFqV^KbNZort> zo~S=Ra7^s;L&hh(F!Zg2{}${g80<$)N;XtNC^Ye<7It`+dsT9TjTXEYus!C1?PBSV8!9x(#rD5Da=#rHE0VSp9}?v|b;w!Rj;UBdgEA zbvVaZuMJsm^&j!EHE?w73t;{jZ8r2xxEEY(6kJNE1=DTpprd+aq8b(UTFVBK7QY`r zk}JU@kQRV^4t0PlKt2y3UjPSy833d>uLnp^jI1Xw6C(^M5vU#28x^_vQ80Uv>h&G5 z(dsi;Tdxj?vHldX#`<&rRo1H!tF6EEUxTvF`b)$*>owf(KVswagU7}{Jal}5xF#$w z(Ir$RWIa&>p0bPOqS{oy9;^zmJ_x?OzXGflV1?%-KGpzMNgp0qJ<4h-EAHBgNs3v6 z(Ml&p1a%+wR#LAaV-u{1kyLjB)cOdjuCxB!FWP#wf3)TP3o2oM1pu!OSY!P)LJKGZ zMs4Z|i`+Nttpqg!3Nsc(9T(}7kJ!_wPj)uEgy>PI`QqOJuLXFY2fPo{g&V@_b6uaR zl9NTCJ4Yj;;1n+cE4X9=TsF>nedx#5YlAfG7Xx*mZ2#5PpZYDc{@iyd+&J2}GVIPi zeTQza291sDIb>YCYdBL6)MaKg_T|hZTZ>NMLfNG50JqoQ4X(w!0dVhg2G^%@jMuuI zI29So(vg!}0enSt^UJft-ixPhBE6>K1R&0v;O$TQtOYeEwWyIb)NM~zr|2C z8tu4`cSi_%&d*0CP{k?5Ynlr&uSYWmXnT zbb`wFf_bkY<6-Bq@m9p>IIG`?SlD}$)pz)M>$QPvtbRjdp#B=Dxf(WK0z0p=-arh$ z0d@Ng`@|YJBF=)NJu!G3+}bn6jEmYLuyQRTN(TZ>mu?Bpzxx_ML+yl~0Q$%Efu>2F z)NaLv2yM-U45sS`Ac8dvwjShBJYp2GX;;kWLW04@^fJdw~Z2TvvZ$d#Eut6q)UsGw$U-2B@ zOA~V{7e?^ewd>I|Y_@Cv#C@A@y5>py>c;9D#yq z-WvL5f;BpFv*mt!yM+~|H6}9IdNb-%3$X-b;;eztcp&?ZOG4kI-Y_A|m>`*(b3r|= z1F=@X`k`+CYX(;@pw|FvN7ZfU&CHzK8UtTIMuqB3YEWlPtThM@Fzn4FYt*DIR@Cgz zt!Z=jSTo+;hx%@7I0FL91t>v_6Ybr0Y)`2F7i-~c@l zpf?2EQFVqJ#WJ(>X0DxH8ZjXm@tpuwP;eeH7M?IJ(Ha3YM@`&pjgQ)9jhnpH8bOH0 z(!ZfPU}BxHL6OI?$e=>rNsK(aiLFZj;0l1Z#>f}GCg6;+7o7liQoV(58sh>*RDX6j zRHyqfbVV&#(~XpAwHX2c2m+Tdc5p`^@rk`LV&sI)OpIzUS7)*SO$MkYyOEOwc^3z` z72tm9Yr<^-?#G?rc2c}K=Qv|?%->jGAVu7bB>-kas0N!y(&t+bLTwN%Ptlm|xgMs% z5eNo4IM`9)m+%OnuuSw?+ALSdLsWKx8D5tB?AL-BnaCMtCxtusFGoEJD~SChBISkQ zH%=?2q;FI(vdl{(YeL8 zh-4_t2TSA72ocU)1dz1lfd<_9ue$h>nYG4%<~05!fPUd00Ge}7BS1T5xXx3EmR?Zi zMJ)QBA2uPWCzR}kdp%KlxCwAGU)E&WG8VQs2F%tDxAnLYX4Jg}%;1{%2w>)e&~twS zW_TD>y#>qx>dJObv-GSJvYkUgX&29v7u8j6=A%}$9`&O04jdaNb>1baSloz;c6P3} zq2n?;c{j~tWyf0p%M8*Mz&-`>d-1ssz+!VCk)R5=;NOEX+zMKsda_j@o1bC!PLiFY z?I`Awohb*{sz_=R%o@?7t=UU*uXhtoKH-slLHWQ+wf1gKdQ^bxodX~BjX~oRS~l4vip-`p*#up|e{k4) zpt9u{Nx@}{vfgf}PrUOS=iR3K$CUL6C0mywlzfzLw&FeCWPQcGVtXgsSi#=RzaZ){ zc)yBzK7nMjSAx$7`0Z~s zc|;Q$|1^(~_h>RH1h|=eOf7Si(+#H)@~v^BH)IY8AceW*UuW6{WbP!ce4b5QT+(GK zr&AX@vx2j~$i=*5V-UaCcFp4@f5I-X-(lY1tQahv`E9mq4%?)2W7Lb-gG-`d9)h?Q z@Ee6IM^WT=!psN~W(g98GdW^f~Sv?{F;;e!WR1atL-D;#*HA7lAn$96N)!n zRL{UL9koF5(Bw$ekn#}?Mn9?&WS?mm7q7f60=18a?`NnQgCHNH{d`Dp6cdO-93>pZ z`a;)56B1eZsSw=v7yTeF8;B`F31uN8HIjD;X;yd@B-+&H;gkYI;#uBNUe(bO?4!dD z24__KMa<(Rozcgg@&#H9P5z@!@RBi84{krV3Fv>^+u!S>Cip~er_PLC7ah#B- h=@iX)u+&I0#9K2VNoAD$%%jLmaKHqO{(&m={~vY|3043A literal 0 HcmV?d00001 diff --git a/Source/OpenSimNGC.Appearance.Baking/Data/character/upperbody_highlights_alpha.tga b/Source/OpenSimNGC.Appearance.Baking/Data/character/upperbody_highlights_alpha.tga new file mode 100644 index 0000000000000000000000000000000000000000..2d8102b583d3a8346afb6cb671b9b7f9f3d3780d GIT binary patch literal 6509 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 == LLTexLayer::RP_COLOR) + success &= layer->render(x, y, width, height, bound_target); + renderAlphaMaskTextures(x, y, width, height, bound_target, false); // :403 +``` + +There is no other reference to `RP_BUMP` in `lltexlayer.cpp` or `lltexlayerparams.cpp`. The bump layers +(`head bump base`, `bump_head_base.tga`, `wrinkles_shading`, `eyebrowsbump`, `facialhair bump`, +`base_upperbody bump`, `upper_clothes bump`, …) are parsed, kept in `mLayerList`, and skipped by the +render loop. **The 5th component of an uploaded bake is not a bump map.** The name in Ledger Q-8 is a +legacy of the 2009 viewers; the current compositor does not produce one, and neither does this library. + +`head_wrinkles_highlights_alpha.tga`, referenced only by the bump layer `wrinkles_shading` +(`avatar_lad.xml:9192`), does not exist in the viewer's `character/` directory either (checked S0d), so it +cannot be embedded; nothing that is rendered needs it. + +## 2. What the 5th component is: the morph mask + +The reference bakes (Truly Bazar, captured 2026-09-03) are five-component J2C. Measured with the library's +decoder: component 3 is the visibility alpha (on the head it is the eyelash mask `head_alpha.tga`; on the +bald hair it is 1 everywhere), and component 4 is a flat 1 on the head and a flat 254 on upper, lower, eyes +and hair. That is exactly the output of `LLTexLayerSet::gatherMorphMaskAlpha`: + +``` +void LLTexLayerSet::gatherMorphMaskAlpha(U8 *data, ...) lltexlayer.cpp:460-472 +{ + memset(data, 255, width * height); // :463 default: 255 + for(LLTexLayerInterface* layer : mLayerList) + layer->gatherAlphaMasks(data, ...); // :467 + // Set alpha back to that of our alpha masks. + renderAlphaMaskTextures(..., true); // :471 (GL buffer only; not `data`) +} +``` + +The packing of RGB + alpha + this array into a 5-component image is **not in the 26.1.1 viewer**: its +`indra/newview/llviewertexlayer.cpp` (353 lines) has no `LLViewerTexLayerSetBuffer::doUpload`, no J2C encode and +no upload path at all — the LL viewer stopped uploading client bakes when SL's bake service took over, and +only renders locally for its own display. The five-component reference bakes therefore come from a client +that still carries the old upload path, which under P-1 is a capture tool, not an authority. The component +order R, G, B, A(visibility), M(morph mask) is established **empirically** from the five reference bakes above +(component 3 carries the eyelash visibility mask on the head; component 4 is the flat morph-mask value), and +matches the historical LL packing (`"RGBHM"`) that the old upload path wrote. It cannot be cited to a line of +the permitted viewer files. + +### 2.1 Which layers contribute + +`gatherAlphaMasks` → `addAlphaMask` (`lltexlayer.cpp:1287-1290`, `:1513-1539`): + +``` + const U8* alphaData = getAlphaData(); // :1517 cached morph mask + if (!alphaData && hasAlphaParams()) // :1518 + renderMorphMasks(..., force_render = false); // :1525 → returns unless hasMorph() (:1294) + if (alphaData) + for (i) data[i] = (U8)((data[i] * ((U16)alphaData[i] + 1)) >> 8); // :1530-1537 multiply +``` + +`renderMorphMasks` caches its result only `if (hasMorph() && success)` (`:1389`), and refuses to render +at all without `force_render` unless `hasMorph()` (`:1294-1298`). So **only layers with `mHasMorph` +contribute**; every other layer leaves `data` untouched. `mHasMorph` is set through +`LLTexLayerTemplate::setHasMorph` (`lltexlayer.cpp:1717-1727`) for the layers named in the +`` block of `avatar_lad.xml` (`avatar_lad.xml:17473-17502`): + +| body_region | layer | morphs | +|---|---|---| +| head | `facialhair` | Displace_Hair_Facial | +| upper_body | `upper_clothes` | Displace_Loose_Upperbody, Shirtsleeve_flair | +| lower_body | `lower_pants` | Displace_Loose_Lowerbody, Leg_Pantflair, Low_Crotch, Leg_Longcuffs | + +No other layer set has morph masks, so eyes, hair, skirt and the five extra bakes always carry 255. + +### 2.2 Which wearables contribute: template layers vs plain layers + +A layer set holds two kinds of layer (`LLTexLayerSet::setInfo`, `lltexlayer.cpp:290-297`): a layer whose +`isUserSettable()` is true becomes an `LLTexLayerTemplate`, any other a plain `LLTexLayer`. `isUserSettable()` +is exactly `mLocalTexture != -1` (`lltexlayer.cpp:64`): **only layers with a `local_texture` are templates.** + +- A **template** layer is rendered once per worn wearable of its type: `LLTexLayerTemplate::render` + (`:1659-1689`) walks `updateWearableCache()` (`:1615-1637`, the worn wearables of `getWearableType()`), and + for each one calls `wearable->writeToAvatar` (`:1676`, so that wearable's parameters are current) and renders + that wearable's clone of the layer (`:1678`). Its morph mask, however, contributes **only once, from the + top (last) worn wearable** — `LLTexLayerTemplate::gatherAlphaMasks` (`:1710-1719`) takes + `U32 i = num_wearables - 1; getLayer(i)` with the comment *"For rendering morph masks, we only want to use + the top wearable"*, unlike `render`, which loops over all of them. (S1c correction: S0d/S0e recorded this as + once per instance.) `upper_clothes` (local texture `upper_shirt`) and + `lower_pants` (`lower_pants`) are templates. +- A **plain** layer is rendered exactly once by `LLTexLayer::render` (`:1023-1200`) whatever is worn, with the + avatar's current parameter values — the values the worn wearables wrote in wear order, so the last-worn + wearable of a type wins — and its morph mask contributes once (`LLTexLayer::gatherAlphaMasks`, + `:1287-1290`). `facialhair` has no local texture and is a plain layer: with two hairs worn it is rendered + once, from the second hair's parameters, not once per hair. + +(`getWearableType`, `:842-880`, derives a texture-less layer's type from its parameters; it only matters for +templates and for `getSkip`, since a plain layer never enumerates instances.) + +### 2.3 What each contribution is: the layer's alpha mask + +`renderMorphMasks` (`lltexlayer.cpp:1292-1512`) builds the layer's mask in the alpha channel and reads it +back (`:1490-1493`: the alpha of an RGBA readback): + +1. If the first alpha parameter is not `multiply_blend`, clear alpha to 0 (`:1309-1320`); otherwise start + from the alpha already in the buffer. +2. For each alpha parameter, `LLTexLayerParamAlpha::render` (`lltexlayerparams.cpp:262-372`): skipped + entirely when `getSkip()` (`:234-259`: `skip_if_zero` with an effective weight of 0, or the parameter's + wearable type not worn); otherwise the mask file is pushed through `decodeAndProcess(domain, weight)` + (`:330`) and blended **additively** (`BT_ADD`, "approximates max", `:287`) or by **multiplication** + (`BF_DEST_ALPHA, BF_ZERO`, "approximates min", `:283`) per `multiply_blend`. The effective weight is + the parameter's own value for the avatar's sex, else its default (`:272`). +3. Multiply by the local texture's alpha if the texture has 4 components (`lltexlayer.cpp:1338-1353`). +4. Multiply by the static mask image if `file_is_mask` (`:1355-1371`). +5. Multiply by the layer colour's alpha if it is not 1 (`:1373-1380`). + +The colour pass calls this with `force_render = true` (`LLTexLayer::render`, `:1073-1076`) whenever the +layer has alpha parameters, so a layer all of whose parameters are skipped still produces a mask: all +zero (step 1 cleared it and nothing was added). That is why the female head's morph mask is 0 everywhere: +`facialhair`'s four masks (sideburns 1005, moustache 1007, soul patch 1009, chin curtains 1011 — `sex="male"`, `skip_if_zero`) all skip, the +mask is 0, and 255 × (0 + 1) >> 8 = 0. The reference head bake's component 4 is 1 (lossy coding of 0). + +A layer whose net colour alpha is ≈ 0 is not rendered in the colour pass (`:1040-1044`) and so has no +cached mask; `addAlphaMask` then renders it on demand (`:1518-1526`) with the same rules. + +### 2.4 A worn wearable with no texture asset still contributes (S1c) + +This is the mechanism behind Ledger Q-12. Aleric Fenwood wears a jacket and **no shirt asset** — but his Shirt +slot is occupied: item `77c41e39-38f9-f75a-0000-585989bf0000` (the default shirt item) with asset id +`00000000-…`. His reference upper bake carries a real morph mask (31.4% of pixels at 0, 67.0% at 224); the +library produced a uniform 255 and logged `upper_clothes morph: no Shirt worn: mask left at 255`. + +The authority resolves it, and rules out the other candidate S1b floated: + +1. **`` is the only selector; the jacket never contributes.** `gatherMorphMaskAlpha` + (`lltexlayer.cpp:460-472`) does walk *every* layer of the set, so the block is not a filter there. But a layer + can only contribute what `LLTexLayer::addAlphaMask` (`:1513-1531`) can get from `getAlphaData()`, and that + cache (`:1183-1200`) is only ever filled by `renderMorphMasks`, which returns at once unless `hasMorph()` + (`:1297-1302`) and caches only under `if (hasMorph() && success)` (`:1389`). `mHasMorph` is set exactly for + the layers named in ``: `LLAvatarAppearance::addMaskedMorph` (`llavatarappearance.cpp:1352-1358`, + fed from `avatar_lad.xml:17473-17502` at `:971-985`) fills `mBakedTextureDatas[baked].mMaskedMorphs`, and the + layerset loader looks each one up by name and calls `layer->setHasMorph(true)` (`:1237-1243`). + `upper_jacket` is **not** in ``, so `hasMorph()` is false for it and it contributes nothing to + the morph mask however it is worn. S1b candidate (a) — "the gather should include `upper_jacket`" — is + **refuted**. Its five `param_alpha` entries (`jacket Sleeve Length#1020`, `jacket Collar Front#1022`, + `Collar Back#1024`, `bottom length upper#620`, `open upper#622`) drive the **colour** pass only. + +2. **The contributing wearable is counted, not its texture.** `upper_clothes` has `local_texture="upper_shirt"`, + so it is an `LLTexLayerTemplate` of wearable type Shirt (2.2). `LLTexLayerTemplate::gatherAlphaMasks` + (`:1710-1719`) asks `updateWearableCache()` (`:1615-1638`) for the worn wearables of that type — and that + function counts **`LLWearable` objects**, not textures: `getWearableCount(wearable_type)` and + `getWearable(wearable_type, i)`, with no reference to whether the wearable has an image. `getLayer(i)` + (`:1639-1656`) then needs only an `LLLocalTextureObject` for the layer's `local_texture`, which a worn shirt + has whether or not a texture asset was ever set. So a worn-but-assetless shirt yields a real `LLTexLayer`, + `hasAlphaParams()` is true (four `param_alpha` children), `hasMorph()` is true, and `renderMorphMasks` + (`:1296-1400`) produces a mask from the parameter alphas alone. The texture-alpha accumulation step + (`:1336-1353`) contributes nothing, because there is no image — which is also why the **colour** pass draws + nothing for this layer and the visible bake is unaffected. + +3. **The parameters are on the avatar, not the missing asset.** `upper_clothes`'s four alpha params are + `Sleeve Length Cloth#600` (`shirt_sleeve_alpha.tga`, `multiply_blend="false"`), `Shirt Bottom Cloth#601` + (`shirt_bottom_alpha.tga`), `Collar Front Height Cloth#602` (`shirt_collar_alpha.tga`) and + `Collar Back Height Cloth#778` (`shirt_collar_back_alpha.tga`). All four are `group="1"` driven params; their + drivers are `Sleeve Length#800`, `Shirt Bottom#801`, `Collar Front#802` and `Collar Back#781`, which are + `group="0"` shirt-owned params and therefore travel in the avatar's `VisualParams`. A missing shirt *asset* + costs no parameter value. + +**Predicted mechanism for the observed histogram.** `#600` is first in `mParamAlphaList` and is +`multiply_blend="false"`, so `renderMorphMasks` clears the buffer to 0 and accumulates `shirt_sleeve_alpha.tga` +(`:1307-1330`); `#601`, `#602` and `#778` then multiply in. The 31.4% of pixels at 0 is the upper-body area no +(short-sleeved, bottom-limited, low-collar) shirt covers — beyond the sleeve, below the shirt bottom, above the +collar — and the 67.0% at 224 is where the shirt does cover. The parameters responsible, by name and id, are +`Sleeve Length Cloth#600` (dominant, being the clearing param), then `Shirt Bottom Cloth#601`, +`Collar Front Height Cloth#602` and `Collar Back Height Cloth#778`. + +**The rule to implement** (general, not jacket- or shirt-specific): *a morph-mask layer with a `local_texture` +contributes the mask of the **top worn wearable of its type**; a wearable counts as worn when its slot is +occupied, whether or not it carries a texture asset. Its parameter values come from the avatar's parameters, +which for an assetless wearable are supplied entirely by the caller's `VisualParams`.* + +## 3. What the library does (`TexLayerCompositor.Bake`) + +1. `AvatarLad` parses `` into `MorphMaskLayers[body_region] = { layer names }`. +2. The colour pass renders a layer with a local texture once per worn wearable of its type and a layer + without one exactly once with the merged parameters (2.2), computing each rendered instance's mask + (`ComputeMask`) and caching it per (layer, instance), mirroring `mAlphaCache`. +3. After the colour layers, `MorphMask` starts at 255 everywhere (`memset(data, 255)`). For each layer of + the set named in `MorphMaskLayers` that has alpha parameters: a template layer contributes the mask of the + **top worn wearable of its type** (2.2), a plain layer contributes once; each contribution's mask (cached, + or computed on demand with the same rules) is multiplied in with `(m * (mask + 1)) >> 8`. A wearable whose + slot is worn but carries no texture asset is a contributing instance like any other (2.4); its parameters + come from `BakeRequest.VisualParams`. +4. Sets without morph layers, and the invisible-alpha short-circuit, yield 255 everywhere. +5. `J2kCodec.EncodeBake` writes R, G, B, A, M as a five-component single-tile J2C; `Decode` exposes a + fifth component as `RgbaPlanes.Mask`. + +Reference check (S0d golden run, Truly Bazar, 512): head 0 vs reference 1, all other channels 255 vs +254 — the difference is the reference's lossy coding of the same constants. S0e: the plain-layer rule above +replaced S0d's per-instance treatment of `facialhair`; Truly wears one hair, so her numbers are unchanged. diff --git a/Source/OpenSimNGC.Appearance.Baking/Enums.cs b/Source/OpenSimNGC.Appearance.Baking/Enums.cs new file mode 100644 index 00000000000..52763e595a2 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/Enums.cs @@ -0,0 +1,70 @@ +namespace OpenSimNGC.Appearance.Baking; + +///

+/// Wearable types with the wire values every viewer and OpenSim use (the same numbers as +/// OpenMetaverse.WearableType; kept local so the library does not depend on the client library). +/// +public enum WearableKind +{ + Shape = 0, Skin = 1, Hair = 2, Eyes = 3, Shirt = 4, Pants = 5, Shoes = 6, Socks = 7, Jacket = 8, Gloves = 9, + Undershirt = 10, Underpants = 11, Skirt = 12, Alpha = 13, Tattoo = 14, Physics = 15, Universal = 16, + Invalid = 255, +} + +/// +/// Avatar texture slots (the viewer's ETextureIndex, the same numbers as OpenMetaverse.AvatarTextureIndex). +/// Wearable assets key their textures by these numbers; bakes land in the *Baked slots. +/// +public enum TextureSlot +{ + Unknown = -1, + HeadBodypaint = 0, UpperShirt = 1, LowerPants = 2, EyesIris = 3, Hair = 4, UpperBodypaint = 5, LowerBodypaint = 6, LowerShoes = 7, + HeadBaked = 8, UpperBaked = 9, LowerBaked = 10, EyesBaked = 11, + LowerSocks = 12, UpperJacket = 13, LowerJacket = 14, UpperGloves = 15, UpperUndershirt = 16, LowerUnderpants = 17, Skirt = 18, + SkirtBaked = 19, HairBaked = 20, + LowerAlpha = 21, UpperAlpha = 22, HeadAlpha = 23, EyesAlpha = 24, HairAlpha = 25, + HeadTattoo = 26, UpperTattoo = 27, LowerTattoo = 28, + HeadUniversalTattoo = 29, UpperUniversalTattoo = 30, LowerUniversalTattoo = 31, SkirtTattoo = 32, HairTattoo = 33, EyesTattoo = 34, + LeftArmTattoo = 35, LeftLegTattoo = 36, Aux1Tattoo = 37, Aux2Tattoo = 38, Aux3Tattoo = 39, + LeftArmBaked = 40, LeftLegBaked = 41, Aux1Baked = 42, Aux2Baked = 43, Aux3Baked = 44, +} + +/// Well-known texture ids the bake pipeline treats specially. +public static class BakeConstants +{ + /// The "no texture" placeholder a wearable carries for a slot it does not paint (IMG_DEFAULT_AVATAR). + public static readonly OpenMetaverse.UUID DefaultAvatarTexture = new("c228d1cf-4b5d-4ba8-84f4-899a0796aa97"); + /// An alpha wearable carrying this texture hides the whole region (IMG_INVISIBLE). + public static readonly OpenMetaverse.UUID InvisibleTexture = new("3a367d1c-bef1-6d43-7595-e88c1e3aadb3"); + + /// The TextureEntry face each bake channel is written to. + public static TextureSlot BakedSlotOf(BakeChannel ch) => ch switch + { + BakeChannel.Head => TextureSlot.HeadBaked, BakeChannel.Upper => TextureSlot.UpperBaked, BakeChannel.Lower => TextureSlot.LowerBaked, + BakeChannel.Eyes => TextureSlot.EyesBaked, BakeChannel.Skirt => TextureSlot.SkirtBaked, BakeChannel.Hair => TextureSlot.HairBaked, + BakeChannel.LeftArm => TextureSlot.LeftArmBaked, BakeChannel.LeftLeg => TextureSlot.LeftLegBaked, + BakeChannel.Aux1 => TextureSlot.Aux1Baked, BakeChannel.Aux2 => TextureSlot.Aux2Baked, BakeChannel.Aux3 => TextureSlot.Aux3Baked, + _ => TextureSlot.Unknown, + }; +} + +public static class WearableKinds +{ + /// The wearable type name avatar_lad.xml uses in wearable= attributes and LLWearable files. + public static string TypeName(WearableKind t) => t switch + { + WearableKind.Shape => "shape", WearableKind.Skin => "skin", WearableKind.Hair => "hair", WearableKind.Eyes => "eyes", WearableKind.Shirt => "shirt", + WearableKind.Pants => "pants", WearableKind.Shoes => "shoes", WearableKind.Socks => "socks", WearableKind.Jacket => "jacket", WearableKind.Gloves => "gloves", + WearableKind.Undershirt => "undershirt", WearableKind.Underpants => "underpants", WearableKind.Skirt => "skirt", WearableKind.Alpha => "alpha", + WearableKind.Tattoo => "tattoo", WearableKind.Physics => "physics", WearableKind.Universal => "universal", _ => "", + }; + + public static WearableKind? FromName(string name) + { + foreach (WearableKind t in Enum.GetValues()) + if (t != WearableKind.Invalid && string.Equals(TypeName(t), name, StringComparison.OrdinalIgnoreCase)) return t; + return null; + } + + public static bool IsBodyPart(WearableKind t) => t is WearableKind.Shape or WearableKind.Skin or WearableKind.Hair or WearableKind.Eyes; +} diff --git a/Source/OpenSimNGC.Appearance.Baking/FidelityCheck.cs b/Source/OpenSimNGC.Appearance.Baking/FidelityCheck.cs new file mode 100644 index 00000000000..4f0b34f3867 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/FidelityCheck.cs @@ -0,0 +1,51 @@ +using OpenMetaverse; + +namespace OpenSimNGC.Appearance.Baking; + +/// +/// The fidelity gate's evidence. Names everything in an outfit the compositor does not reproduce faithfully; +/// an empty list means every wearable type, texture slot and bundled resource the requested channels need is +/// one the compositor handles. The library only reports; whether to refuse is the caller's decision (the +/// web-viewer gateway refuses on any reason; the simulator decides per ADR-005). +/// +public static class FidelityCheck +{ + /// Wearable types whose textures and parameters the compositor handles. + public static readonly WearableKind[] SupportedKinds = + { + WearableKind.Shape, WearableKind.Skin, WearableKind.Hair, WearableKind.Eyes, WearableKind.Shirt, WearableKind.Pants, WearableKind.Shoes, + WearableKind.Socks, WearableKind.Jacket, WearableKind.Gloves, WearableKind.Undershirt, WearableKind.Underpants, WearableKind.Skirt, + WearableKind.Alpha, WearableKind.Tattoo, WearableKind.Physics, WearableKind.Universal, + }; + + public sealed record WornSummary(WearableKind Kind, string Label, IReadOnlyDictionary Textures); + + /// Every reason the outfit cannot be baked faithfully for the given channels; empty means supported. + public static List Check(IReadOnlyList worn, TexLayerCompositor compositor, IEnumerable channels) + { + var reasons = new List(); + var channelList = channels.ToList(); + var slotsInScope = channelList.SelectMany(compositor.SlotsOf).ToHashSet(); + // Several wearables of one type are layered in wear order and the five Bakes-on-Mesh extra bakes are made from + // their layer sets; neither is a refusal. Body parts are still one each: a second shape, skin, hair or eyes would + // be silently ignored by a viewer, so refuse. + foreach (var group in worn.GroupBy(w => w.Kind)) + if (group.Count() > 1 && WearableKinds.IsBodyPart(group.Key)) + reasons.Add($"{group.Count()} {group.Key} wearables worn at once; a body part can only be worn once"); + foreach (var w in worn) + { + if (!SupportedKinds.Contains(w.Kind)) { reasons.Add($"{w.Label}: wearable type {w.Kind} is not composited"); continue; } + foreach (var (idx, id) in w.Textures) + { + if (id == UUID.Zero || id == BakeConstants.DefaultAvatarTexture) continue; + if (TexLayerCompositor.WearableOf(idx) == WearableKind.Invalid) { reasons.Add($"{w.Label}: texture slot {idx} is unknown to the compositor"); continue; } + if (!slotsInScope.Contains(idx) && idx != TextureSlot.Skirt && idx != TextureSlot.SkirtTattoo) + reasons.Add($"{w.Label}: texture slot {idx} is not drawn by any of the bakes being made"); + } + } + foreach (var file in channelList.SelectMany(compositor.ResourceFilesOf).Distinct(StringComparer.OrdinalIgnoreCase)) + if (!compositor.Resources.Exists(file)) + reasons.Add($"bundled resource {file} is missing or unreadable"); + return reasons; + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/IBakeBackend.cs b/Source/OpenSimNGC.Appearance.Baking/IBakeBackend.cs new file mode 100644 index 00000000000..6b92fc5c713 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/IBakeBackend.cs @@ -0,0 +1,23 @@ +namespace OpenSimNGC.Appearance.Baking; + +/// +/// A bake compositor. Implementations take a complete +/// and return one per channel that the worn wearables +/// touch. Implementations must be pure with respect to their inputs: same request, +/// same bytes, same hashes. +/// +public interface IBakeBackend +{ + /// + /// Composite every affected channel for the given request. + /// + /// The complete bake input. + /// Cancellation token; a cancelled bake returns nothing and stores nothing. + /// + /// One result per channel actually produced. Channels with no contributing + /// layer (for example with no skirt worn) are omitted. + /// + /// If is cancelled. + /// If the request is malformed (corrupt wearable text or undecodable texture bytes). + Task> BakeAsync(BakeRequest r, CancellationToken ct); +} diff --git a/Source/OpenSimNGC.Appearance.Baking/J2kCodec.cs b/Source/OpenSimNGC.Appearance.Baking/J2kCodec.cs new file mode 100644 index 00000000000..db5f235209c --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/J2kCodec.cs @@ -0,0 +1,119 @@ +using System.Text; +using CoreJ2K; +using CoreJ2K.Configuration; +using CoreJ2K.Skia; +using SkiaSharp; + +namespace OpenSimNGC.Appearance.Baking; + +/// +/// JPEG 2000 in and out via CoreJ2K.Skia (the encoder the rest of the tree uses; S0a V7). +/// Encoding is SINGLE-TILE: CoreJ2K's default 256x256 tiling produces codestreams that render blank in +/// viewers (upstream #201), so the tile is always the whole image. +/// +public static class J2kCodec +{ + /// Codestream size fields from the SIZ marker segment. + public sealed record SizInfo(int Xsiz, int Ysiz, int XOsiz, int YOsiz, int XTsiz, int YTsiz, int XTOsiz, int YTOsiz, int Csiz) + { + public int TilesX => (int)Math.Ceiling((Xsiz - XTOsiz) / (double)XTsiz); + public int TilesY => (int)Math.Ceiling((Ysiz - YTOsiz) / (double)YTsiz); + public int TileCount => TilesX * TilesY; + public bool SingleTile => TileCount == 1; + } + + /// + /// Decode a J2C codestream or JP2 file to planar RGBA. 1 component = grey, 2 = grey+alpha, 3 = RGB, + /// 4 = RGBA, 5 = RGBA plus the morph mask a viewer appends to its own bakes (exposed as ). + /// + /// If the bytes are not a decodable JPEG 2000 image. + public static RgbaPlanes Decode(byte[] data) + { + if (data is null || data.Length == 0) throw new ArgumentException("empty JPEG 2000 data"); + CoreJ2K.Util.InterleavedImage img; + try { img = J2kImage.FromBytes(data, new J2KDecoderConfiguration()); } + catch (Exception ex) { throw new ArgumentException($"JPEG 2000 decode failed: {ex.Message}", ex); } + using (img) + { + int w = img.Width, h = img.Height, n = w * h, c = img.NumberOfComponents; + var hasAlpha = c == 2 || c >= 4; + var p = new RgbaPlanes(w, h, hasAlpha); + byte[] Comp(int i) { var b = img.GetComponentBytes(i); return b.Length == n ? b : throw new ArgumentException($"JPEG 2000 component {i} has {b.Length} samples, expected {n}"); } + if (c >= 3) + { + Array.Copy(Comp(0), p.R, n); Array.Copy(Comp(1), p.G, n); Array.Copy(Comp(2), p.B, n); + if (c >= 4) Array.Copy(Comp(3), p.A, n); + if (c >= 5) p.Mask = Comp(4); // a viewer bake's morph mask (Docs/MORPH-MASK-PASS.md §2) + } + else + { + var g = Comp(0); + Array.Copy(g, p.R, n); Array.Copy(g, p.G, n); Array.Copy(g, p.B, n); + if (c == 2) Array.Copy(Comp(1), p.A, n); + } + return p; + } + } + + /// The encoder settings for a bake of the given size: one tile, 9/7 irreversible, RPCL, raw codestream (no JP2 wrapper). + public static J2KEncoderConfiguration EncoderConfig(int width, int height, double quality = 0.85) + { + // decomposition levels: 7 at 512 and above, fewer for small images (each level halves the image) + var levels = Math.Clamp((int)Math.Floor(Math.Log2(Math.Min(width, height))) - 2, 1, 7); + var cfg = new J2KEncoderConfiguration(); + // Below 64 px the quality-derived bitrate is smaller than the codestream headers and CoreJ2K refuses + // ("target bitrate too low"); such images are only ever small source textures, so encode them losslessly. + if (Math.Min(width, height) < 64) cfg = cfg.WithLossless(); + else cfg = cfg.WithQuality(quality); + return cfg + .WithTiles(t => t.SetSize(width, height)) + .WithWavelet(w => { if (Math.Min(width, height) >= 64) w.UseIrreversible97(); w.WithDecompositionLevels(levels); }) + .WithProgression(p => p.WithOrder(ProgressionOrder.RPCL)) + .WithFileFormat(false); + } + + /// Encode planar RGBA to a single-tile J2C codestream (four components). + public static byte[] Encode(RgbaPlanes img, double quality = 0.85) + { + using var bmp = img.ToSkBitmap(); + return bmp.EncodeToJ2K(EncoderConfig(img.W, img.H, quality)); + } + + /// + /// Encode a bake the way a viewer uploads one: five components R, G, B, A (visibility alpha) and M (the + /// morph mask, Docs/MORPH-MASK-PASS.md), single tile, same settings as . A null mask is + /// written as 255 everywhere, the value gatherMorphMaskAlpha starts from. + /// + public static byte[] EncodeBake(RgbaPlanes img, byte[]? morphMask, double quality = 0.85) + { + int w = img.W, h = img.H, n = w * h; + if (morphMask is not null && morphMask.Length != n) throw new ArgumentException($"morph mask has {morphMask.Length} samples, expected {n}"); + var mask = morphMask; + if (mask is null) { mask = new byte[n]; Array.Fill(mask, (byte)255); } + // CoreJ2K builds a multi-component source from one greyscale PGM stream per component. + var header = Encoding.ASCII.GetBytes($"P5\n{w} {h}\n255\n"); + Stream Pgm(byte[] plane) { var ms = new MemoryStream(header.Length + n); ms.Write(header); ms.Write(plane, 0, n); ms.Position = 0; return ms; } + var streams = new List { Pgm(img.R), Pgm(img.G), Pgm(img.B), Pgm(img.A), Pgm(mask) }; + try + { + var source = J2kImage.CreateEncodableSource(streams) ?? throw new InvalidOperationException("CoreJ2K returned no encodable source for five planes"); + return J2kImage.ToBytes(source, EncoderConfig(w, h, quality)); + } + finally { foreach (var s in streams) s.Dispose(); } + } + + /// Parse the SIZ marker of a codestream (or of the codestream inside a JP2 file). + public static SizInfo ParseSiz(byte[] data) + { + // find SOC (FF4F) immediately followed by SIZ (FF51) + var at = -1; + for (var i = 0; i + 3 < data.Length; i++) + if (data[i] == 0xFF && data[i + 1] == 0x4F && data[i + 2] == 0xFF && data[i + 3] == 0x51) { at = i + 2; break; } + if (at < 0) throw new FormatException("no SOC+SIZ marker found"); + int U16(int o) => data[o] << 8 | data[o + 1]; + int U32(int o) => data[o] << 24 | data[o + 1] << 16 | data[o + 2] << 8 | data[o + 3]; + var s = at + 2; // after the FF51 marker: Lsiz(2) Rsiz(2) Xsiz Ysiz XOsiz YOsiz XTsiz YTsiz XTOsiz YTOsiz Csiz(2) + if (s + 38 > data.Length) throw new FormatException("truncated SIZ"); + return new SizInfo(U32(s + 4), U32(s + 8), U32(s + 12), U32(s + 16), U32(s + 20), U32(s + 24), U32(s + 28), U32(s + 32), U16(s + 36)); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/OpenSimNGC.Appearance.Baking.csproj b/Source/OpenSimNGC.Appearance.Baking/OpenSimNGC.Appearance.Baking.csproj new file mode 100644 index 00000000000..040bfd5ac6e --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/OpenSimNGC.Appearance.Baking.csproj @@ -0,0 +1,63 @@ + + + + + + net10.0 + enable + enable + OpenSimNGC.Appearance.Baking + OpenSimNGC.Appearance.Baking + true + false + + $(NoWarn);CS1591 + true + OpenSimNGC.Appearance.Baking + Server-side avatar bake compositor shared by OpenSim-Tranquillity regions and the NGC web-viewer gateway. + BSD-3-Clause + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/OpenSimNGC.Appearance.Baking/Raster.cs b/Source/OpenSimNGC.Appearance.Baking/Raster.cs new file mode 100644 index 00000000000..721a3d2f961 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/Raster.cs @@ -0,0 +1,142 @@ +using SkiaSharp; + +namespace OpenSimNGC.Appearance.Baking; + +/// A single 8-bit channel. +public sealed class Plane +{ + public readonly int W, H; + public readonly byte[] Data; + public Plane(int w, int h, byte[]? data = null) { W = w; H = h; Data = data ?? new byte[w * h]; } + public static Plane Filled(int w, int h, byte v) { var p = new Plane(w, h); if (v != 0) Array.Fill(p.Data, v); return p; } +} + +/// Four planar 8-bit channels. `HasAlpha` false means the source had no alpha (A is all 255). +public sealed class RgbaPlanes +{ + public readonly int W, H; + public readonly byte[] R, G, B, A; + public readonly bool HasAlpha; + /// A fifth 8-bit plane when the source carried one: the morph mask of a viewer bake (Docs/MORPH-MASK-PASS.md). Null otherwise. + public byte[]? Mask; + + public RgbaPlanes(int w, int h, bool hasAlpha) + { + W = w; H = h; HasAlpha = hasAlpha; + R = new byte[w * h]; G = new byte[w * h]; B = new byte[w * h]; A = new byte[w * h]; + if (!hasAlpha) Array.Fill(A, (byte)255); + } + + internal RgbaPlanes(int w, int h, byte[] r, byte[] g, byte[] b, byte[] a, bool hasAlpha) { W = w; H = h; R = r; G = g; B = b; A = a; HasAlpha = hasAlpha; } + + /// To an unpremultiplied RGBA8888 SkiaSharp bitmap (for JPEG 2000 encoding or PNG dumps). + public SKBitmap ToSkBitmap() + { + var bmp = new SKBitmap(new SKImageInfo(W, H, SKColorType.Rgba8888, SKAlphaType.Unpremul)); + var px = new SKColor[W * H]; + for (var i = 0; i < px.Length; i++) px[i] = new SKColor(R[i], G[i], B[i], A[i]); + bmp.Pixels = px; + return bmp; + } + + /// From any SkiaSharp bitmap; alpha is taken as present when the bitmap's alpha type is not opaque. + public static RgbaPlanes FromSkBitmap(SKBitmap bmp) + { + var hasAlpha = bmp.AlphaType != SKAlphaType.Opaque; + var p = new RgbaPlanes(bmp.Width, bmp.Height, hasAlpha); + var px = bmp.Pixels; + for (var i = 0; i < px.Length; i++) { p.R[i] = px[i].Red; p.G[i] = px[i].Green; p.B[i] = px[i].Blue; if (hasAlpha) p.A[i] = px[i].Alpha; } + return p; + } + + public RgbaPlanes Resample(int w, int h) + { + if (w == W && h == H) return this; + var p = new RgbaPlanes(w, h, Raster.Resample(R, W, H, w, h), Raster.Resample(G, W, H, w, h), Raster.Resample(B, W, H, w, h), + HasAlpha ? Raster.Resample(A, W, H, w, h) : Filled(w * h, 255), HasAlpha); + if (Mask is not null) p.Mask = Raster.Resample(Mask, W, H, w, h); + return p; + } + + private static byte[] Filled(int n, byte v) { var a = new byte[n]; Array.Fill(a, v); return a; } +} + +/// Pixel helpers: resampling and the viewer's alpha-mask ramp. +public static class Raster +{ + public static byte Mul(byte a, byte b) => (byte)((a * b + 127) / 255); + + /// + /// Resample one channel. Downscaling averages the covered source box (no aliasing on 2048 sources), + /// upscaling is bilinear with clamped edges, which is what the viewer's GL sampling of a smaller layer does. + /// + public static byte[] Resample(byte[] src, int sw, int sh, int dw, int dh) + { + if (sw == dw && sh == dh) return src; + var dst = new byte[dw * dh]; + if (dw <= sw && dh <= sh && sw % dw == 0 && sh % dh == 0) + { + int fx = sw / dw, fy = sh / dh, area = fx * fy; + for (var y = 0; y < dh; y++) + for (var x = 0; x < dw; x++) + { + var sum = 0; + for (var yy = 0; yy < fy; yy++) + { + var row = (y * fy + yy) * sw + x * fx; + for (var xx = 0; xx < fx; xx++) sum += src[row + xx]; + } + dst[y * dw + x] = (byte)((sum + area / 2) / area); + } + return dst; + } + float rx = (float)sw / dw, ry = (float)sh / dh; + for (var y = 0; y < dh; y++) + { + var sy = (y + 0.5f) * ry - 0.5f; + var y0 = (int)MathF.Floor(sy); + var ty = sy - y0; + var ya = Math.Clamp(y0, 0, sh - 1) * sw; + var yb = Math.Clamp(y0 + 1, 0, sh - 1) * sw; + for (var x = 0; x < dw; x++) + { + var sx = (x + 0.5f) * rx - 0.5f; + var x0 = (int)MathF.Floor(sx); + var tx = sx - x0; + var xa = Math.Clamp(x0, 0, sw - 1); + var xb = Math.Clamp(x0 + 1, 0, sw - 1); + var top = src[ya + xa] + (src[ya + xb] - src[ya + xa]) * tx; + var bot = src[yb + xa] + (src[yb + xb] - src[yb + xa]) * tx; + dst[y * dw + x] = (byte)Math.Clamp(top + (bot - top) * ty + 0.5f, 0, 255); + } + } + return dst; + } + + public static Plane Resample(Plane p, int w, int h) => w == p.W && h == p.H ? p : new Plane(w, h, Resample(p.Data, p.W, p.H, w, h)); + + /// + /// The viewer's LLImageTGA::decodeAndProcess: a mask file's grey value is pushed through a ramp whose + /// position follows the parameter weight and whose width is `domain` (0 = hard step at 1 - weight). + /// + public static byte[] ProcessAlpha(byte[] gray, float domain, float weight) + { + var lut = new byte[256]; + var w = Math.Clamp(weight, 0f, 1f); + if (domain > 0f) + { + var scale = 1f / domain; + var offset = (1f - domain) * (1f - w); + var bias = -(scale * offset); + for (var i = 0; i < 256; i++) lut[i] = (byte)Math.Clamp(255f * (i / 255f * scale + bias), 0f, 255f); + } + else + { + var threshold = (byte)(255f * (1f - w)); + for (var i = 0; i < 256; i++) lut[i] = i >= threshold ? (byte)255 : (byte)0; + } + var dst = new byte[gray.Length]; + for (var i = 0; i < gray.Length; i++) dst[i] = lut[gray[i]]; + return dst; + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/ResourceImages.cs b/Source/OpenSimNGC.Appearance.Baking/ResourceImages.cs new file mode 100644 index 00000000000..b1c98dbc083 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/ResourceImages.cs @@ -0,0 +1,79 @@ +namespace OpenSimNGC.Appearance.Baking; + +/// +/// The viewer's bundled character images (parameter masks, skin base, eye whites, aux base) that the layer +/// sets of avatar_lad.xml name by file, decoded once. The library embeds the 56 files avatar_lad.xml references +/// (see THIRD-PARTY-NOTICES.md); a different source can be supplied for tests. +/// +public sealed class ResourceImages +{ + /// Prefix of the embedded character images' manifest resource names. + public const string EmbeddedPrefix = "OpenSimNGC.Appearance.Baking.Data.character."; + + private static readonly Lazy s_embedded = new(() => new ResourceImages(OpenEmbedded), LazyThreadSafetyMode.ExecutionAndPublication); + + /// The images embedded in this assembly. + public static ResourceImages Embedded => s_embedded.Value; + + private readonly Func _open; + private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); + private readonly object _lock = new(); + + /// Returns the bytes of a named file (for example "shirt_sleeve_alpha.tga"), or null when absent. + public ResourceImages(Func open) { _open = open; } + + /// Images from a directory on disk (a viewer's character/ folder). + public static ResourceImages FromDirectory(string dir) + => new(file => { var p = Path.Combine(dir, file); return File.Exists(p) ? File.OpenRead(p) : null; }); + + private static Stream? OpenEmbedded(string file) => typeof(ResourceImages).Assembly.GetManifestResourceStream(EmbeddedPrefix + file); + + private TgaImage? Load(string file) + { + lock (_lock) + { + if (_cache.TryGetValue(file, out var img)) return img; + img = null; + try + { + using var s = _open(file); + if (s is not null) + { + using var ms = new MemoryStream(); + s.CopyTo(ms); + img = Tga.Decode(ms.ToArray()); + } + } + catch { img = null; } + _cache[file] = img; + return img; + } + } + + public bool Exists(string file) => Load(file) is not null; + + /// Grey (mask) reading of a file: grey files as-is, RGBA files by their alpha, RGB files as solid 255. + public Plane? Mask(string file) + { + var img = Load(file); + if (img is null) return null; + var n = img.W * img.H; + var data = new byte[n]; + if (img.IsGray) Array.Copy(img.R, data, n); + else if (img.HasAlpha) Array.Copy(img.A, data, n); + else Array.Fill(data, (byte)255); + return new Plane(img.W, img.H, data); + } + + /// Colour reading of a file: grey files become luminance in RGB with alpha 255, as GL_LUMINANCE would. + public RgbaPlanes? Image(string file) + { + var img = Load(file); + if (img is null) return null; + var n = img.W * img.H; + var p = new RgbaPlanes(img.W, img.H, img.HasAlpha); + Array.Copy(img.R, p.R, n); Array.Copy(img.G, p.G, n); Array.Copy(img.B, p.B, n); + if (img.HasAlpha) Array.Copy(img.A, p.A, n); + return p; + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/SkiaBakeBackend.cs b/Source/OpenSimNGC.Appearance.Baking/SkiaBakeBackend.cs new file mode 100644 index 00000000000..99bf4ba3e95 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/SkiaBakeBackend.cs @@ -0,0 +1,154 @@ +using OpenMetaverse; + +namespace OpenSimNGC.Appearance.Baking; + +/// +/// The library's bake backend: wearable text in, JPEG 2000 bakes out. Wearables are parsed with +/// , textures decoded with , each channel composited by +/// at , encoded as a five-component single-tile +/// codestream (RGB, visibility alpha, morph mask; Docs/MORPH-MASK-PASS.md) and hashed with +/// . Pure with respect to its inputs; no I/O. +/// +public sealed class SkiaBakeBackend : IBakeBackend +{ + private static readonly BakeChannel[] ClassicChannels = { BakeChannel.Head, BakeChannel.Upper, BakeChannel.Lower, BakeChannel.Eyes, BakeChannel.Hair }; + private static readonly (BakeChannel Channel, TextureSlot Slot)[] ExtraChannels = + { + (BakeChannel.LeftArm, TextureSlot.LeftArmTattoo), (BakeChannel.LeftLeg, TextureSlot.LeftLegTattoo), + (BakeChannel.Aux1, TextureSlot.Aux1Tattoo), (BakeChannel.Aux2, TextureSlot.Aux2Tattoo), (BakeChannel.Aux3, TextureSlot.Aux3Tattoo), + }; + + private readonly TexLayerCompositor _compositor; + + /// A backend over the embedded avatar_lad.xml and character images. + public SkiaBakeBackend() : this(new TexLayerCompositor()) { } + + public SkiaBakeBackend(TexLayerCompositor compositor) { _compositor = compositor; } + + public TexLayerCompositor Compositor => _compositor; + + /// JPEG 2000 quality for the encoded bakes (0..1). + public double Quality { get; init; } = 0.85; + + /// The channels an outfit needs: the five classic ones always, the skirt when a skirt is worn, and each extra (Bakes-on-Mesh) channel a worn wearable paints. + public static IReadOnlyList ChannelsFor(IReadOnlyList wearables) + { + var list = new List(ClassicChannels); + if (wearables.Any(w => w.Kind == WearableKind.Skirt)) list.Add(BakeChannel.Skirt); + foreach (var (ch, slot) in ExtraChannels) + if (wearables.Any(w => w.Textures.TryGetValue(slot, out var id) && id != UUID.Zero && id != BakeConstants.DefaultAvatarTexture)) + list.Add(ch); + return list; + } + + /// + public Task> BakeAsync(BakeRequest r, CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + return Task.FromResult(Bake(r, ct)); + } + + /// Synchronous form of . + public IReadOnlyList Bake(BakeRequest r, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(r); + if (r.BakeSize < 8 || r.BakeSize > 4096) throw new ArgumentException($"BakeSize {r.BakeSize} out of range"); + + // 1. wearables (corrupt text is a refusal, ADR-005) + var parsed = new List<(WearableInput Input, ParsedWearable Wearable)>(); + foreach (var w in r.Wearables) + { + ParsedWearable pw; + if (string.IsNullOrWhiteSpace(w.RawText)) + { + // A worn slot with no asset behind it. The viewer counts wearables, not textures + // (LLTexLayerTemplate::updateWearableCache, lltexlayer.cpp:1615-1638), so such a slot is still a + // contributing instance of its type; it just carries no textures and no stored parameters of its + // own, and its parameter values come from the avatar's (BakeRequest.VisualParams). + // Docs/MORPH-MASK-PASS.md §2.4. + pw = new ParsedWearable((WearableKind)w.WearableType, "", new Dictionary(), new Dictionary()); + parsed.Add((w, pw)); + continue; + } + try { pw = WearableParser.Parse(w.RawText); } + catch (FormatException ex) { throw new ArgumentException($"wearable {w.AssetId}: {ex.Message}", ex); } + if ((int)pw.Kind != w.WearableType && w.WearableType is >= 0 and < 255) + pw = pw with { Kind = (WearableKind)w.WearableType }; // the caller's slot wins over a mislabelled asset, as the viewer's does + parsed.Add((w, pw)); + } + var wearables = parsed.Select(p => p.Wearable).ToList(); + var channels = ChannelsFor(wearables); + + // 1b. the channels actually asked for. The fidelity gate below still sees the full set, so a partial bake + // reports the same refusals as a full one; only the compositing loop and the decode are narrowed. + var requested = r.Channels is null ? channels : channels.Where(r.Channels.Contains).ToList(); + + // 2. textures (undecodable bytes are a refusal; absent ones are reported per channel). Only the textures + // the requested channels can draw are decoded: a texture reaches the canvas through a slot, so one no + // requested channel's layer set names cannot affect any bake this call produces. On a full bake that is + // every texture in the request, exactly as before. + var drawnSlots = requested.SelectMany(_compositor.SlotsOf).ToHashSet(); + var wanted = new HashSet(); + foreach (var pw in wearables) + foreach (var (slot, id) in pw.Textures) + if (drawnSlots.Contains(slot)) wanted.Add(id); + var decoded = new Dictionary(); + foreach (var (id, tex) in r.Textures) + { + if (!wanted.Contains(id)) continue; + ct.ThrowIfCancellationRequested(); + var t0 = BakeTimings.Now; + try { decoded[id] = J2kCodec.Decode(tex.J2kBytes); } + catch (ArgumentException ex) { throw new ArgumentException($"texture {id}: {ex.Message}", ex); } + r.Timings?.AddDecode(t0, (long)decoded[id].W * decoded[id].H); + } + + var worn = new List(); + var summaries = new List(); + foreach (var (input, pw) in parsed) + { + var label = $"{pw.Kind} {input.AssetId.ToString()[..8]}"; + var textures = new Dictionary(); + foreach (var (slot, id) in pw.Textures) + if (decoded.TryGetValue(id, out var img)) textures[slot] = img; + worn.Add(new WornWearable { Kind = pw.Kind, Label = label, Params = pw.Params, TextureIds = pw.Textures, Textures = textures }); + summaries.Add(new FidelityCheck.WornSummary(pw.Kind, label, pw.Textures)); + } + + // 3. the fidelity gate's evidence, once for the outfit; the caller decides what to do with it + var refusals = FidelityCheck.Check(summaries, _compositor, channels); + + // 4. each requested channel + var results = new List(requested.Count); + foreach (var ch in requested) + { + ct.ThrowIfCancellationRequested(); + var slots = _compositor.SlotsOf(ch).ToHashSet(); + var missing = new List(); + foreach (var pw in wearables) + foreach (var (slot, id) in pw.Textures) + if (slots.Contains(slot) && id != UUID.Zero && id != BakeConstants.DefaultAvatarTexture && !decoded.ContainsKey(id) && !missing.Contains(id)) + missing.Add(id); + + var tComposite = BakeTimings.Now; + var composite = _compositor.Bake(ch, worn, r.BakeSize, r.VisualParams); + r.Timings?.AddComposite(tComposite); + var tEncode = BakeTimings.Now; + var bytes = J2kCodec.EncodeBake(composite.Image, composite.MorphMask, Quality); + r.Timings?.AddEncode(tEncode, bytes.Length); + var unsupported = composite.Layers + .Where(l => l.Status == "skipped" && (l.Detail.Contains("missing", StringComparison.Ordinal) || l.Detail.Contains("unknown", StringComparison.Ordinal))) + .Select(l => $"{l.Layer}: {l.Detail}") + .ToList(); + var notes = composite.Layers.Select(l => $"{l.Layer} {l.Status}: {l.Detail}").ToList(); + if (composite.Invisible) notes.Insert(0, "invisible: the whole region is hidden by an alpha wearable"); + var fidelity = new FidelityReport(unsupported, missing, notes, refusals); + results.Add(new BakeResult(ch, bytes, BakeHash.Compute(ch, r), fidelity) { NothingDrawn = composite.NothingDrawn }); + } + return results; + } + + /// Whether the shape's `male` parameter (80) says male, from the parsed wearables. + public bool IsMale(IReadOnlyList wearables) + => _compositor.IsMale(wearables.Select(w => new WornWearable { Kind = w.Kind, Params = w.Params, TextureIds = w.Textures }).ToList()); +} diff --git a/Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md b/Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md new file mode 100644 index 00000000000..76d5e7783db --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md @@ -0,0 +1,110 @@ +# Third-party notices — OpenSimNGC.Appearance.Baking + +## `Data/avatar_lad.xml` + +**Origin:** Linden Lab Second Life viewer, file `indra/newview/character/avatar_lad.xml`. +This is the avatar "LAD" (Linden Avatar Definition) file: it defines the visual +parameters, wearable layers, texture layer sets and morph targets that the bake +compositor reproduces server-side (ADR-007). + +**Licence:** GNU Lesser General Public License v2.1, with the Second Life viewer +linking exception granted by Linden Lab (the "Linden Lab Second Life Viewer +Source License" exception that permits linking the viewer source with non-LGPL +code). The file is embedded unmodified as a data resource; it is not compiled or +linked into executable code. The full LGPL 2.1 text is in the viewer's `LICENSE` +file and at . + +**Copied from:** a local checkout of the viewer source tree at `F:\viewer-develop` +on 2026-09-03. That directory is not a git repository (no `.git`), so the exact +upstream commit could not be read from it. Identifying data that could be read: + +| Field | Value | +|---|---| +| `indra/newview/VIEWER_VERSION.txt` | `26.1.1` | +| `avatar_lad.xml` `wearable_definition_version` | `22` | +| `avatar_lad.xml` `version` | `2.0` | +| File size | 354,436 bytes | +| SHA-256 | `ace7a7aebac5bee593d2ec2f5a487404cf53859e54537d00e53173c8fa1ee2cd` | + +The SSB design documents (`Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md` §3) +name the viewer commit used for the wire contract as `62033f2`; that identity could +not be confirmed against `F:\viewer-develop` and is recorded here as a claim, not a fact. + +**Modifications:** none. Byte-for-byte copy. + +## `Data/character/*.tga` (56 files) + +**Origin:** Linden Lab Second Life viewer, directory `indra/newview/character/`: the parameter alpha masks, +the skin base images, eye whites and the Bakes-on-Mesh `aux_base.tga` that the `layer_set` definitions of +`avatar_lad.xml` name by file. The compositor cannot reproduce a bake without them (every clothing layer is +shaped by one), so they ship with the library alongside `avatar_lad.xml` (ADR-007 extended in S0b). + +**Licence:** the same as `avatar_lad.xml` above: GNU LGPL 2.1 with the Linden Lab viewer linking exception. +Embedded unmodified as data resources; not compiled or linked. + +**Source:** the Linden Lab viewer source tree at `F:\viewer-develop` (viewer 26.1.1 per +`indra/newview/VIEWER_VERSION.txt`), directory `indra/newview/character/`. The files were first taken (S0b, +2026-09-03) from the copy redistributed inside the `LibreMetaverse` 3.1.4 NuGet package; in S0d every one of +the 56 was re-verified byte for byte (SHA-256 below) against the viewer tree and found identical, so the viewer +tree is the recorded source. `avatar_lad.xml` also names `head_wrinkles_highlights_alpha.tga`, which exists in +neither the viewer tree nor the package; only a bump-pass layer references it and bump layers are never rendered +into a bake (see `Docs/MORPH-MASK-PASS.md`), so nothing is affected. + +| File | SHA-256 | +|---|---| +| `aux_base.tga` | `2fc3cc8a65c332ba03abba73e747501f2d4cd1127539e7ad3a36820a6b1c9932` | +| `blush_alpha.tga` | `167f13ae91a6f48b07df261469f670f70fef9116d7e6f024ebb35c1ce07649bf` | +| `body_skingrain.tga` | `272efb5be2f339ea34d510dbf454e8d7c3397c67eec5fd26fc4a38a5ce03c70e` | +| `bodyfreckles_alpha.tga` | `da5c28bcebd359e03d27dbd8532efc3dcdfef2606d3490f30f15231be9abaefc` | +| `bump_face_wrinkles.tga` | `6ec85b56861e85320533bede582ff66edb62370333bfa627d52a361e4b8b1eed` | +| `bump_head_base.tga` | `7081ba6dc675c7c80cf5140aa12ca108b6fd33290e83958e9e8cfaa2c6c353ad` | +| `bump_lowerbody_base.tga` | `c93f907dc57841095965e4626b1a62709d5e9805c685ab3747731d9847f8079e` | +| `bump_pants_wrinkles.tga` | `41e0e2b9687f4181651ef498a7cffd77cd97e166113dcd98b04dfb3af18d8e5d` | +| `bump_shirt_wrinkles.tga` | `3265c2bf5446d171a31f30222e016e026d171e65c74dae09eb136bc271d16f44` | +| `bump_upperbody_base.tga` | `4733155e00b7c7f915e457328f9ad4e9c4ad927e59ea01a01ef5e3a167b2f54e` | +| `eyebrows_alpha.tga` | `24754e92c79df93ec8340203d83053a979adf4fa50d5cb5462422bae68f170a7` | +| `eyeliner_alpha.tga` | `415d8211facb46604d671ac81ffccd562bda045d32038cad8e7710af4a7b248c` | +| `eyeshadow_inner_alpha.tga` | `c93f47bafa17d611a2a324a67476f928647bd95d5f0bd4a3c8a16a0c73ad4233` | +| `eyeshadow_outer_alpha.tga` | `ba8172b57b8634c72a022f6ee8421f5c61bb2e89079640fbb5a9876db8e2aec9` | +| `eyewhite.tga` | `868162aca011392fcdbf58edf3dd9bd0d4a2c35f48f7d5e5473403b493f6e1e9` | +| `facehair_chincurtains_alpha.tga` | `a855d69630f404ea71a95c8df9a9c32a64e226514973296928a782a33c596b54` | +| `facehair_moustache_alpha.tga` | `b373c83536c7f2bbb3456b615a9ce5c8826e3ee11af0cc2dccd094070bc6be24` | +| `facehair_sideburns_alpha.tga` | `2c530eeae1dd16ea512080fb2245409dfdc6fab037ad73253256350e66ef681d` | +| `facehair_soulpatch_alpha.tga` | `edbdd8ceedb7edba67e4fe542e949536737e29786dddbc84ffa8506fa678e79d` | +| `freckles_alpha.tga` | `8d00e3786f9ad9e4be969182fa0d5983128049536dc69a553d69b93549628da8` | +| `glove_length_alpha.tga` | `d3ca442c95377459455de9f8cf36d36d2120e4ff5a6212fc2331ad5cf9228e7b` | +| `gloves_fingers_alpha.tga` | `8ac5b237a0647a7373c0bd0e0fcd967dc510ba49f0201d7f88a24774a5968711` | +| `head_alpha.tga` | `6b8b7d9bc4dd54f6b2a1d977875f02d00e687608749089fa7a504531718cf877` | +| `head_color.tga` | `6cd54f034a8c7fe3a1faf2aa66a489ea7e2db06d112a7c3e4fbd185046f8ac62` | +| `head_hair.tga` | `e0c0136059337b115d3d533d9b55244ab035f3ac42765dc72419e870ee5472d2` | +| `head_highlights_alpha.tga` | `f28fd1ddeb1ee7e4280536dfedf76698867ba2b13aababab14a74d06fdf69253` | +| `head_shading_alpha.tga` | `06aff93404b5a164d2c0631c8e0140094b723aafca985362a7ef2c672923e0de` | +| `head_skingrain.tga` | `59d8ec4188cf8a66996804829beb7f807a9fa8d99f94c3bd2f4763ff995de029` | +| `jacket_length_lower_alpha.tga` | `e6f7c87f356258c317d49a4e9b28f6a64893c136cb10fd39fbb18999c7282a66` | +| `jacket_length_upper_alpha.tga` | `2d40a4c818a05c3112c8185c9ac5fe96a6cc1675722ec73ee310e1a3d5d55801` | +| `jacket_open_lower_alpha.tga` | `5ac2b754b48859e0706519798205198ba59912d5319b7a185e725ce45f6b3415` | +| `jacket_open_upper_alpha.tga` | `efb61d9d080a0221f0677d513b98d608814548b95283f4b6fab37cfbe52e6604` | +| `lipgloss_alpha.tga` | `3ea04c5662aa36927051f728530ee8b9e8d6d74473ed63b3e00f5573665bf540` | +| `lips_mask.tga` | `e8540a42e40d10e92be1598194707148bf15a55f6553f9b293cc3004881ca883` | +| `lipstick_alpha.tga` | `afd140146466bf0f95d39e2b50be267de047b7666925a2d054e9a86cc2e98f96` | +| `lowerbody_color.tga` | `cdf46efc0284dd74d8a26e9adb0fa769a2b2130bd712c4f2b566f3cc89c4d5f6` | +| `lowerbody_highlights_alpha.tga` | `64d2cd0e3ce6e506e0a805121a4b047b375f3359a74eb6c77582f3f45dadef99` | +| `lowerbody_shading_alpha.tga` | `b6608d4ff3dac02aadcabb8d6565a54a245d4705f604078c9b3786bac3bd3d10` | +| `nailpolish_alpha.tga` | `cb88a2bcf379abe8f524eca960bad4ca7f2668af3357dc012407ad98202cbc19` | +| `pants_length_alpha.tga` | `dcbdfd9be0fe0ee3948f18681ec3076b060dac9be4ab90ee24ba29cdb06fd23b` | +| `pants_waist_alpha.tga` | `416a348b0b1cd7883f093e00d14c402e82ebf883c713cea0fa5c1614c388ddb7` | +| `rosyface_alpha.tga` | `c54fd26df68e6f13f9e312f925c785e911b54f00ba4f2e1039a97f0c1478171b` | +| `shirt_bottom_alpha.tga` | `d4e2e56c1b75eb1f173e004e2ec889a49f36d830975607a53308dba5da12ba0a` | +| `shirt_collar_alpha.tga` | `07dcfb155b833bb6ed12cb975437ae9929ef000f1c911fe846c24e322817a9fb` | +| `shirt_collar_back_alpha.tga` | `ff012cade37e65581f4e0f753541b945e0731baa80d5a6bd1a40ec9baf0b4981` | +| `shirt_sleeve_alpha.tga` | `363654ffdd35d80f42c13e13c834d349e925210b940a2abfe97c2d9f833eb359` | +| `shoe_height_alpha.tga` | `7fa78cb7925f13a3115c552fcca7d033ed7eb7897ae2853e2b6706aabf38ca25` | +| `skirt_length_alpha.tga` | `35e63c27f666b3b99b6214104635c4748ecc1968e9c12a182a6eb3933b839846` | +| `skirt_slit_back_alpha.tga` | `d1a16ec74e572a6d7cc2e7abfb1d9562a3af07e71bd24783c045f49099c3bcc2` | +| `skirt_slit_front_alpha.tga` | `f5d88fc2e2d2e003cdc8340a493b62e939544f11caa0d5e657dd3cda70bf2f6e` | +| `skirt_slit_left_alpha.tga` | `f1edee562c09b787e89efcf4f5977d0d3c281caa48fce4c49c3d6339d0d4c4e3` | +| `skirt_slit_right_alpha.tga` | `727aaffc9a8aba1ac47a9259a8af1a641d1453f3ca6528188cd96e83ce56d2a4` | +| `upperbody_color.tga` | `50571c9aee7289162eb10d7b5ddfea2c1feb9bf7941f255b828e5c7d88f5543f` | +| `upperbody_highlights_alpha.tga` | `b98e34fea0659604cf713263028641cadedc0c694f683c6c997089e951c48d72` | +| `upperbody_shading_alpha.tga` | `1f91154b45efdfabae9433b0e65bb2bf88721ca561c8762db1d932dcba666415` | +| `upperbodyfreckles_alpha.tga` | `ddf4c3aa27990bdd7d33c8762a87f50e87e32f0b5cb5660e108df00c8a68cf93` | diff --git a/Source/OpenSimNGC.Appearance.Baking/TexLayerCompositor.cs b/Source/OpenSimNGC.Appearance.Baking/TexLayerCompositor.cs new file mode 100644 index 00000000000..124fff5fde6 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/TexLayerCompositor.cs @@ -0,0 +1,685 @@ +using OpenMetaverse; + +namespace OpenSimNGC.Appearance.Baking; + +/// One worn wearable as the compositor sees it: its type, its stored parameters, its decoded textures by slot. +public sealed class WornWearable +{ + public WearableKind Kind; + public string Label = ""; + public IReadOnlyDictionary Params = new Dictionary(); + public IReadOnlyDictionary TextureIds = new Dictionary(); + public IReadOnlyDictionary Textures = new Dictionary(); +} + +/// What happened to one layer of one bake: drawn, or skipped and why. This is the coverage evidence. +public sealed record LayerReport(string Layer, string Status, string Detail, WearableKind? Wearable); + +public sealed class CompositeResult +{ + public required RgbaPlanes Image; + public required List Layers; + public bool Invisible; + /// + /// True when no layer of this set drew anything: every colour layer was skipped. It is a fact about the + /// layer decisions, never about the pixels — a layer that drew a fully transparent texture (a bald hair) has + /// drawn, and so has the case, where an alpha wearable deliberately hides the region. + /// Both of those are legitimate bakes; an undrawn channel is not one, because the set's alpha starts opaque + /// and nothing carved it, so it encodes as a solid image of whatever the canvas was cleared to. + /// + public bool NothingDrawn; + /// + /// The bake's 5th component: LLTexLayerSet::gatherMorphMaskAlpha — 255 everywhere, multiplied by the alpha + /// mask of every contributing instance of the set's morph-mask layers (Docs/MORPH-MASK-PASS.md §2). + /// + public required byte[] MorphMask; +} + +/// +/// The bake compositor. It interprets the `layer_set` definitions of avatar_lad.xml the way the viewer's +/// LLTexLayerSet does (layer order, per-layer alpha masks with the domain ramp, colour parameters and global +/// colours, driver parameters, sex gating, write-all-channels layers, visibility masks for the final alpha), on +/// plain byte planes at the requested size. Extracted from the web-viewer gateway (ADR-003), where it replaced +/// the LibreMetaverse Baker, which tiles sub-1024 layers into a mosaic, ignores every parameter-driven layer, +/// and applies masks with hard edges. +/// +public sealed class TexLayerCompositor +{ + /// The local_texture names of avatar_lad.xml, in TextureSlot order (the viewer's texture dictionary). + private static readonly Dictionary TextureByName = new(StringComparer.OrdinalIgnoreCase) + { + ["head_bodypaint"] = TextureSlot.HeadBodypaint, ["upper_shirt"] = TextureSlot.UpperShirt, ["lower_pants"] = TextureSlot.LowerPants, + ["eyes_iris"] = TextureSlot.EyesIris, ["hair_grain"] = TextureSlot.Hair, ["upper_bodypaint"] = TextureSlot.UpperBodypaint, + ["lower_bodypaint"] = TextureSlot.LowerBodypaint, ["lower_shoes"] = TextureSlot.LowerShoes, ["lower_socks"] = TextureSlot.LowerSocks, + ["upper_jacket"] = TextureSlot.UpperJacket, ["lower_jacket"] = TextureSlot.LowerJacket, ["upper_gloves"] = TextureSlot.UpperGloves, + ["upper_undershirt"] = TextureSlot.UpperUndershirt, ["lower_underpants"] = TextureSlot.LowerUnderpants, ["skirt"] = TextureSlot.Skirt, + ["lower_alpha"] = TextureSlot.LowerAlpha, ["upper_alpha"] = TextureSlot.UpperAlpha, ["head_alpha"] = TextureSlot.HeadAlpha, + ["eyes_alpha"] = TextureSlot.EyesAlpha, ["hair_alpha"] = TextureSlot.HairAlpha, ["head_tattoo"] = TextureSlot.HeadTattoo, + ["upper_tattoo"] = TextureSlot.UpperTattoo, ["lower_tattoo"] = TextureSlot.LowerTattoo, ["head_universal_tattoo"] = TextureSlot.HeadUniversalTattoo, + ["upper_universal_tattoo"] = TextureSlot.UpperUniversalTattoo, ["lower_universal_tattoo"] = TextureSlot.LowerUniversalTattoo, + ["skirt_tattoo"] = TextureSlot.SkirtTattoo, ["hair_tattoo"] = TextureSlot.HairTattoo, ["eyes_tattoo"] = TextureSlot.EyesTattoo, + ["leftarm_tattoo"] = TextureSlot.LeftArmTattoo, ["leftleg_tattoo"] = TextureSlot.LeftLegTattoo, ["aux1_tattoo"] = TextureSlot.Aux1Tattoo, + ["aux2_tattoo"] = TextureSlot.Aux2Tattoo, ["aux3_tattoo"] = TextureSlot.Aux3Tattoo, + }; + + /// Which wearable type carries each texture slot (the viewer's texture dictionary). + public static WearableKind WearableOf(TextureSlot idx) => idx switch + { + TextureSlot.HeadBodypaint or TextureSlot.UpperBodypaint or TextureSlot.LowerBodypaint => WearableKind.Skin, + TextureSlot.UpperShirt => WearableKind.Shirt, + TextureSlot.LowerPants => WearableKind.Pants, + TextureSlot.EyesIris => WearableKind.Eyes, + TextureSlot.Hair => WearableKind.Hair, + TextureSlot.LowerShoes => WearableKind.Shoes, + TextureSlot.LowerSocks => WearableKind.Socks, + TextureSlot.UpperJacket or TextureSlot.LowerJacket => WearableKind.Jacket, + TextureSlot.UpperGloves => WearableKind.Gloves, + TextureSlot.UpperUndershirt => WearableKind.Undershirt, + TextureSlot.LowerUnderpants => WearableKind.Underpants, + TextureSlot.Skirt => WearableKind.Skirt, + TextureSlot.LowerAlpha or TextureSlot.UpperAlpha or TextureSlot.HeadAlpha or TextureSlot.EyesAlpha or TextureSlot.HairAlpha => WearableKind.Alpha, + TextureSlot.HeadTattoo or TextureSlot.UpperTattoo or TextureSlot.LowerTattoo => WearableKind.Tattoo, + TextureSlot.HeadUniversalTattoo or TextureSlot.UpperUniversalTattoo or TextureSlot.LowerUniversalTattoo or TextureSlot.SkirtTattoo + or TextureSlot.HairTattoo or TextureSlot.EyesTattoo or TextureSlot.LeftArmTattoo or TextureSlot.LeftLegTattoo + or TextureSlot.Aux1Tattoo or TextureSlot.Aux2Tattoo or TextureSlot.Aux3Tattoo => WearableKind.Universal, + _ => WearableKind.Invalid, + }; + + /// The `body_region` of the layer_set a bake channel is made from. + public static string RegionOf(BakeChannel bt) => bt switch + { + BakeChannel.Head => "head", BakeChannel.Upper => "upper_body", BakeChannel.Lower => "lower_body", BakeChannel.Eyes => "eyes", + BakeChannel.Hair => "hair", BakeChannel.Skirt => "skirt", BakeChannel.LeftArm => "leftarm", BakeChannel.LeftLeg => "leftleg", + BakeChannel.Aux1 => "aux1", BakeChannel.Aux2 => "aux2", BakeChannel.Aux3 => "aux3", _ => "", + }; + + /// The wearable type name avatar_lad.xml uses in `wearable=` attributes. + public static string TypeName(WearableKind t) => WearableKinds.TypeName(t); + + private readonly AvatarLad _lad; + private readonly ResourceImages _res; + private readonly Dictionary<(string File, int Size), Plane> _maskCache = new(); + private readonly Dictionary<(string File, int Size), RgbaPlanes> _imageCache = new(); + private readonly Dictionary<(string File, float Domain, byte Weight, int Size), Plane> _processedCache = new(); + private readonly object _cacheLock = new(); + + public TexLayerCompositor(AvatarLad lad, ResourceImages res) { _lad = lad; _res = res; } + + /// A compositor over the embedded avatar_lad.xml and character images. + public TexLayerCompositor() : this(AvatarLad.Embedded, ResourceImages.Embedded) { } + + public AvatarLad Lad => _lad; + public ResourceImages Resources => _res; + + /// The texture slots the given bake draws or masks with (its layers' local textures). + public IEnumerable SlotsOf(BakeChannel bt) + { + if (!_lad.LayerSets.TryGetValue(RegionOf(bt), out var set)) yield break; + foreach (var l in set.Layers) + if (l.LocalTexture is not null && TextureByName.TryGetValue(l.LocalTexture, out var idx)) yield return idx; + } + + /// Every bundled file the given bake's colour layers, masks and visibility masks need. + public IEnumerable ResourceFilesOf(BakeChannel bt) + { + if (!_lad.LayerSets.TryGetValue(RegionOf(bt), out var set)) yield break; + if (set.StaticAlphaFile is not null) yield return set.StaticAlphaFile; + foreach (var l in set.Layers) + { + if (l.Bump) continue; + if (l.StaticImage is not null) yield return l.StaticImage; + foreach (var pid in l.AlphaParams) + if (_lad.Params.TryGetValue(pid, out var def) && def.Alpha is { TgaFile: { Length: > 0 } f }) yield return f; + } + } + + /// Every visual parameter id the given bake's layers read: colour and alpha parameters, the global colours' parameters, and the drivers that set them. + public IReadOnlyCollection ParamsOf(BakeChannel bt) + { + var ids = new HashSet(); + if (!_lad.LayerSets.TryGetValue(RegionOf(bt), out var set)) return ids; + foreach (var l in set.Layers) + { + if (l.Bump) continue; + ids.UnionWith(l.ColorParams); + ids.UnionWith(l.AlphaParams); + if (l.GlobalColor is { Length: > 0 } g && _lad.GlobalColors.TryGetValue(g, out var gids)) ids.UnionWith(gids); + } + // drivers: a stored parameter (e.g. the shirt's sleeve length 800) that drives a layer parameter (600) + for (var depth = 0; depth < 3; depth++) + { + var before = ids.Count; + foreach (var def in _lad.Params.Values) + if (def.Driven.Count > 0 && def.Driven.Any(d => ids.Contains(d.Id))) ids.Add(def.Id); + if (ids.Count == before) break; + } + return ids; + } + + // ---------------------------------------------------------------- parameters + + private sealed class ParamState + { + public readonly Dictionary Direct = new(); + public readonly Dictionary Derived = new(); + public readonly HashSet WornTypes = new(); + /// Each wearable's own values (direct + driven) for the parameters its type owns, so several + /// wearables of one type each render their layer with their own sleeve length, colour, etc. (LLWearable::writeToAvatar per instance). + public readonly Dictionary> PerInstance = new(); + /// The wearable whose layer is being rendered, if any. + public WornWearable? Instance; + public bool Male; + /// Each rendered layer instance's alpha mask (LLTexLayer::mAlphaCache), for the morph-mask gather. + public readonly Dictionary<(LayerDef Layer, WornWearable? Instance), byte[]> Masks = new(ReferenceTupleComparer.Instance); + } + + private sealed class ReferenceTupleComparer : IEqualityComparer<(LayerDef Layer, WornWearable? Instance)> + { + public static readonly ReferenceTupleComparer Instance = new(); + public bool Equals((LayerDef Layer, WornWearable? Instance) a, (LayerDef Layer, WornWearable? Instance) b) => ReferenceEquals(a.Layer, b.Layer) && ReferenceEquals(a.Instance, b.Instance); + public int GetHashCode((LayerDef Layer, WornWearable? Instance) k) => HashCode.Combine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(k.Layer), k.Instance is null ? 0 : System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(k.Instance)); + } + + /// + /// LLTexLayerInterface::getWearableType: the layer's local texture's wearable type, or, without a local texture, + /// the single wearable type its colour and alpha parameters belong to; Invalid when they belong to several or none. + /// + public WearableKind LayerKind(LayerDef layer) + { + if (layer.LocalTexture is not null && TextureByName.TryGetValue(layer.LocalTexture, out var slot)) return WearableOf(slot); + var kind = WearableKind.Invalid; + foreach (var id in layer.ColorParams.Concat(layer.AlphaParams)) + { + if (!_lad.Params.TryGetValue(id, out var def) || def.Wearable is not { Length: > 0 } owner || WearableKinds.FromName(owner) is not { } k) continue; + if (kind != WearableKind.Invalid && k != kind) return WearableKind.Invalid; + kind = k; + } + return kind; + } + + private ParamState ResolveParams(IReadOnlyList worn, IReadOnlyDictionary? overlay) + { + var st = new ParamState(); + foreach (var w in worn) + { + st.WornTypes.Add(w.Kind); + var owner = TypeName(w.Kind); + foreach (var (id, v) in w.Params) + { + // A wearable only carries the parameters its type owns (LLWearable::createVisualParams); anything else is noise. + if (!_lad.Params.TryGetValue(id, out var def) || !string.Equals(def.Wearable, owner, StringComparison.OrdinalIgnoreCase)) continue; + st.Direct[id] = Math.Clamp(v, def.Min, def.Max); + } + } + // Caller-supplied values (BakeRequest.VisualParams) fill in only what no worn wearable stores. + if (overlay is not null) + foreach (var (id, v) in overlay) + if (!st.Direct.ContainsKey(id) && _lad.Params.TryGetValue(id, out var def)) st.Direct[id] = Math.Clamp(v, def.Min, def.Max); + // Drivers: a stored parameter that drives others sets them (LLDriverParam::setWeight) unless they are stored themselves. + PropagateDrivers(st.Direct, st.Derived); + foreach (var w in worn) + { + var owner = TypeName(w.Kind); + var direct = new Dictionary(); + foreach (var (id, v) in w.Params) + if (_lad.Params.TryGetValue(id, out var def) && string.Equals(def.Wearable, owner, StringComparison.OrdinalIgnoreCase)) + direct[id] = Math.Clamp(v, def.Min, def.Max); + var derived = new Dictionary(); + PropagateDrivers(direct, derived); + foreach (var (id, v) in derived) direct.TryAdd(id, v); + st.PerInstance[w] = direct; + } + st.Male = Weight(st, 80) > 0.5f; + return st; + } + + private void PropagateDrivers(Dictionary direct, Dictionary derived) + { + var frontier = direct.Keys.ToList(); + for (var depth = 0; depth < 3 && frontier.Count > 0; depth++) + { + var next = new List(); + foreach (var id in frontier) + { + var def = _lad.Params[id]; + if (def.Driven.Count == 0) continue; + var input = direct.TryGetValue(id, out var dv) ? dv : derived[id]; + foreach (var d in def.Driven) + { + if (direct.ContainsKey(d.Id) || derived.ContainsKey(d.Id) || !_lad.Params.TryGetValue(d.Id, out var driven)) continue; + derived[d.Id] = DrivenWeight(def, d, driven, input); + next.Add(d.Id); + } + } + frontier = next; + } + } + + /// LLDriverParam::getDrivenWeight: the trapezoid min1/max1/max2/min2 of the driver maps onto the driven range. + private static float DrivenWeight(ParamDef driver, DrivenInfo d, ParamDef driven, float input) + { + float dmin = driven.Min, dmax = driven.Max; + if (input <= d.Min1) return d.Min1 == d.Max1 && d.Min1 <= driver.Min ? dmax : dmin; + if (input <= d.Max1) return dmin + (input - d.Min1) / (d.Max1 - d.Min1) * (dmax - dmin); + if (input <= d.Max2) return dmax; + if (input <= d.Min2) return dmax + (input - d.Max2) / (d.Min2 - d.Max2) * (dmin - dmax); + return d.Max2 >= driver.Max ? dmax : dmin; + } + + private float Weight(ParamState st, int id) + { + if (!_lad.Params.TryGetValue(id, out var def)) return 0f; + // the wearable being rendered owns its parameters: its own values first (multi-wearables), then the merged outfit + if (st.Instance is { } inst && string.Equals(def.Wearable, TypeName(inst.Kind), StringComparison.OrdinalIgnoreCase) + && st.PerInstance.TryGetValue(inst, out var own) && own.TryGetValue(id, out var ov)) return Math.Clamp(ov, def.Min, def.Max); + if (st.Direct.TryGetValue(id, out var v) || st.Derived.TryGetValue(id, out v)) return Math.Clamp(v, def.Min, def.Max); + return def.Default; + } + + /// The weight a parameter contributes for this avatar: its value, or the default when the parameter is for the other sex. + private float Effective(ParamState st, ParamDef def) + { + var applies = def.Sex == ParamSex.Both || (def.Sex == ParamSex.Male) == st.Male; + return applies ? Weight(st, def.Id) : def.Default; + } + + /// LLTexLayerParamAlpha::getSkip: zero weight with skip_if_zero, or the owning wearable type not worn. + private bool Skip(ParamState st, ParamDef def) + { + if (def.Alpha is { SkipIfZero: true } && MathF.Abs(Effective(st, def)) < 1e-6f) return true; + if (def.Wearable is { Length: > 0 } w && WearableKinds.FromName(w) is { } t && t != WearableKind.Invalid && !st.WornTypes.Contains(t)) return true; + return false; + } + + /// LLTexLayerParamColor::getNetColor: the stops interpolated by the raw weight (the viewer does not normalise by min/max here). + private Rgba ParamColor(ParamState st, ParamDef def) + { + var colors = def.Color!.Colors; + if (colors.Count == 0) return Rgba.Transparent; + var last = colors.Count - 1; + var scaled = Effective(st, def) * last; + var i0 = Math.Clamp((int)scaled, 0, last); + if (i0 == last) return colors[last]; + return Rgba.Lerp(colors[i0], colors[i0 + 1], scaled - i0); + } + + /// LLTexLayer::calculateTexLayerColor. + private Rgba FoldColors(ParamState st, IEnumerable ids, Rgba net) + { + foreach (var id in ids) + { + if (!_lad.Params.TryGetValue(id, out var def) || def.Color is null) continue; + var c = ParamColor(st, def); + net = def.Color.Op switch + { + ColorOp.Add => net + c, + ColorOp.Multiply => net * c, + ColorOp.Blend => Rgba.Lerp(net, c, Weight(st, id)), + _ => net, + }; + } + return net.Clamp(); + } + + private Rgba GlobalColor(ParamState st, string name) + => _lad.GlobalColors.TryGetValue(name, out var ids) && ids.Count > 0 ? FoldColors(st, ids, Rgba.Transparent) : Rgba.White; + + /// LLTexLayer::findNetColor. Returns whether a colour was specified (a flat fill is drawn only then). + private bool NetColor(ParamState st, LayerDef layer, out Rgba color) + { + if (layer.ColorParams.Count > 0) + { + var start = layer.GlobalColor is { Length: > 0 } g ? GlobalColor(st, g) : layer.FixedColor.A > 0 ? layer.FixedColor : Rgba.Transparent; + color = FoldColors(st, layer.ColorParams, start); + return true; + } + if (layer.GlobalColor is { Length: > 0 } gc) { color = GlobalColor(st, gc); return true; } + if (layer.FixedColor.A > 0) { color = layer.FixedColor; return true; } + color = Rgba.White; + return false; + } + + // ---------------------------------------------------------------- resources at bake size + + private Plane? MaskAt(string file, int size) + { + lock (_cacheLock) + { + if (_maskCache.TryGetValue((file, size), out var p)) return p; + var m = _res.Mask(file); + if (m is null) return null; + p = Raster.Resample(m, size, size); + _maskCache[(file, size)] = p; + return p; + } + } + + private RgbaPlanes? ImageAt(string file, int size) + { + lock (_cacheLock) + { + if (_imageCache.TryGetValue((file, size), out var p)) return p; + var img = _res.Image(file); + if (img is null) return null; + p = img.Resample(size, size); + _imageCache[(file, size)] = p; + return p; + } + } + + /// A parameter mask at the given weight: the ramp is applied at the file's own resolution, then resampled (as GL samples the processed texture). + private Plane? ProcessedMask(string file, float domain, float weight, int size) + { + var q = (byte)Math.Clamp((int)MathF.Round(Math.Clamp(weight, 0f, 1f) * 255f), 0, 255); + var key = (file, domain, q, size); + lock (_cacheLock) + { + if (_processedCache.TryGetValue(key, out var p)) return p; + var raw = _res.Mask(file); + if (raw is null) return null; + var processed = new Plane(raw.W, raw.H, Raster.ProcessAlpha(raw.Data, domain, q / 255f)); + p = Raster.Resample(processed, size, size); + _processedCache[key] = p; + return p; + } + } + + // ---------------------------------------------------------------- the bake + + public CompositeResult Bake(BakeChannel bt, IReadOnlyList worn, int size) => Bake(bt, worn, size, null); + + /// Values for parameters no worn wearable stores (a caller's merged view); never override a wearable's own value. + public CompositeResult Bake(BakeChannel bt, IReadOnlyList worn, int size, IReadOnlyDictionary? overlayParams) + { + var region = RegionOf(bt); + if (!_lad.LayerSets.TryGetValue(region, out var set)) throw new InvalidOperationException($"avatar_lad.xml has no layer_set for {region}"); + var st = ResolveParams(worn, overlayParams); + var n = size * size; + var canvas = new RgbaPlanes(size, size, hasAlpha: true); + Array.Fill(canvas.A, (byte)255); // LLTexLayerSet::render clears to opaque black + var reports = new List(); + var maskLayers = set.Layers.Where(l => l.VisibilityMask).ToList(); + + // An alpha wearable whose texture is the invisible one hides the whole region. + foreach (var ml in maskLayers) + if (ml.LocalTexture is not null && TextureByName.TryGetValue(ml.LocalTexture, out var midx)) + foreach (var w in worn.Where(w => w.Kind == WearableOf(midx))) + if (w.TextureIds.TryGetValue(midx, out var mid) && mid == BakeConstants.InvisibleTexture) + { + Array.Clear(canvas.A); + reports.Add(new LayerReport(ml.Name, "invisible", $"{w.Label}: IMG_INVISIBLE hides the whole {region} bake", w.Kind)); + var m255 = new byte[n]; Array.Fill(m255, (byte)255); + return new CompositeResult { Image = canvas, Layers = reports, Invisible = true, MorphMask = m255 }; + } + + foreach (var layer in set.Layers) + { + if (layer.Bump) { continue; } // bump pass: viewer-side normal maps, never part of the uploaded bake + if (layer.VisibilityMask) continue; // applied to the final alpha below + if (layer.LocalTexture is not null) + { + if (!TextureByName.TryGetValue(layer.LocalTexture, out var idx)) { reports.Add(new LayerReport(layer.Name, "skipped", $"unknown local_texture {layer.LocalTexture}", null)); continue; } + var type = WearableOf(idx); + var instances = worn.Where(w => w.Kind == type).ToList(); + if (instances.Count == 0) { reports.Add(new LayerReport(layer.Name, "skipped", $"no {type} worn", type)); continue; } + foreach (var w in instances) + { + w.Textures.TryGetValue(idx, out var tex); + st.Instance = w; + RenderLayer(set, layer, st, canvas, size, w, idx, tex, reports); + st.Instance = null; + } + } + else RenderLayer(set, layer, st, canvas, size, null, TextureSlot.Unknown, null, reports); // no local texture: a plain LLTexLayer, rendered once with the avatar's merged parameters (lltexlayer.cpp:64, :290-297) + } + + // LLTexLayerSet::gatherMorphMaskAlpha (Docs/MORPH-MASK-PASS.md §2): 255, times the mask of every contributing + // instance of the set's morph-mask layers. A layer with a local texture is an LLTexLayerTemplate and contributes + // once per worn wearable of its type (lltexlayer.cpp:1706-1714); a layer without one is a plain LLTexLayer + // (isUserSettable() is mLocalTexture != -1, lltexlayer.cpp:64, :290-297) and contributes exactly once, with the + // avatar's merged parameters (the last-worn wearable's values), whatever is worn. + var morph = new byte[n]; + Array.Fill(morph, (byte)255); + if (_lad.MorphMaskLayers.TryGetValue(region, out var morphLayers)) + { + foreach (var layer in set.Layers) + { + if (layer.Bump || !morphLayers.Contains(layer.Name) || layer.AlphaParams.Count == 0) continue; // addAlphaMask: only hasAlphaParams() layers + var kind = LayerKind(layer); + List instances; + if (layer.LocalTexture is null) instances = new List { null }; // plain LLTexLayer: once + else + { + if (kind == WearableKind.Invalid) { reports.Add(new LayerReport(layer.Name, "morph", "no wearable type: no instances", null)); continue; } + // LLTexLayerTemplate::gatherAlphaMasks (lltexlayer.cpp:1710-1719) takes getLayer(num_wearables - 1) + // only — "For rendering morph masks, we only want to use the top wearable" — unlike render(), which + // loops over every instance. A wearable counts as worn whether or not it has a texture asset + // (updateWearableCache, :1615-1638). Docs/MORPH-MASK-PASS.md §2.2, §2.4. + var ofKind = worn.Where(w => w.Kind == kind).ToList(); + if (ofKind.Count == 0) { reports.Add(new LayerReport(layer.Name, "morph", $"no {kind} worn: mask left at 255", kind)); continue; } + instances = new List { ofKind[^1] }; + } + foreach (var w in instances) + { + if (!st.Masks.TryGetValue((layer, w), out var mask)) + { + // not rendered in the colour pass: render its mask on demand, as addAlphaMask does + st.Instance = w; + NetColor(st, layer, out var color); + RgbaPlanes? tex = null; + if (w is not null && layer.LocalTexture is not null && TextureByName.TryGetValue(layer.LocalTexture, out var slot)) w.Textures.TryGetValue(slot, out tex); + mask = ComputeMask(layer, st, canvas.A, size, w, tex, color, new List(), out _); + st.Instance = null; + if (mask is null) { reports.Add(new LayerReport(layer.Name, "morph", $"{(w is null ? "" : w.Label + ": ")}mask file missing; not applied", kind)); continue; } + } + long sum = 0; + for (var i = 0; i < n; i++) { morph[i] = (byte)((morph[i] * (mask[i] + 1)) >> 8); sum += mask[i]; } + reports.Add(new LayerReport(layer.Name, "morph", $"{(w is null ? "once (plain layer)" : w.Label)}: morph mask *= layer mask (mean {sum / (double)n:F1})", w?.Kind ?? kind)); + } + } + } + + // LLTexLayerSet::renderAlphaMaskTextures: the bake's alpha is 1 (or the set's static alpha file), times every visibility mask. + if (set.StaticAlphaFile is not null) + { + var a = MaskAt(set.StaticAlphaFile, size); + if (a is not null) Array.Copy(a.Data, canvas.A, n); + } + else if (set.ClearAlpha || maskLayers.Count > 0) Array.Fill(canvas.A, (byte)255); + foreach (var ml in maskLayers) + { + if (ml.StaticImage is not null) + { + var m = ml.StaticIsMask ? MaskAt(ml.StaticImage, size) : ImageAt(ml.StaticImage, size) is { } im ? new Plane(size, size, im.A) : null; + if (m is null) { reports.Add(new LayerReport(ml.Name, "skipped", $"resource {ml.StaticImage} missing", null)); continue; } + for (var i = 0; i < n; i++) canvas.A[i] = Raster.Mul(canvas.A[i], m.Data[i]); + reports.Add(new LayerReport(ml.Name, "mask", $"alpha *= {ml.StaticImage}", null)); + } + else if (ml.LocalTexture is not null && TextureByName.TryGetValue(ml.LocalTexture, out var midx)) + { + var type = WearableOf(midx); + var instances = worn.Where(w => w.Kind == type).ToList(); + if (instances.Count == 0) { reports.Add(new LayerReport(ml.Name, "skipped", $"no {type} worn", type)); continue; } + foreach (var w in instances) + { + if (!w.Textures.TryGetValue(midx, out var tex)) { reports.Add(new LayerReport(ml.Name, "skipped", $"{w.Label}: no {midx} texture", type)); continue; } + if (!tex.HasAlpha) { reports.Add(new LayerReport(ml.Name, "mask", $"{w.Label}: {midx} has no alpha channel (opaque)", type)); continue; } + var t = tex.Resample(size, size); + for (var i = 0; i < n; i++) canvas.A[i] = Raster.Mul(canvas.A[i], t.A[i]); + reports.Add(new LayerReport(ml.Name, "mask", $"{w.Label}: alpha *= {midx} ({tex.W}x{tex.H})", type)); + } + } + } + // LLTexLayerSet::render draws each layer or skips it; if every one skipped, nothing reached the canvas. + // Deliberate all-transparent output (a drawn but transparent layer, or the IMG_INVISIBLE short-circuit + // above) is NOT this: those drew. + var nothingDrawn = !reports.Any(l => l.Status == "drawn"); + return new CompositeResult { Image = canvas, Layers = reports, MorphMask = morph, NothingDrawn = nothingDrawn }; + } + + /// + /// LLTexLayer::renderMorphMasks, the mask part: the layer's alpha parameters accumulated (additive from 0, or a leading + /// multiply parameter from the current alpha), times the local texture's alpha, times the static mask, times the layer + /// colour's alpha. Null only when a mask file is missing. reports the all-parameters-skipped + /// case (the viewer then draws the layer through this all-zero mask). + /// + private byte[]? ComputeMask(LayerDef layer, ParamState st, byte[] currentAlpha, int size, WornWearable? w, RgbaPlanes? tex, Rgba color, List used, out bool allSkipped) + { + var n = size * size; + var first = _lad.Params.GetValueOrDefault(layer.AlphaParams[0]); + var mask = first?.Alpha is { MultiplyBlend: true } ? (byte[])currentAlpha.Clone() : new byte[n]; + var applied = 0; + foreach (var pid in layer.AlphaParams) + { + if (!_lad.Params.TryGetValue(pid, out var def) || def.Alpha is null || string.IsNullOrEmpty(def.Alpha.TgaFile)) continue; + if (Skip(st, def)) { used.Add($"{def.Name}#{pid}=skip"); continue; } + var eff = Effective(st, def); + var p = ProcessedMask(def.Alpha.TgaFile, def.Alpha.Domain, eff, size); + if (p is null) { allSkipped = false; return null; } + if (def.Alpha.MultiplyBlend) for (var i = 0; i < n; i++) mask[i] = Raster.Mul(mask[i], p.Data[i]); + else for (var i = 0; i < n; i++) mask[i] = (byte)Math.Min(255, mask[i] + p.Data[i]); + used.Add($"{def.Name}#{pid}={eff:F2}{(def.Alpha.MultiplyBlend ? "*" : "+")}"); + applied++; + } + allSkipped = applied == 0 && first?.Alpha is not { MultiplyBlend: true }; + if (allSkipped) return mask; // every parameter skipped and nothing added: an all-zero mask, as the viewer's cleared alpha + if (tex is { HasAlpha: true } && layer.LocalTexture is not null) + { + var t = tex.Resample(size, size); + for (var i = 0; i < n; i++) mask[i] = Raster.Mul(mask[i], t.A[i]); + used.Add("texture alpha*"); + } + if (layer.StaticImage is not null && layer.StaticIsMask) + { + var m = MaskAt(layer.StaticImage, size); + if (m is not null) { for (var i = 0; i < n; i++) mask[i] = Raster.Mul(mask[i], m.Data[i]); used.Add($"{layer.StaticImage}*"); } + } + if (MathF.Abs(color.A - 1f) > 1e-4f) + { + var ca = (byte)Math.Round(color.A * 255); + for (var i = 0; i < n; i++) mask[i] = Raster.Mul(mask[i], ca); + } + return mask; + } + + /// LLTexLayer::render for one layer instance. + private void RenderLayer(LayerSetDef set, LayerDef layer, ParamState st, RgbaPlanes canvas, int size, WornWearable? w, TextureSlot idx, RgbaPlanes? tex, List reports) + { + var n = size * size; + var who = w is null ? "" : $"{w.Label}: "; + var colorSpecified = NetColor(st, layer, out var color); + if (color.A < 1e-4f) { reports.Add(new LayerReport(layer.Name, "skipped", $"{who}colour alpha 0 {color}", w?.Kind)); return; } + var detail = new List(); + if (colorSpecified) detail.Add($"colour {color}"); + + byte[]? mask = null; + if (layer.AlphaParams.Count > 0) + { + // LLTexLayer::renderMorphMasks: the alpha channel becomes the mask; additive params start from 0, a leading multiply param from what is there. + var used = new List(); + mask = ComputeMask(layer, st, canvas.A, size, w, tex, color, used, out var allSkipped); + if (mask is null) { reports.Add(new LayerReport(layer.Name, "skipped", $"{who}a mask file is missing", w?.Kind)); return; } + st.Masks[(layer, w)] = mask; // LLTexLayer::mAlphaCache: reused by the morph-mask gather + if (allSkipped) + { + // Every mask parameter skipped and nothing to add to an empty mask: the viewer draws the layer through an all-zero mask, i.e. nothing. + Array.Copy(mask, canvas.A, n); + reports.Add(new LayerReport(layer.Name, "skipped", $"{who}every mask parameter skipped [{string.Join(", ", used)}]", w?.Kind)); + return; + } + Array.Copy(mask, canvas.A, n); // the viewer leaves the mask in the alpha channel (the skirt's shape comes from this) + detail.Add($"masks [{string.Join(", ", used)}]"); + } + + var drew = false; + if (layer.LocalTexture is not null && !layer.LocalTextureAlphaOnly) + { + if (tex is null) { reports.Add(new LayerReport(layer.Name, "skipped", $"{who}no {idx} texture on the wearable", w?.Kind)); return; } + var t = tex.Resample(size, size); + DrawImage(canvas, t, color, mask, layer.WriteAllChannels, n); + detail.Insert(0, $"{who}{idx} {tex.W}x{tex.H}{(tex.HasAlpha ? "+alpha" : "")} -> {size}"); + drew = true; + } + if (layer.StaticImage is not null) + { + if (layer.StaticIsMask) + { + var m = MaskAt(layer.StaticImage, size); + if (m is null) { reports.Add(new LayerReport(layer.Name, "skipped", $"{who}resource {layer.StaticImage} missing", w?.Kind)); return; } + // A mask file colours the layer's own colour through the file's grey (the fixed-function GL_ALPHA reading avatar_lad.xml was written for). + var alpha = mask ?? MaskTimes(m.Data, color.A, n); + DrawFill(canvas, color, alpha, layer.WriteAllChannels, n); + detail.Add($"{layer.StaticImage} (mask)"); + } + else + { + var img = ImageAt(layer.StaticImage, size); + if (img is null) { reports.Add(new LayerReport(layer.Name, "skipped", $"{who}resource {layer.StaticImage} missing", w?.Kind)); return; } + DrawImage(canvas, img, color, mask, layer.WriteAllChannels, n); + detail.Add($"{layer.StaticImage}{(img.HasAlpha ? "+alpha" : "")}"); + } + drew = true; + } + if ((layer.LocalTexture is null || layer.LocalTextureAlphaOnly) && layer.StaticImage is null && colorSpecified) + { + var alpha = mask ?? Filled(n, (byte)Math.Round(color.A * 255)); + DrawFill(canvas, color, alpha, layer.WriteAllChannels, n); + detail.Add("flat fill"); + drew = true; + } + reports.Add(new LayerReport(layer.Name, drew ? "drawn" : "skipped", string.Join("; ", detail), w?.Kind)); + } + + private static byte[] Filled(int n, byte v) { var a = new byte[n]; Array.Fill(a, v); return a; } + + private static byte[] MaskTimes(byte[] m, float a, int n) + { + var ca = (byte)Math.Round(Math.Clamp(a, 0f, 1f) * 255); + if (ca == 255) return m; + var o = new byte[n]; + for (var i = 0; i < n; i++) o[i] = Raster.Mul(m[i], ca); + return o; + } + + /// Texture (or bundled image) modulated by the layer colour, blended by the mask or by its own alpha; replace mode writes all channels. + private static void DrawImage(RgbaPlanes dst, RgbaPlanes src, Rgba color, byte[]? mask, bool replace, int n) + { + byte cr = (byte)Math.Round(color.R * 255), cg = (byte)Math.Round(color.G * 255), cb = (byte)Math.Round(color.B * 255), ca = (byte)Math.Round(color.A * 255); + var tint = cr != 255 || cg != 255 || cb != 255; + for (var i = 0; i < n; i++) + { + var r = tint ? Raster.Mul(src.R[i], cr) : src.R[i]; + var g = tint ? Raster.Mul(src.G[i], cg) : src.G[i]; + var b = tint ? Raster.Mul(src.B[i], cb) : src.B[i]; + var a = mask is not null ? mask[i] : Raster.Mul(src.A[i], ca); + if (replace) { dst.R[i] = r; dst.G[i] = g; dst.B[i] = b; dst.A[i] = a; continue; } + if (a == 0) continue; + Blend(dst, i, r, g, b, a, mask is null); + } + } + + private static void DrawFill(RgbaPlanes dst, Rgba color, byte[] alpha, bool replace, int n) + { + byte cr = (byte)Math.Round(color.R * 255), cg = (byte)Math.Round(color.G * 255), cb = (byte)Math.Round(color.B * 255); + for (var i = 0; i < n; i++) + { + var a = alpha[i]; + if (replace) { dst.R[i] = cr; dst.G[i] = cg; dst.B[i] = cb; dst.A[i] = a; continue; } + if (a == 0) continue; + Blend(dst, i, cr, cg, cb, a, alphaFromSource: false); + } + } + + /// src over dst by a; with a source-alpha blend the alpha channel follows GL's (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) too. + private static void Blend(RgbaPlanes dst, int i, byte r, byte g, byte b, byte a, bool alphaFromSource) + { + if (a == 255) { dst.R[i] = r; dst.G[i] = g; dst.B[i] = b; if (alphaFromSource) dst.A[i] = 255; return; } + dst.R[i] = (byte)(dst.R[i] + ((r - dst.R[i]) * a + 127) / 255); + dst.G[i] = (byte)(dst.G[i] + ((g - dst.G[i]) * a + 127) / 255); + dst.B[i] = (byte)(dst.B[i] + ((b - dst.B[i]) * a + 127) / 255); + if (alphaFromSource) dst.A[i] = (byte)((a * a + dst.A[i] * (255 - a) + 127) / 255); + } + + /// The sex the shape's `male` parameter (80) says; needed by a client to pick a body. + public bool IsMale(IReadOnlyList worn) => ResolveParams(worn, null).Male; +} diff --git a/Source/OpenSimNGC.Appearance.Baking/Tga.cs b/Source/OpenSimNGC.Appearance.Baking/Tga.cs new file mode 100644 index 00000000000..a9eb1e0e598 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/Tga.cs @@ -0,0 +1,109 @@ +namespace OpenSimNGC.Appearance.Baking; + +/// A decoded TGA: planar 8-bit channels plus what the file actually carried. +public sealed class TgaImage +{ + public readonly int W, H; + public readonly bool IsGray, HasAlpha; + /// Grey files: the grey values. Colour files: red. + public readonly byte[] R, G, B, A; + internal TgaImage(int w, int h, bool isGray, bool hasAlpha, byte[] r, byte[] g, byte[] b, byte[] a) { W = w; H = h; IsGray = isGray; HasAlpha = hasAlpha; R = r; G = g; B = b; A = a; } +} + +/// +/// A small Truevision TGA reader for the viewer's bundled character images: grey (types 3/11), true-colour +/// 24/32-bit (types 2/10) and colour-mapped 8-bit (types 1/9), uncompressed or RLE, either row origin. +/// Replaces the client library's Targa decoder so the library carries no client dependency. +/// +public static class Tga +{ + public static TgaImage Decode(byte[] data) + { + if (data.Length < 18) throw new FormatException("TGA: header too short"); + int idLen = data[0], cmapType = data[1], type = data[2]; + int cmapFirst = data[3] | data[4] << 8, cmapLen = data[5] | data[6] << 8, cmapBits = data[7]; + int w = data[12] | data[13] << 8, h = data[14] | data[15] << 8, bpp = data[16], desc = data[17]; + var topLeft = (desc & 0x20) != 0; + if (w <= 0 || h <= 0) throw new FormatException("TGA: bad size"); + var pos = 18 + idLen; + + byte[]? pal = null; + var palBytes = 0; + if (cmapType == 1) + { + palBytes = (cmapBits + 7) / 8; + pal = new byte[cmapLen * palBytes]; + Array.Copy(data, pos, pal, 0, pal.Length); + pos += pal.Length; + } + + var rle = type >= 9; + var baseType = rle ? type - 8 : type; + var pixBytes = (bpp + 7) / 8; + var n = w * h; + var raw = new byte[n * pixBytes]; + if (!rle) + { + if (data.Length < pos + raw.Length) throw new FormatException("TGA: truncated pixel data"); + Array.Copy(data, pos, raw, 0, raw.Length); + } + else + { + var o = 0; + while (o < raw.Length) + { + if (pos >= data.Length) throw new FormatException("TGA: truncated RLE data"); + int packet = data[pos++]; + var count = (packet & 0x7F) + 1; + if ((packet & 0x80) != 0) + { + if (pos + pixBytes > data.Length) throw new FormatException("TGA: truncated RLE run"); + for (var i = 0; i < count && o < raw.Length; i++) { Array.Copy(data, pos, raw, o, pixBytes); o += pixBytes; } + pos += pixBytes; + } + else + { + var len = Math.Min(count * pixBytes, raw.Length - o); + if (pos + len > data.Length) throw new FormatException("TGA: truncated RLE literal"); + Array.Copy(data, pos, raw, o, len); + o += len; pos += len; + } + } + } + + bool isGray = baseType == 3, hasAlpha = false; + var r = new byte[n]; var g = new byte[n]; var b = new byte[n]; var a = new byte[n]; + Array.Fill(a, (byte)255); + for (var y = 0; y < h; y++) + { + var srcRow = topLeft ? y : h - 1 - y; + for (var x = 0; x < w; x++) + { + var s = (srcRow * w + x) * pixBytes; + var d = y * w + x; + switch (baseType) + { + case 3: + r[d] = g[d] = b[d] = raw[s]; + if (pixBytes == 2) { a[d] = raw[s + 1]; hasAlpha = true; } + break; + case 2: + if (pixBytes >= 3) { b[d] = raw[s]; g[d] = raw[s + 1]; r[d] = raw[s + 2]; } + else { var v = raw[s] | raw[s + 1] << 8; b[d] = (byte)((v & 0x1F) << 3); g[d] = (byte)(((v >> 5) & 0x1F) << 3); r[d] = (byte)(((v >> 10) & 0x1F) << 3); } + if (pixBytes == 4) { a[d] = raw[s + 3]; hasAlpha = true; } + break; + case 1: + if (pal is null) throw new FormatException("TGA: colour-mapped image without a colour map"); + var idx = (pixBytes == 2 ? raw[s] | raw[s + 1] << 8 : raw[s]) - cmapFirst; + var p = Math.Clamp(idx, 0, cmapLen - 1) * palBytes; + if (palBytes >= 3) { b[d] = pal[p]; g[d] = pal[p + 1]; r[d] = pal[p + 2]; if (palBytes == 4) { a[d] = pal[p + 3]; hasAlpha = true; } } + else { var v = pal[p] | pal[p + 1] << 8; b[d] = (byte)((v & 0x1F) << 3); g[d] = (byte)(((v >> 5) & 0x1F) << 3); r[d] = (byte)(((v >> 10) & 0x1F) << 3); } + break; + default: + throw new FormatException($"TGA: unsupported image type {type}"); + } + } + } + return new TgaImage(w, h, isGray, hasAlpha, r, g, b, a); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/VisualParamEncoder.cs b/Source/OpenSimNGC.Appearance.Baking/VisualParamEncoder.cs new file mode 100644 index 00000000000..f4b1e79a348 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/VisualParamEncoder.cs @@ -0,0 +1,77 @@ +namespace OpenSimNGC.Appearance.Baking; + +/// +/// The VisualParams block of AgentSetAppearance, built the way a viewer builds it. +/// +/// LLAgent::sendAgentSetAppearance sends one byte per parameter whose group is 0 (tweakable) or 3 (transmit, +/// not tweakable), iterating the avatar's parameter map in id order: 253 parameters with the current +/// avatar_lad.xml. Each byte is F32_to_U8(weight, min, max) = (U8)(((clamp(weight) - min) / (max - min)) * 255) +/// in float32, truncated, not rounded. The weight is the avatar's current value: the value stored in the worn +/// wearable of the parameter's type (the topmost when several are worn); for a type that is not worn the viewer +/// keeps whatever the sim last stored for the avatar, so those bytes are carried forward from the sim's blob; +/// only when there is no such blob does the avatar_lad.xml default apply. +/// +public static class VisualParamEncoder +{ + public sealed record Result(byte[] Bytes, int FromWearables, int Carried, int Defaults, IReadOnlyList Ids); + + /// The transmitted parameters in send order. + public static List SendList(AvatarLad lad) => lad.Params.Values.Where(p => p.Group is 0 or 3).OrderBy(p => p.Id).ToList(); + + /// Position of a parameter id in the send order, or -1. + public static int IndexOf(AvatarLad lad, int id) => SendList(lad).FindIndex(p => p.Id == id); + + /// llmath.h F32_to_U8: float32 arithmetic, truncation. + public static byte F32ToU8(float val, float lower, float upper) + { + val = Math.Clamp(val, lower, upper); + if (upper == lower) return 0; + float x = ((val - lower) / (upper - lower)) * 255f; + return (byte)x; + } + + /// The parameter definitions. + /// every worn wearable in wear order (later of a type is on top) with its stored parameters + /// the VisualParams the sim last sent for this avatar (its stored row), or null + public static Result Encode(AvatarLad lad, IEnumerable<(WearableKind Kind, IReadOnlyDictionary Params)> worn, IReadOnlyList? carried) + { + var list = SendList(lad); + // topmost wearable per type name + var byType = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var (kind, prms) in worn) + { + var name = WearableKinds.TypeName(kind); + if (name.Length == 0) continue; + if (byType.TryGetValue(name, out var have)) + { + var merged = new Dictionary(have); + foreach (var (k, v) in prms) merged[k] = v; + byType[name] = merged; + } + else byType[name] = prms; + } + var useCarried = carried is not null && carried.Count == list.Count; + var bytes = new byte[list.Count]; + int fromWearables = 0, carriedN = 0, defaults = 0; + for (var i = 0; i < list.Count; i++) + { + var p = list[i]; + if (p.Wearable is { Length: > 0 } owner && byType.TryGetValue(owner, out var prms) && prms.TryGetValue(p.Id, out var v)) + { + bytes[i] = F32ToU8(v, p.Min, p.Max); + fromWearables++; + } + else if (useCarried) + { + bytes[i] = carried![i]; + carriedN++; + } + else + { + bytes[i] = F32ToU8(p.Default, p.Min, p.Max); + defaults++; + } + } + return new Result(bytes, fromWearables, carriedN, defaults, list.Select(p => p.Id).ToList()); + } +} diff --git a/Source/OpenSimNGC.Appearance.Baking/WearableParser.cs b/Source/OpenSimNGC.Appearance.Baking/WearableParser.cs new file mode 100644 index 00000000000..e97f2e3cad4 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking/WearableParser.cs @@ -0,0 +1,74 @@ +using System.Globalization; +using OpenMetaverse; + +namespace OpenSimNGC.Appearance.Baking; + +/// A wearable asset's body, parsed: its type, name, stored parameters and textures by slot. +public sealed record ParsedWearable(WearableKind Kind, string Name, IReadOnlyDictionary Params, IReadOnlyDictionary Textures); + +/// +/// Reads the LLWearable text format every bodypart and clothing asset uses: +/// +/// LLWearable version 22 +/// Name +/// Description +/// permissions 0 { ... } +/// sale_info 0 { ... } +/// type 4 +/// parameters 3 +/// 800 1 +/// ... +/// textures 1 +/// 1 <uuid> +/// +/// Tolerant of the braces blocks and of blank lines; strict about the sections it needs. +/// +public static class WearableParser +{ + public static ParsedWearable Parse(string text) + { + if (string.IsNullOrWhiteSpace(text)) throw new FormatException("wearable: empty asset"); + var lines = text.Replace("\r\n", "\n").Split('\n'); + if (!lines[0].TrimStart().StartsWith("LLWearable", StringComparison.Ordinal)) throw new FormatException("wearable: not an LLWearable asset"); + var name = lines.Length > 1 ? lines[1].Trim() : ""; + + WearableKind? kind = null; + var prms = new Dictionary(); + var textures = new Dictionary(); + var i = 2; + while (i < lines.Length) + { + var line = lines[i++].Trim(); + if (line.Length == 0) continue; + var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); + switch (parts[0]) + { + case "type" when parts.Length >= 2 && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var t): + kind = (WearableKind)t; + break; + case "parameters" when parts.Length >= 2 && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var pc): + for (var k = 0; k < pc && i < lines.Length; k++) + { + var p = lines[i++].Trim().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); + if (p.Length < 2) { k--; if (lines[i - 1].Trim().Length == 0) continue; throw new FormatException($"wearable: bad parameter line '{lines[i - 1]}'"); } + if (!int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) throw new FormatException($"wearable: bad parameter id '{p[0]}'"); + if (!float.TryParse(p[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) throw new FormatException($"wearable: bad parameter value '{p[1]}'"); + prms[id] = v; + } + break; + case "textures" when parts.Length >= 2 && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var tc): + for (var k = 0; k < tc && i < lines.Length; k++) + { + var p = lines[i++].Trim().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); + if (p.Length < 2) { k--; if (lines[i - 1].Trim().Length == 0) continue; throw new FormatException($"wearable: bad texture line '{lines[i - 1]}'"); } + if (!int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var slot)) throw new FormatException($"wearable: bad texture slot '{p[0]}'"); + if (!UUID.TryParse(p[1], out var id)) throw new FormatException($"wearable: bad texture id '{p[1]}'"); + textures[(TextureSlot)slot] = id; + } + break; + } + } + if (kind is null) throw new FormatException("wearable: no 'type' line"); + return new ParsedWearable(kind.Value, name, prms, textures); + } +} From 1acfa7a57e9d44ae6311ab920171dbb13aeffd03 Mon Sep 17 00:00:00 2001 From: JohnLegionH Date: Sun, 20 Sep 2026 12:46:14 -0500 Subject: [PATCH 2/8] feat(ais): Inventory API v3 region caps module, tests, docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AISv3Module registers the InventoryAPIv3 and LibraryAPIv3 caps behind a config flag and serves the viewer's AIS v3 surface: fetch (item/category/COF, depth contract taken from the viewer's own fetch path), item and category mutations with delta envelopes, SlamFolder with a compensating restore, purge descendents, and CopyLibraryCategory. Supporting service work: IInventoryService gains a folder-delete overload that can delete outside Trash (implemented across XInventoryService, the local and remote connectors, HGInventoryBroker, the HG inventory services and the XInventory in-connector); system folders are resolved deterministically rather than by folders[0]; the duplicate-system-folder race is narrowed. Removes the seven-file OpenSim.Services.AISv3 ASP.NET webapi template (WeatherForecast scaffolding) that the region module replaces. Docs/feature/ais-v3 carries the spec extracted from the viewer, the A5 live checklist, the malformed-LLSD audit and the A6-A13 investigation records. Folded from feature/ais-v3, 76 commits: 133c3e3d2d docs(ais): AIS v3 spec extracted from viewer + tree state 8aa8104875 feat(ais): AISv3Module skeleton, router, envelope fixtures; remove webapi template 03e428c124 feat(ais): inventory backend for the fetch surface 9be9d2e58d feat(ais): fetch routes + LibraryAPIv3 (read-only) 9d43eb2a9d test(ais): HTTP acceptance harness for the fetch surface 4d9a661f02 docs(ais): A-R3 refined, A-Q6 opened, orphans scope recorded f8bfaa2b41 feat(ais): item and category mutations with delta envelopes 72ee7be828 test(ais): mutation harness d5afbce36d docs(ais): depth contract settled from the viewer's fetch path e2ad97af70 feat(ais): folder deletion outside Trash via a new IInventoryService overload 2bf8ed89f4 fix(ais): protected-folder set taken from the viewer's own table 193d7e6fc7 feat(ais): SlamFolder with compensating restore 664608d125 test(ais): slam and create harness, fault injection 20ab17ffb7 fix(ais): create body shapes taken from the viewer's builders d18897eb63 feat(ais): purge descendents ac5a43ed69 feat(ais): CopyLibraryCategory 295884ed7f test(ais): purge and copy harness 6fc4ab3426 fix(ais): categories create shape from asAISCreateCatLLSD 4d725a7b2d feat(ais): advertise InventoryAPIv3 and LibraryAPIv3 behind the region flag 3448d6e380 docs(ais): A5 live checklist fa4eb5f14a fix(ais): register the caps as variable-path handlers ee6d2a3554 docs(ais): A6 and A5 ledger rows, risk A-R7 6cd13a3645 fix(ais): resolve system folders deterministically, not by folders[0] 68bfa60735 fix(inventory): narrow the race that creates duplicate system folders e800eb61b6 docs(ais): fill the A6 and A7 ledger commit hashes 4165077a5f docs(ais): A5 checklist run 2026-09-04 — results and constraints 455b28e97f docs(ais): fill the A8 ledger commit hash 80e234820a docs(ais): step 8 passed; step 13 fallback confirmed server-side 3fb35f63b5 docs(ais): A9 - no duplicate COFs exist; the pattern is the HG suitcase skeleton, correcting A7-DUPLICATE-COF.md 96a8bad2f4 docs(ais): fill the A9 ledger commit hash dd90969f6e docs(ais): A10 - step 10 reopened, no cause established 40a1438774 docs(ais): fill the A10 ledger commit hash f1d32d6ba6 docs(ais): A-Q15 closed - the item-create fallback is LL's, not Firestorm's 25ce8b0ad4 feat(ais): log mutation response deltas at DEBUG b18bc40dd4 docs(ais): A12 design brief - attachment reconciliation, and the real cause of step 10 6d5475de4d docs(ais): A13 - step 10 closed; A-Q17 answered; T-2 deployed 7e023e862f docs(ais): fill the A13 ledger commit hash 15bb1c5af2 docs(ais): fill the A14 ledger commit hash 2697af65b0 docs(ais): step 7 is not viewer-reachable; Robust deploy verified live 60776649d4 docs(ais): fill the A15 ledger commit hash 1ea8efa063 docs(ssb): S3 recorded — Q-14 closed, Q-16 ruled, Q-6 now measurable 1a7dce26ca test(ais): reproduce A16 — UpdateItem answers 200 and drops asset_id 159fd41d39 fix(ais): UpdateItem persists the asset, the permissions and the version cb27bc93ee docs(ais): A18 ledger row, spec §1d-ter, checklist step 16 66fc104811 docs(ais): record the A18 deploy of both roots 0b8abad335 docs(ais): repair a stray newline in the A18 deploy row's backup path 6b2a33b067 docs(ais): do not call the attribution correction a "P-1 session" f9dee4592b feat(ais/ssb): editing a worn wearable now rebakes that channel 05c4e0847e docs(ssb): S9 ledger row, the trigger table, checklist step 17 77050c5372 docs(ssb): S10 Part 5 — trigger table, Q-11 live results, step 18 508ebc1b34 fix(ais): a refused wearable save is a 403, and library textures are legal bb4bcd03dc feat(config): S12 — one switch for the grid, and say which config decided 373bdcbe1c test(ais): adversarial cross-user cases for the real backend f8263e22b8 fix(ais): owner-scope InventoryServiceBackend (AIS-SEC-1) 0cbd371567 docs(ais): record AIS-SEC-1 16f75c18e0 docs(ais): step 19 verified bd5cfe05d0 test(ais): malformed and invalid SLAM bodies must not write 97c4ff0800 test(ais): mutation harness sends non-map bodies instead of nulling them 6c37b5e9d5 fix(ais): fail closed on malformed bodies, strict SLAM validation (AIS-SEC-2) 9ea6cdb201 docs(ais): record AIS-SEC-2 d0dd427513 docs(ais): step 20 verified 3057327361 fix(inventory): serialize CreateUserInventory per principal, warn on root duplicates (AIS-COF-1) 5f3bf17741 docs(ais): record AIS-COF-1 d5ba0f340e docs(ais): step 21 verified ffdd386f3f test(ais): concurrent mutations on one folder must serialize 24fedc52b9 fix(ais): per-(agent,folder) mutation lock for slam/purge/create (AIS-SEC-3) 3e1da288af docs(ais): record AIS-SEC-3 5ff178748a docs(ais): step 22 verified 57f5e0d5af test(ais): CreateInventory must report partial creates on failure 0386575b65 fix(ais): report partially created objects when CreateInventory fails (AIS-SEC-4) 73c8933253 docs(ais): record AIS-SEC-4 7a895014f0 docs(ais): record AIS-AUDIT-1 (malformed-LLSD blind spot, tree-wide) c7c2b5d848 docs(env): record ENV-1 863af777ab test(ais): error hygiene and cyclic folder traversal d567a724e0 fix(ais): log exceptions server-side, guard traversal with a visited set (AIS-SEC-5) 9402cf6550 docs(ais): record AIS-SEC-5 --- Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md | 164 +++ .../ais-v3/A12-ATTACHMENT-RECONCILIATION.md | 248 ++++ Docs/feature/ais-v3/A13-STEP10-CLOSED.md | 126 ++ Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md | 531 ++++++++ Docs/feature/ais-v3/A5-RUN-2026-09-04.md | 155 +++ Docs/feature/ais-v3/A6-LIVE-FAILURE.md | 117 ++ Docs/feature/ais-v3/A7-DUPLICATE-COF.md | 159 +++ .../ais-v3/A9-SUITCASE-NOT-DUPLICATE.md | 108 ++ Docs/feature/ais-v3/AIS-V3-SPEC.md | 541 ++++++++ Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md | 272 ++++ Source/OpenSim.Capabilities/Caps.cs | 8 +- Source/OpenSim.Capabilities/CapsHandlers.cs | 10 +- .../AIS/AISv3Module.cs | 579 ++++++++ .../AIS/AisCopy.cs | 111 ++ .../AIS/AisEnvelope.cs | 163 +++ .../AIS/AisFolderLocks.cs | 96 ++ .../AIS/AisHandler.cs | 1200 +++++++++++++++++ .../AIS/AisInventory.cs | 199 +++ .../AIS/AisMutation.cs | 270 ++++ .../AIS/AisPurge.cs | 109 ++ .../AIS/AisRouter.cs | 193 +++ .../AIS/AisSlam.cs | 184 +++ .../AIS/AisWornAssets.cs | 53 + .../AIS/IAisInventoryBackend.cs | 134 ++ .../PluginRegistration.cs | 1 + .../AgentAssetsTransactions.cs | 5 +- .../AssetTransactionModule.cs | 5 +- .../AssetTransaction/AssetXferUploader.cs | 53 +- .../Library/LocalInventoryService.cs | 1 + .../Inventory/HGInventoryBroker.cs | 17 + .../LocalInventoryServiceConnector.cs | 5 + .../RemoteXInventoryServiceConnector.cs | 10 + .../Interfaces/IAgentAssetTransactions.cs | 12 +- .../Inventory/XInventoryInConnector.cs | 10 +- .../Controllers/WeatherForecastController.cs | 32 - .../OpenSim.Services.AISv3.csproj | 13 - .../OpenSim.Services.AISv3.http | 6 - Source/OpenSim.Services.AISv3/Program.cs | 25 - .../OpenSim.Services.AISv3/WeatherForecast.cs | 12 - .../appsettings.Development.json | 8 - .../OpenSim.Services.AISv3/appsettings.json | 9 - .../Inventory/XInventoryServicesConnector.cs | 26 +- .../HGInventoryService.cs | 6 + .../HGSuitcaseInventoryService.cs | 6 + .../IInventoryService.cs | 14 + .../XInventoryService.cs | 203 ++- .../AIS/AisCapRegistrationTests.cs | 178 +++ .../AIS/AisConcurrencyHttpTests.cs | 413 ++++++ .../AIS/AisCreatePartialHttpTests.cs | 274 ++++ .../AIS/AisCrossUserHttpTests.cs | 384 ++++++ .../AIS/AisDuplicateSystemFolderTests.cs | 179 +++ .../AIS/AisErrorHygieneTraversalTests.cs | 290 ++++ .../AIS/AisFetchRoutesHttpTests.cs | 474 +++++++ .../AIS/AisHandlerHttpTests.cs | 280 ++++ .../AIS/AisInventoryTests.cs | 173 +++ .../AIS/AisMutationHttpTests.cs | 685 ++++++++++ .../AIS/AisPurgeCopyHttpTests.cs | 327 +++++ .../AIS/AisResponseLoggingTests.cs | 223 +++ .../AIS/AisRouterTests.cs | 179 +++ .../AIS/AisSlamCreateHttpTests.cs | 699 ++++++++++ .../AIS/CapturedLog.cs | 94 ++ .../AIS/FakeAisBackend.cs | 351 +++++ .../AIS/Fixtures/category-fetch.xml | 85 ++ .../AIS/Fixtures/cof-links.xml | 49 + .../AIS/Fixtures/error.xml | 11 + .../AIS/Fixtures/item-fetch.xml | 33 + .../AIS/Fixtures/link-fetch.xml | 34 + .../AIS/Fixtures/mutation-envelope.xml | 24 + .../AIS/PrincipalIgnoringInventoryService.cs | 185 +++ .../AIS/WornWearableRebakeTests.cs | 199 +++ ...on.ClientStack.LindenCaps.AIS.Tests.csproj | 34 + .../Asset/LibraryTextureInWearableTests.cs | 86 ++ .../Mock/TestOSHttpResponse.cs | 9 +- 73 files changed, 11999 insertions(+), 162 deletions(-) create mode 100644 Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md create mode 100644 Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md create mode 100644 Docs/feature/ais-v3/A13-STEP10-CLOSED.md create mode 100644 Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md create mode 100644 Docs/feature/ais-v3/A5-RUN-2026-09-04.md create mode 100644 Docs/feature/ais-v3/A6-LIVE-FAILURE.md create mode 100644 Docs/feature/ais-v3/A7-DUPLICATE-COF.md create mode 100644 Docs/feature/ais-v3/A9-SUITCASE-NOT-DUPLICATE.md create mode 100644 Docs/feature/ais-v3/AIS-V3-SPEC.md create mode 100644 Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AISv3Module.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisCopy.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisEnvelope.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisFolderLocks.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisHandler.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisInventory.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisMutation.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisPurge.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisRouter.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisSlam.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisWornAssets.cs create mode 100644 Source/OpenSim.Region.ClientStack.LindenCaps/AIS/IAisInventoryBackend.cs delete mode 100644 Source/OpenSim.Services.AISv3/Controllers/WeatherForecastController.cs delete mode 100644 Source/OpenSim.Services.AISv3/OpenSim.Services.AISv3.csproj delete mode 100644 Source/OpenSim.Services.AISv3/OpenSim.Services.AISv3.http delete mode 100644 Source/OpenSim.Services.AISv3/Program.cs delete mode 100644 Source/OpenSim.Services.AISv3/WeatherForecast.cs delete mode 100644 Source/OpenSim.Services.AISv3/appsettings.Development.json delete mode 100644 Source/OpenSim.Services.AISv3/appsettings.json create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCapRegistrationTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisConcurrencyHttpTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCreatePartialHttpTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCrossUserHttpTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisDuplicateSystemFolderTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisErrorHygieneTraversalTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisFetchRoutesHttpTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisHandlerHttpTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisInventoryTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisMutationHttpTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisPurgeCopyHttpTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisResponseLoggingTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisRouterTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisSlamCreateHttpTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/CapturedLog.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/FakeAisBackend.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/category-fetch.xml create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/cof-links.xml create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/error.xml create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/item-fetch.xml create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/link-fetch.xml create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/mutation-envelope.xml create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/PrincipalIgnoringInventoryService.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/WornWearableRebakeTests.cs create mode 100644 Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests.csproj create mode 100644 Tests/OpenSim.Region.CoreModules.Tests/Asset/LibraryTextureInWearableTests.cs diff --git a/Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md b/Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md new file mode 100644 index 00000000000..952c3600ece --- /dev/null +++ b/Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md @@ -0,0 +1,164 @@ +# A10 — re-diagnosing step 10, after A9 removed the original explanation + + +> **ANSWERED 2026-09-04 by A12/A13. Neither hypothesis in §5 was right.** +> +> - **H1 (a response delta the viewer rejected) is dead.** A11's response logging showed the deltas were correct +> and complete: `_removed_items` named the link and `_updated_category_versions` named the COF with its new +> version. Nothing was rejected because nothing was wrong. +> - **H2 (the appearance record never updated) was wrong as framed.** The record was not simply never written — +> the wearables were correct, 7 entries and no dress. H2 assumed a *missing server behaviour*; the truth is a +> *lost write*. +> +> **The cause:** the detach happened and the **deferred appearance save was dropped** because the agent left +> before the five-second timer fired — `AvatarFactoryModule.SaveAppearance`, `sp == null -> continue`. It predates +> AIS, hits the legacy path equally, and is symmetric: wear loses the same way. Diagnosed in +> `A12-ATTACHMENT-RECONCILIATION.md`, fixed in `dc4e417bb3`, closed in `A13-STEP10-CLOSED.md`. +> +> **What this document got right and should be kept for:** take-off is `DELETE /item` and not a slam; COF +> resolution was never wrong; and the passing re-run it examined was not a clean repetition. Those three findings +> stand. Its §5 hypotheses do not. + + +**Date:** 2026-09-04. **Region:** Ebony. **Avatar:** Truly Bazar +(`a7d2ff2e-dc32-44d8-aa61-3d22070a4964`). **Sources:** `OpenSim.Server.RegionServer20260904.log` and the live +database, read-only. + +**Conclusion up front: no single cause is established, and the passing re-run cannot be attributed to the A7 +fix. This session stops without a fix, as the brief requires.** What it does establish is that three of the four +lines of inquiry were aimed at the wrong operation, and it rules several things out with evidence. + +> **Timestamps.** Log times are local; `FROM_UNIXTIME` in the database is UTC, five hours ahead — the dump that +> finished at local 13:33 is stamped `18:33:26`. Both are quoted below in their own clock and labelled. + +--- + +## 1. The operation under test is not a slam + +**Take-off is `DELETE /item/{linkid}` → `RemoveItem`. It is not `SlamFolder`.** + +Every `SlamFolder` in the log is immediately preceded by `GET /category/5d7b7115-…/links`, and +`5d7b7115-edcb-4638-b5f9-196a1dd7aed3` is the asset id of the `AT_LINK_FOLDER` link named **"Truly Base"** — the +saved outfit folder. Fetching an outfit's links and then slamming COF is **wear-outfit** (step 9): + +| time (local) | request | +|---|---| +| 12:33:28,670 | `GET /category/5d7b7115-…/links` → FetchCategoryLinks | +| 12:33:28,720 | `PUT /category/71c3c184-…/links` → **SlamFolder** | +| 12:36:38,536 | `GET /category/5d7b7115-…/links` | +| 12:36:38,588 | `PUT /category/71c3c184-…/links` → **SlamFolder** | +| 12:37:33,205 | `GET /category/5d7b7115-…/links` | +| 12:37:33,239 | `PUT /category/71c3c184-…/links` → **SlamFolder** | + +The take-offs are the `RemoveItem` calls, each followed by a fetch of the garment's own item — and the garment is +the same one every time, `21ae19b0-75a8-41ba-b6d9-1f0472e39437`: + +| time (local) | request | +|---|---| +| 12:36:18,320 | `DELETE /item/dd6ac393-…` → **RemoveItem** | +| 12:37:26,116 | `DELETE /item/92873235-…` → **RemoveItem**, then `GET /item/21ae19b0-…` | +| 12:37:52,105 | `DELETE /item/f1137049-…` → **RemoveItem**, then `GET /item/21ae19b0-…` | + +**Consequence for the brief:** lines of inquiry (a), (b) and (c) all concern the slam's create-then-remove +ordering and its response deltas. That machinery was not on the failing path. It was examined anyway (§3) and is +sound. + +## 2. COF resolution was never wrong — confirmed on both sides of the fix + +Every mutation in the failing window addressed `71c3c184-…`, the correct root COF, never `52c327c4-…`. After the +fix the new WARN prints the resolution explicitly: + +``` +13:11:38,047 WARN [AIS]: agent "a7d2ff2e-…" has 2 folders of type CurrentOutfit + ("52c327c4-cb7d-4365-a7f0-62a6f7545265 v1, 71c3c184-410b-4dae-b20a-855741cf1faf v466"); + using "71c3c184-410b-4dae-b20a-855741cf1faf" version 466 +``` + +Same folder before and after. **The A7 fix changed which folder was chosen in exactly zero cases**, because the +suitcase COF was never a candidate the old code could return (A9). Whatever made step 10 pass on re-run, it was +not this. + +## 3. What was examined and cleared + +| Checked | Finding | +|---|---| +| Slam ordering (`AisSlam.Run :95-147`) | Correct. Creates all, then deletes the prior links by id; on a creation failure it rolls back and reports; the compensating `Rollback :150-156` only ever deletes ids it created in this call, so it **cannot** fire spuriously against a successful removal. | +| `AisEnvelope.IsLink :46-47` | Handles **both** `AT_LINK` (24) and `AT_LINK_FOLDER` (25), so the outfit-folder link is slammed like any other. An earlier suspicion that type 25 was being skipped and accumulating is wrong. | +| `RemoveItem` (`AisHandler.cs:366-382`) | Correct by inspection. Captures `item.Folder` **before** the delete, reports `_removed_items` with the item id and `_updated_category_versions` with the parent's freshly read version — which is exactly what spec §1d-bis requires for `DELETE /item/{id}`. | +| Does an item delete bump the folder version? | **Yes.** `MySqlItemHandler.Delete(string[], string[])` collects the parents and calls `IncrementFolderVersion` for each. (The single-field overload deliberately does not, because it delegates.) So the version the response reports really has moved. | +| `FetchCOF` shape (`FetchLinks :251-271`) | Conformant. Emits `_embedded.links` plus the resolved targets in `_embedded.items`; §1c says the viewer takes a COF's descendent count from `links` alone. | +| Current COF contents | 14 links, **no duplicates**, no surviving skirt link. Nothing was left behind by any removal. | + +Line of inquiry (a) asked for surviving links that should have been removed. **There are none** — but that is +weak evidence, because the current state postdates the re-run and a full slam has rewritten the folder since. + +## 4. The failure signature, and why the re-run does not clear it + +The failure is visible in the log as **`FetchCOF` immediately followed by `CreateInventory` into COF** — the +viewer reading the folder and instantly putting something back: + +| login | FetchCOF | next CreateInventory into COF | gap | +|---|---|---|---| +| **12:38:07 (failing)** | 12:38:10,905 | **12:38:10,944** | **39 ms — automatic repair** | +| 13:11:34 (post-deploy) | 13:11:38,002 | none before the next user action | — | +| 13:14:41 (the "pass") | 13:14:44,639 | 13:15:19,543 | 35 s — a deliberate wear | +| 13:28:13 | 13:28:16,911 | none | — | + +39 ms is not a user. The viewer read COF and repaired it against its own idea of what should be there. + +**And the re-run was not a repetition of the failing sequence.** Between the take-off and the relog that "passed", +a full wear-outfit slam intervened: + +| time (local) | event | +|---|---| +| 13:12:11,289 | `DELETE /item/7235d8bd-…` → RemoveItem (the take-off) | +| **13:13:53,919** | **`PUT /category/71c3c184-…/links` → SlamFolder** — rewrites every link in COF | +| 13:14:23,031 | `DELETE /item/18c41e76-…` → RemoveItem | +| 13:14:41,487 | relog | + +The database confirms the slam rewrote the folder wholesale: thirteen of the fourteen links carry creation +timestamps of `18:13:53`–`18:13:54` UTC (local 13:13:53–54), and only the dress added later differs at `18:15:19`. + +So any stale state left by the 13:12:11 take-off was **erased by a full slam** before the relog that was recorded +as a pass. The pass is therefore consistent with the fix working, with the slam having papered over the problem, +and with the problem being intermittent. **It does not discriminate between them, and an unexplained pass is not +a pass.** + +## 5. What remains, and what would settle it + +Two hypotheses survive. Neither can be confirmed from what exists. + +**H1 — a mutation response delta the viewer rejected.** If the `RemoveItem` response did not take effect in the +viewer's model, the viewer kept the link and restored it at the next login. Against it: the code is correct by +inspection, and checklist step 6 (delete an item, verified with *no duplicate left in the source folder*) exercises +the same route and passed. **Response bodies are not logged**, so the actual delta sent at 12:37:52 cannot be +recovered. + +**H2 — the avatar appearance record was never updated.** OpenSim keeps worn wearables in the avatar appearance +record, separate from the COF links. Under the legacy path a take-off travels as `AgentIsNowWearing` over UDP and +`AvatarFactoryModule` updates that record. Under AIS the viewer removed the COF link over HTTP, and **nothing in +the AIS handler touches appearance** — Ledger P-2 forbids it from taking a `Scene` or `ScenePresence` at all. If +the record still listed the garment, then at the next login the viewer would find itself wearing something with no +COF link and create one, which is exactly the 39 ms repair. This also fits the checklist's own note that Firestorm +has its own outfit machinery and may or may not send `AvatarNowWearing` alongside (SSB ledger A-Q6). Against it: +the region log contains **zero** `AVATAR FACTORY` lines for the whole day, so the module's activity is invisible +here and the hypothesis cannot be tested from this log either. + +**What would settle it, in order of cost:** + +1. **Log AIS response bodies at DEBUG for mutations** (or at least the delta keys and the reported version). The + single missing piece in both hypotheses is what we actually sent back. This is a small change and it is what + A6 taught: a request that arrives and misbehaves must not look the same as one that never arrived. +2. **Reproduce cleanly**: take off one garment, then relog **without** any intervening wear or slam, and watch for + `FetchCOF` → `CreateInventory` within a few tens of milliseconds. That is a definitive, one-minute test, and it + is the run the checklist should have recorded. +3. **Capture the appearance record** (the avatar's serialised wearables) immediately before and after a take-off. + If the garment is still listed after, H2 is confirmed and H1 is dead. + +## 6. Status + +- Step 10 is **not** diagnosed. The A7 explanation is withdrawn (A9) and nothing has replaced it. +- The A7 resolution fix (`6cd13a3645`) stays, on its own merits, but **must not be described as fixing step 10**. +- The A5 run record's step 10 row — "FAILED, then passed after the A7 fix" — is **misleading as to cause** and is + corrected by this document. +- No code was changed in this session. diff --git a/Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md b/Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md new file mode 100644 index 00000000000..e24900cf269 --- /dev/null +++ b/Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md @@ -0,0 +1,248 @@ +# A12 — reconciling COF changes with attachment state: design brief + + +> **CONFIRMED and ACTED ON, 2026-09-04 (A13).** §2's finding — the detach happened and the deferred appearance +> save was lost — is the cause, and it is fixed: `dc4e417bb3` flushes a pending save on `OnRemovePresence`, +> deployed in merge `bfb50070d8`. Checklist step 10 then passed on a clean run and is closed +> (`A13-STEP10-CLOSED.md`). +> +> **Still outstanding from §4 and §5**, unaffected by that fix: the viewer skips its removal arm entirely when +> `isFullyLoaded()` is false and never retries, and an offline agent has no viewer to reconcile at all — the case +> Phase 2's Robust hosting makes normal. Option **B3, login-time reconciliation**, remains the standing +> recommendation. + + +**Design only. No behaviour changed this session.** + +> **The brief's premise does not survive the evidence, and the correction matters more than the design.** +> +> The brief states: *"AIS removed the COF link; nothing detached the object."* **The object was detached.** What +> was lost is the *appearance record write* that should have recorded the detach — dropped by a five-second +> deferred save that raced the logout. See §2. +> +> That failure has **nothing to do with AIS**. It reproduces on the legacy path, it reproduces on wear as well as +> take-off, and it has been in the tree since long before this branch. AIS made it visible because AIS made +> take-off fast and quiet. +> +> There is still a genuine AIS-shaped gap (§3) — it is just not what broke step 10. + +--- + +## 1. Part 1(a) — how a detach normally happens, and every store it writes + +The viewer sends UDP, and the region does the rest: + +| # | Step | Where | +|---|---|---| +| 1 | `DetachAttachmentIntoInv` / `ObjectDetach` packet arrives | `LLClientView.cs:8434`, `:8436` → `HandleDetachAttachmentIntoInv :9267`, `HandleObjectDetach :9289` | +| 2 | Raised as `OnDetachAttachmentIntoInv` / `OnObjectDetach` | `LLClientView.cs:89`, `:91` | +| 3 | Subscribed by the attachments module | `AttachmentsModule.cs:975-976` | +| 4 | Resolved to a `SceneObjectGroup` and dispatched | `Client_OnObjectDetach :1480-1491`, `Client_OnDetachAttachmentIntoInv :1493-1510` (matches on `group.FromItemID`) | +| 5 | The detach proper | `DetachSingleAttachmentToInv :892-952` | + +**The four stores a detach writes**, all inside `DetachSingleAttachmentToInv`: + +| Store | Call | Persisted by | +|---|---|---| +| **A. In-memory appearance** | `sp.Appearance.DetachAttachment(so.FromItemID)` (`:945`) | nothing on its own — memory only | +| **B. The `Avatars` table (`_ap_*` rows)** | `m_scene.AvatarFactory.QueueAppearanceSave(sp.UUID)` (`:947`) | **deferred**; see §2 | +| **C. The ScenePresence's attachment list** | `sp.RemoveAttachment(so)` (`:949`) | memory only | +| **D. The scene object + its inventory asset** | `UpdateDetachedObject(sp, so, scriptedState)` (`:950` → `:1217-1247`) | `m_scene.DeleteSceneObject` (`:1232`) then `UpdateKnownItem` (`:1246`) | + +Store **B** is the one that matters here, and it is the only one that is not written synchronously. + +## 2. Part 1(b) — what the viewer expects, and what actually failed + +### The viewer reconciles attachments itself. It is not asking the server to. + +Taking off an attachment does **not** send a detach directly: + +```cpp +// LLAppearanceMgr::removeItemsFromAvatar — llappearancemgr.cpp:4204-4232 +LLPointer cb = new LLUpdateAppearanceOnDestroy(true, true, post_update_func); // :4214 +... +if (item && item->getType() == LLAssetType::AT_OBJECT) + LL_DEBUGS("Avatar") << "ATT removing attachment " ... ; // :4220-4223 — logs only +... +removeCOFItemLinks(linked_item_id, cb); // :4228 +``` + +`removeCOFItemLinks` (`:3239-3266`) deletes the COF link — `remove_inventory_item(..., true)` for `AT_OBJECT` +(`:3253`), the immediate variant. **That is the `DELETE /item` we saw.** No detach message is sent here. + +The detach comes from the **callback**. When the link deletes complete, `LLUpdateAppearanceOnDestroy` runs +`updateAppearanceFromCOF`, which diffs the COF against what is actually worn and acts: + +```cpp +// llappearancemgr.cpp:2631-2673 +LLAgentWearables::findAttachmentsAddRemoveInfo(obj_items, objects_to_remove, objects_to_retain, items_to_add); +... +// (don't remove attachments until avatar is fully loaded - reduces random attaching/detaching/reattaching at log-on) +if (gAgentAvatarp->isFullyLoaded()) // :2654 +{ + LLAgentWearables::userRemoveMultipleAttachments(objects_to_remove); // :2656 <- the detach +} +... +LLAgentWearables::userAttachMultipleAttachments(items_to_add); // :2673 <- the attach +``` + +So the contract is: **COF is the source of truth, and the viewer — not the server — reconciles the objects to +it.** The server's job is only to accept the link change and then to handle the ordinary detach/attach messages +the viewer sends afterwards. + +The opposite direction confirms the same model. When the *server* detaches something, the viewer tidies COF +itself — `unregisterAttachment` (`:4459-4479`) calls `onDetachCompleted` and then `removeCOFItemLinks(item_id)` +(`:4471`). And `getIsProtectedCOFItem` (`:4502-4531`) refuses raw deletion of a COF link — *"force users to +choose 'Detach' or 'Take Off'"* — precisely so the removal goes through `removeItemFromAvatar`, which is the +function above. + +**Answer to the question the brief poses:** it is **neither** a missing server behaviour nor a viewer message we +failed to handle. The viewer sent the detach, we handled it, and we then lost the write. + +### What actually failed, from the log + +| time | event | source | +|---|---|---| +| 14:09:35,236 | `DELETE /item/dfcc1be2-…` → RemoveItem | AIS | +| 14:09:35,267 | `RemoveItem -> 200 _removed_items=[dfcc1be2-…] _updated_category_versions={71c3c184-…:501}` | AIS — **correct** | +| ~14:09:35,3xx | viewer's `updateAppearanceFromCOF` → `userRemoveMultipleAttachments` → UDP detach | viewer | +| 14:09:35,408 | `[ATTACHMENTS MODULE]: Updating asset for attachment c91d9878-…, attachpoint 18` | `UpdateKnownItem` (`AttachmentsModule.cs:1031`) — **the detach's own object save** | +| ~14:09:40 | **logout** (`[JANUS PLUGIN] Detach. Detached` 14:09:40,012) | region | +| 14:09:40,0–,2 | `Updating asset for attachment` for attachpoints **11, 40, 7, 8** | `DeRezAttachments :533-561` | + +**Attachpoint 18 is absent from the logout batch.** The dress was no longer attached in the scene — because it +had already been detached five seconds earlier. Yet the database still holds `_ap_18 = 9bec8993-…`. + +The reason: + +```csharp +public void QueueAppearanceSave(UUID agentid) // AvatarFactoryModule.cs:334-342 +{ + long timestamp = DateTime.Now.Ticks + Convert.ToInt64(m_savetime * 1000 * 10000); // m_savetime = 5 (:51) + m_savequeue[agentid] = timestamp; + m_updateTimer.Start(); +} + +private void SaveAppearance(List ids) // :811-830 +{ + foreach (UUID id in ids) + { + ScenePresence sp = m_scene.GetScenePresence(id); + if (sp == null) + continue; // :818-819 <- the write is DROPPED, silently + SetAppearanceAssets(id, sp.Appearance); + m_scene.AvatarService.SetAppearance(id, sp.Appearance); // :828 <- the ONLY persist for a live avatar + } +} +``` + +The detach queued a save for ~14:09:40.4. The avatar logged out at ~14:09:40.0. When the timer fired, +`GetScenePresence` returned null, the loop `continue`d, and **the only write that would have cleared `_ap_18` +never happened**. + +**Nothing flushes it at logout.** `DeRezAttachments` (`:533-561`) saves each attachment *object* through +`UpdateDetachedObject`, and never touches `sp.Appearance` or `AvatarService`. `AvatarFactoryModule.cs:828` is the +only appearance persist for a live avatar in the whole tree — the other `SetAppearance` callers are +`RemoteAdminPlugin` and account creation. + +**So: a five-second deferred write, dropped without a log line, with no flush on the path that ends the session.** + +### Why it looked like an AIS problem + +Under AIS, take-off is one small HTTP call and the user is free to log out immediately. The legacy path had the +same race, but a UDP take-off is usually followed by more UDP traffic and a slower user. It is a pre-existing +defect that AIS exposed, in the same way A7's `folders[0]` was a pre-existing fragility. + +## 3. Part 1(c) — SL parity + +SL has no separate attachment store to fall out of step: the COF *is* the record, and the simulator's attachment +state is derived from it. That is why the viewer is written to reconcile against COF and why LL never needed a +"COF changed → detach" server behaviour. + +**Parity therefore does not require us to reconcile at mutation time.** It requires that our derived stores — +`Avatars._ap_*`, `ScenePresence`, the scene object — never disagree with COF once the dust settles. Today they +can, in two ways: + +1. the write that records agreement is lost (§2); and +2. nothing reconciles when no viewer is there to do it (§4). + +## 4. Part 1(d) — the same gap on wear, on slam, and when nobody is reconciling + +**Wear is symmetric.** `AttachmentsModule.cs:1389` sets `sp.Appearance.SetAttachment(...)` and `:1396` queues the +same deferred save. Attach something and log out within five seconds and the record is lost the same way — the +avatar comes back *without* the attachment, the mirror of this bug. + +**Slam is the same code path, multiplied.** `PUT /category/current/links` replaces every link, so +`updateAppearanceFromCOF` computes a large `objects_to_remove` / `items_to_add` and issues many detaches and +attaches, each queueing a save that collapses to one timestamp. One logout inside the window loses the lot. + +**Two holes the viewer cannot cover at all:** + +- **`isFullyLoaded()` is false** (`llappearancemgr.cpp:2654`). The removal arm is skipped entirely and **never + retried** — the COF link is gone, the object stays attached, and no message is ever sent. This is a real + second failure mode with the same visible symptom, and no amount of fixing the save will address it. +- **The agent is not logged in.** There is no viewer to reconcile. Today AIS only serves a region-hosted, + logged-in agent, so this is latent — but **Phase 2 hosts the handler on Robust, where an offline agent is the + normal case, not the exception**. A COF mutation arriving for an offline agent must still leave the stored + appearance consistent, or the avatar rezzes wrong on next login and "repairs" itself back to the old outfit. + +--- + +## 5. Part 2 — options + +**These are two problems and they want different answers. Do not let the second pay for the first.** + +### Problem A — the lost appearance write (the actual cause of step 10) + +| Option | Cost | Forecloses | +|---|---|---| +| **A1. Flush the save queue when the presence closes.** Drain `m_savequeue` for that agent on `OnRemovePresence` / client logout, before the ScenePresence is torn down. | Small and local to `AvatarFactoryModule`. One event subscription plus a synchronous save. | Nothing. | +| **A2. Persist synchronously on detach/attach** instead of queueing. | A DB write per attachment operation; a slam becomes N writes. Was presumably why the queue exists. | The batching the queue buys. | +| **A3. Make `SaveAppearance` not need the presence** — capture the `AvatarAppearance` at queue time rather than dereferencing `sp` at fire time. | Small, but changes save semantics: it would persist a snapshot rather than the latest state. | Coalescing later changes into one write. | +| **A4. Log the drop and do nothing else.** | Trivial. | Nothing — but it fixes nothing either. | + +**Recommendation: A1, plus the WARN from A4.** It closes the race at the exact point the race exists, keeps the +batching, and needs no new architecture. A3 is a reasonable belt-and-braces addition later. + +**What breaks if A1 is wrong:** a flush on logout writes appearance one extra time per session. If the in-memory +appearance were somehow *worse* than the stored one, we would persist the worse one — so the flush must write +only when the queue actually holds an entry for that agent, i.e. only when something really did change. + +### Problem B — reconciling COF when no viewer will + +This is the one P-2 constrains. **P-2 says the AIS handler may take only an agent id, an `IAisInventoryBackend` +and the request.** Every option below preserves that; they differ in where the knowledge lives. + +**First, what the tree already has: nothing.** There is no listener for COF changes anywhere. +`grep FolderType.CurrentOutfit` across `OpenSim.Region.CoreModules` returns two hits, both in +`AvatarFactoryModule` (`:1097`, `:1112`), and both are *writes* — the "Failed Wearable Replacement" path creating +a link. Nothing subscribes to, or notices, a COF change. There is no existing seam to reuse. + +| Option | Cost | Forecloses | +|---|---|---| +| **B1. Event/queue the region subscribes to.** The handler publishes "COF changed for agent X" to an abstraction it is given; the region module subscribes and reconciles against the live `ScenePresence`. | A new interface plus a region-side consumer. In-process it is an event; on Robust it needs a real transport. | Nothing structurally — this is the option that survives Phase 2 intact. Handler stays Scene-free. | +| **B2. Narrow seam: one capability, detach/attach by item id for a present agent.** | Smallest code. | **A lot.** It only works for a present agent, so Phase 2 gains nothing, and the moment the interface exists it will grow. It also re-introduces, in spirit, the coupling P-2 exists to prevent. | +| **B3. Reconcile at login.** On presence creation, diff stored appearance against COF and correct. | Cheap, entirely region-side, no handler change at all, and **it is the only option that fixes an offline mutation**. | Leaves a present avatar visibly wrong until relog. | +| **B4. Do nothing; rely on the viewer.** | Free. | Accepts both holes in §4 — the `isFullyLoaded` skip and the offline agent. | + +**Recommendation: B3 now, B1 when Phase 2 lands. Not B2.** + +- **B3 first** because it is the only option that covers the offline case, which is Phase 2's normal case, and + because it is a pure region-side addition with no AIS or P-2 impact. It also happens to be a safety net for + the `isFullyLoaded` skip and for anything else that leaves the stores disagreeing: at every login, COF wins. +- **B1 when the handler actually needs to reach a live region**, i.e. when Phase 2 makes "the region that has + this agent" a different process. Designing it before then risks building the wrong transport. +- **Not B2.** A capability that only works for a present agent buys nothing for the case that is about to become + normal, and it spends P-2 to get there. + +**What breaks if B3 is wrong:** login-time reconciliation makes COF authoritative over the appearance record. +If COF is ever *itself* wrong — a partial slam, a failed create — we would faithfully reproduce the wrong +outfit and, worse, overwrite a correct appearance record with it. Two guards: reconcile only when the two +disagree, and never strip on an empty or unreadable COF (the same "never trust an empty result" rule that S1e +applied to bake channels). An avatar that logs in with an unreadable COF must keep what it had. + +## 6. What this session did not do + +No behaviour changed, no test written, nothing deployed, the database untouched. `A-Q17` should be closed with +the §2 finding, and two new items opened — the lost appearance write (Problem A) and the reconciliation gap +(Problem B) — but the brief asked for a design and a stop, so those edits are left for the session that acts. diff --git a/Docs/feature/ais-v3/A13-STEP10-CLOSED.md b/Docs/feature/ais-v3/A13-STEP10-CLOSED.md new file mode 100644 index 00000000000..1ac930d71f8 --- /dev/null +++ b/Docs/feature/ais-v3/A13-STEP10-CLOSED.md @@ -0,0 +1,126 @@ +# A13 — step 10 closed, A-Q17 answered, T-2 deployed + +**Date:** 2026-09-04. Docs only. + +--- + +## 1. Step 10 passes, on a clean run, with the cause fixed + +**Build `1.1.202-alpha+bfb50070d8`** — the merged AIS + appearance-flush tree. No slam between the take-off and +the relog, and ~20 seconds in-world before logging out. + +| time | line | +|---|---| +| 17:15:18,402 | `DELETE /item/a4cb683b-…` → RemoveItem | +| 17:15:18,432 | `RemoveItem -> 200 _removed_items=[a4cb683b-…] _updated_category_versions={71c3c184-…:503}` | +| ~17:15:23 | the 5 s timer fires and writes — **inferred**: no drop WARN, and the change persisted | +| 17:15:38 | logout; attachpoints 40, 7, 8 saved; **no flush line** | +| relog | **the dress stayed off** | + +**The absent flush line is the cost guarantee, not a failure.** `FlushAppearanceSaveOnClose` writes only when the +queue actually holds an entry — *a close with nothing queued writes nothing*, which has a test of its own. The +timer had already drained the queue at ~17:15:23, fifteen seconds before logout, so there was nothing to flush. +The flush exists for the other case: a logout **inside** the five-second window. That is what happened at +14:09:40 on the failing run, and that is what lost the dress. + +`A5-RUN-2026-09-04.md` is updated: step 10 **pass**. *(Tally revised again by A15, once step 7 was found to be +unreachable through the viewer: **12 pass, 1 not reachable (7), 2 not run (14, 15)**.)* + +## 2. A-Q17 answered — and neither hypothesis was right + +A10 left two hypotheses. Both are now dead. + +| | Hypothesis | Verdict | +|---|---|---| +| **H1** | a mutation response delta the viewer rejected | **Dead.** A11's logging shows the deltas were correct and complete: `_removed_items` named the link, `_updated_category_versions` named the COF with its new version. Nothing was rejected because nothing was wrong. | +| **H2** | the avatar appearance record was never updated | **Wrong as framed.** The record was not simply never written — the wearables were correct (7 entries, no dress). H2 assumed a *missing server behaviour*; the truth is a *lost write*. | + +**The actual cause (A12):** the detach happened — the viewer reconciles attachments itself in +`updateAppearanceFromCOF` (`llappearancemgr.cpp:2656`) and sent it, the region handled it, and attachpoint 18 was +absent from the logout save batch. What was lost is the **deferred appearance save**: +`AvatarFactoryModule.QueueAppearanceSave` defers by `m_savetime` (5 s) and `SaveAppearance` then does +`sp == null -> continue`, dropping the write silently when the agent has already left. Nothing flushed it — +`DeRezAttachments` never touches appearance. + +It **predates AIS**, hits the **legacy path equally**, and is **symmetric**: wear queues the same deferred save +and loses it the same way, so an attachment worn just before logout comes back missing. + +### The working lesson + +**Three successive diagnoses were aimed at the wrong store, and each was corrected by one read-only query or one +log line.** + +| # | Diagnosis | Store blamed | What corrected it | Cost | +|---|---|---|---|---| +| 1 | A7 | COF resolution — the agent has two type-46 folders and we pick the wrong one | one `SELECT` showing the version-1 folder is parented to `My Suitcase`, so the query could never have returned it (A9) | a fix, a deploy, and a dedupe plan that would have deleted live suitcase skeletons | +| 2 | A10 | the response delta, then the appearance record | the A11 response logging, which showed the deltas were correct (A13) | a session | +| 3 | A12 | attachment reconciliation — "nothing detached the object" | the logout save batch, which listed attachpoints 11/40/7/8 but **not** 18, proving the object *had* been detached | caught in-session, before any code | + +Each wrong turn rested on **one unverified observation** carried in as fact: "AIS returned the version-1 folder", +"the response must have been wrong", "nothing detached the object". None was checked; each was cheap to check. + +**The rule worth keeping: verify the single observation the argument rests on, before building on it.** The tell +is an argument whose whole structure depends on one premise nobody has measured — especially when that premise +arrived in the framing of the problem rather than from the evidence. + +A related habit that paid for itself: when a diagnosis cannot be separated from its alternatives, **add the +missing evidence rather than another theory**. A11 was a session spent only on logging, and it settled a question +two prior sessions could not. + +## 3. T-2 — the fix and its deploy + +**`dc4e417bb3`** on `fix/appearance-save-flush`: + +- flushes a pending appearance save on `EventManager.OnRemovePresence`, raised at `Scene.cs:3866` while the + presence is still resolvable — it is not removed until `:3898` nor disposed until `:3905`; +- **skipped for child agents**, so a teleport cannot publish a stale outfit over a newer one the destination has + since saved; on a teleport the source's root is converted by `MakeChildAgent`, not `RemoveClient`, and the + appearance travels in the agent data; +- **WARN on both drop paths**, so this cannot fail silently again; +- **6 tests, 4 of which fail without it**; the other 2 assert the cost guarantee. + +It also added the `DisableTestParallelization` declaration `OpenSim.Region.CoreModules.Tests` was missing — the +same one, for the same reason, already in `OpenSim.Region.Framework.Tests`. That project was order-dependent and +flaky: a full run failed **9 tests, but not the same 9**, with failures appearing inside `SceneHelpers.SetupScene` +rather than in any assertion. It is now deterministic at **5 failed / 84 passed**. Those 5 are pre-existing and +are **T-3's job**. + +**Deployed** as merge **`bfb50070d8`** on `integration/ais-appearance` — merge-base `11a2456833`, clean, zero +conflicts, no file overlap between the branches. Backup at +`D:\legiongrid\_backup\regionserver-20260904-1703\`. Both features verified present in the **deployed** binaries: +`InventoryAPIv3` and the mutation-delta line in `LindenCaps.dll`, the flush and both WARN lines in +`CoreModules.dll`. + +## 4. Open items — enough context to pick up cold + +~~**1. The live region runs a commit reachable from only one branch.**~~ **DONE 2026-09-04 (A14).** +`fix/appearance-save-flush` merged into `feature/ais-v3`; the merged tree differs from the deployed +`bfb50070d8` only in `Docs/`, so the live binaries are reproducible from the branch. `integration/ais-appearance` +deleted and `D:\tranq-integration` removed. The deployed merge is not an ancestor of the new HEAD, so it is +tagged `deployed/region-2026-09-04` to keep the binaries' `+bfb50070d8` stamp resolvable. + +~~**2. Robust redeploy, still blocked.**~~ **DONE 2026-09-04 (R1).** Merged as `a2c8fb63f3` and deployed; +Robust runs one commit where it ran four, and restarted cleanly at 19:03:58 with trusted-hypergrid loaded. +`ONLYIFTRASH` and `EnsureSystemFolder` are live. **Checklist step 7 was never actually blocked by this** — it is +not reachable through any viewer (A15). + +**3. A12's remaining holes.** The viewer skips its removal arm entirely when `isFullyLoaded()` is false +(`llappearancemgr.cpp:2654`) and **never retries**; and an offline agent has no viewer to reconcile at all, which +Phase 2's Robust hosting makes the normal case. A12 option **B3 — login-time reconciliation** — is the standing +recommendation, with the guard that it must reconcile only on disagreement and **never strip on an empty or +unreadable COF**. + +**4. A-Q16 open.** For a local user, AIS's system-folder resolution scans every type-46 folder including the +suitcase's. Suitcase COFs sit at version 1 so the root always wins today, but nothing enforces it. + +**5. Checklist steps 14 and 15 unrun** — HG folder deletion, and folder thumbnail/favourite. Both are documented +limitations rather than suspected defects. + +**6. T-3: the 5 residual `CoreModules` failures**, now stable and attributable — 2 asset-store assertions in +`AvatarFactoryModuleTests`, 3 IAR loader tests. They were hidden behind the flakiness until T-2 made the project +deterministic. + +**7. Backup integrity, unaudited.** Any `.sql` in `D:\legiongrid\_backup\` written through a PowerShell text +pipeline is **corrupt and unrestorable** — `Set-Content` re-encodes the byte stream and replaces every byte that +is not valid text, which mangles binary column data. `legiongrid-predupe-20260904-1332.sql` was re-taken by shell +redirection and is good (2,685,971,589 bytes, no BOM). **An audit of the rest of that folder has not been run.** diff --git a/Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md b/Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md new file mode 100644 index 00000000000..1ea0765bf15 --- /dev/null +++ b/Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md @@ -0,0 +1,531 @@ +# A5 — the live checklist + +**What this is.** The ordered in-world run for the first region with `AIS_Enabled = true`. Every step names what +to do, what the viewer does when it works, and the symptom when it does not. **Nothing in the AIS implementation +has ever met a real viewer**; 114 unit and HTTP tests pass against fakes, which proves the shapes, not the system. + +**Why the order matters.** Risk A-R1: once `InventoryAPIv3` is in the seed cap, the LL viewer routes deletes, +purges, slams and creates through it with **no fallback** (spec §1g). A failure is not a degraded experience, it is +an operation that silently does not happen. So the read-only steps come first: if step 1 is wrong, stop and turn +the flag off before touching anything mutating. + +--- + +## Before you start + +| # | Do | Confirms | +|---|---|---| +| 0a | Set `AIS_Enabled = true` in the **region's own section** only — `[Ebony]`, not `[AIS]`. Restart the region. | The per-region flag, not a grid-wide flip | +| 0b | Console: `grep "\[AIS\]" OpenSim.Server.RegionServer.log` | One line per enabled region: `region advertises InventoryAPIv3, LibraryAPIv3`. **A second region in that output means the flag leaked — stop.** | +| 0c | On another region, confirm **no** `[AIS]` advertise line | The other regions are untouched | +| 0d | Take a full inventory backup for the test avatar (console `save iar` or a DB dump of `inventoryitems` / `inventoryfolders` for that PrincipalID) | Every mutating step below is destructive; this is the undo | + +**Test avatars:** Truly Bazar and Aleric Fenwood, as in the SSB work. Never Legion. + +--- + +## Phase 1 — read only (steps 1–3) + +Nothing here writes. If any step fails, turn the flag off; do not continue. + +### 1. Full inventory load after a cache clear + +**Do:** log out; delete the viewer's inventory cache (`//*.inv.llsd.gz`); log in to the test +region; open Inventory and let it settle. + +**Works:** the inventory tree fills in, folder by folder, and the item count stops growing. Every folder you open +already has its contents. + +**Fails:** folders stay empty or show "Fetching…" forever; the item count climbs and never settles; the same folder +is requested over and over. That last one is the specific signature of a folder we returned without all three +`_embedded` collections — the viewer never gets a descendent count, never accepts the version, and re-fetches +forever (risk A-R3). + +**Server side:** `grep -c "InventoryAPIv3" ` to see the cap being hit; the request path in the HTTP log shows +`/category//children?depth=…`. Expect `depth=50` for the recursive sweep and `depth=0` for single folders, and +nothing else — those are the only two the viewer sends (spec §1c-bis). + +### 2. Open a deep folder + +**Do:** open a folder at least three levels down that was **not** expanded during step 1. + +**Works:** contents appear immediately or after one brief fetch. + +**Fails:** it stays empty, or reopening it re-fetches every time. + +### 3. Current outfit reads back + +**Do:** open Appearance → Wearing. + +**Works:** every worn item is listed, with its real name rather than "(loading)". + +**Fails:** blanks or missing entries — the COF links resolved but their targets did not +(`GET /category/current/links` must carry the link *targets* in `_embedded.items`). + +--- + +## Phase 2 — single-object mutations (steps 4–8) + +### 4. Rename an item + +**Do:** rename any item. + +**Works:** the new name sticks, survives closing and reopening the folder, and survives a relog. + +**Fails:** the name reverts after a moment (the viewer applied it optimistically and our response did not confirm +it), or it reverts on relog (we did not write it). The likely cause of the first is a missing +`_updated_category_versions` entry for the parent — without it the viewer discards the update entirely +(spec §1d-bis). + +**Server side:** `grep "PATCH /item" `. + +### 5. Rename a folder + +**Do:** rename a folder you created yourself (not a system folder). + +**Works:** as above. + +**Fails:** as above. A category PATCH must list **both** the folder and its parent in +`_updated_category_versions`; only one is a bug. + +### 6. Delete an item + +**Do:** right-click an item → Delete. + +**Works:** it moves to Trash and stays there through a relog. + +**Fails:** it reappears after a moment or after relog. + +### 7. Delete a folder **outside** Trash — NOT REACHABLE THROUGH THE VIEWER + +> **Settled 2026-09-04: this step cannot be performed.** It is not unrun, and it is not blocked — **a resident +> has no way to ask for it.** + +**Do:** nothing. There is no gesture that produces it. + +Firestorm and the LL viewer offer exactly three folder-removal routes, and none of them is a delete of a folder +outside Trash: + +| Route | What it actually is | +|---|---| +| Delete / right-click → Delete | a **MOVE** to Trash — `PATCH /category` changing `parent_id`, not `DELETE /category` | +| Purge a single item in Trash | acts on an item, not a folder | +| Empty Trash | `DELETE /category/{trash}/children` — **step 8**, and it passed | + +**There is no shift-delete for folders.** The protected-folder rule reinforces this: the viewer routes folder +removal through the outfit/inventory machinery rather than raw deletion. + +**Verified in-world 2026-09-04.** Two folders were deleted, one nested and one at the inventory root. **Both +moved to Trash**, and both were still in Trash after a restart. The AIS log for the whole day shows +`CreateInventory` and `UpdateCategory` — the move — and **no `RemoveCategory` at any point**. + +**What this means for A2b.** The `ONLYIFTRASH` work is **still correct and still wanted**: the spec defines +`DELETE /category/{id}` (`llaisapi.h`), so the route must exist and must behave honestly when something calls it — +a script, a future viewer, another AIS client, or our own tooling. But **it was never gating a resident-visible +operation.** The folder removal residents actually perform is Empty Trash, which is step 8, and that has passed +since before the Robust redeploy. + +**Server side:** `grep "DELETE /category" ` — expect nothing from ordinary use. + +### 8. Empty Trash + +**Do:** right-click Trash → Empty Trash. Have at least one folder **and** one loose item in there first. + +**Works:** Trash empties completely, including the contents of the subfolder, and stays empty through a relog. + +**Fails:** Trash still shows its contents afterwards — the purge response must **enumerate** the direct children +(spec §1d-bis); unlike a folder delete, nothing on the viewer side sweeps them for us. Partially emptied means the +service refused part of it; the response names the survivors. + +--- + +## Phase 3 — outfits, the destructive ones (steps 9–10) + +**These are the steps that can strip an avatar.** Do them on Truly first, not on an avatar whose outfit matters. + +### 9. Wear an outfit (slam) + +**Do:** Appearance → Outfits → wear a saved outfit. + +**Works:** the avatar changes to that outfit; Wearing lists exactly the new items and none of the old. + +**Fails, in order of seriousness:** +- **The avatar ends up wearing nothing / partially dressed.** This is the failure the slam ordering is built to + make impossible (links are created before the old ones are removed), so if you see it, stop and report it — the + ordering is wrong, not just a transient. +- **Duplicated attachments or doubled clothing layers.** The new links were created and the old ones were not + removed. Recoverable by wearing the outfit again. This is the known window (Ledger A-Q10): there is no + transaction under a slam. +- The outfit does not change at all: the slam was refused; check the log. + +**Server side:** `grep "PUT /category" `. + +### 10. Take off a garment + +> **Wait about ten seconds in-world before relogging.** Otherwise this step races the appearance-save timer +> instead of testing AIS. +> +> Taking something off updates the avatar's appearance record through a **deferred** write: +> `AvatarFactoryModule.QueueAppearanceSave` schedules it `m_savetime` seconds out — five by default — and +> `SaveAppearance` reads the `ScenePresence` only when the timer fires. Log out inside that window and, before +> `dc4e417bb3`, the write was dropped silently and the garment came back on the next login. That is precisely +> what happened on 2026-09-04: the detach was recorded at 14:09:35,408, the save was due at ~14:09:40.4, and the +> avatar left at ~14:09:40.0. +> +> `dc4e417bb3` flushes the queue on close, so the fast path is now covered too — but a run that logs out +> immediately is still testing the flush rather than the take-off. **Give it ten seconds and the test means what +> it says.** Also note: take-off is `DELETE /item` (RemoveItem), **not** a slam; the viewer removes the COF link +> and reconciles the attachment itself. + +**Do:** right-click a worn garment → Take Off. Wait ~10 seconds. Then relog. + +**Works:** it comes off, the rest of the outfit is untouched, and it stays off through a relog. + +**Fails:** the garment comes back; or **other** garments come off with it — the slam replaced the folder's links +with the wrong set. + +--- + +## Phase 4 — create and copy (steps 11–12) + +### 11. Create a folder + +**Do:** Inventory → + → New Folder, then rename it. + +**Works:** the folder appears, keeps its name, and survives a relog. + +**Fails:** the folder does not appear at all, or appears and vanishes on relog. + +**Server side:** `grep "POST /category" `. + +### 12. Copy a library outfit + +**Do:** open Library → an outfit folder → right-click → Copy to Inventory (or drag it into your inventory). + +**Works:** the folder and its contents appear in your inventory, nested as they were in the library, and the items +are usable — wear one to confirm the permissions came across. + +**Fails:** nothing arrives; or the folder arrives empty; or the items arrive but cannot be worn (permissions were +degraded — the library copy must carry the source's own masks, not `NextPermissions`). + +**Server side:** `grep "COPY /category" `. The destination folder id travels in the `Destination` header. + +--- + +## Phase 5 — the things that are known not to work (steps 13–15) + +Confirm these behave as documented rather than in some worse way. + +### 13. Creating an inventory **item** — RESOLVED, it just works + +**Do:** Inventory → + → New Notecard (or New Script, New Clothing). + +**Expected:** the notecard **is** created, normally, and AIS is never involved. Settled in A11 from the source: +the AIS arm of `create_inventory_item` is inside `#ifdef USE_AIS_FOR_NC` +(`llviewerinventory.cpp:1120`-`:1166`), the macro is not defined, so control falls unconditionally to the legacy +`CreateInventoryItem` UDP send at `:1169`. Confirmed in the 2026-09-04 run: the item was created over UDP and the +only AIS request was a `FetchItem` syncing the result. + +**This step was once "the single biggest argument against flipping this flag more widely". It is not any more** — +the code is LL's, so stock viewers behave identically, and our 501 route is simply never reached for item +creation. Watch only that the item appears and survives a relog. + +### 14. Hypergrid folder deletion — expected refusal + +**Do:** only if this region serves hypergrid visitors. As an HG visitor, try to delete a folder. + +**Expected:** refused. `HGInventoryService` and `HGSuitcaseInventoryService` answer NOGO for folder deletion +whatever the flag says, so the verification step turns that into a 500. + +### 15. Folder thumbnail and favourite — silently dropped + +**Do:** set a folder thumbnail, or mark a folder as a favourite. + +**Expected:** the operation appears to succeed and the setting does not persist across a relog. This tree's +`InventoryFolderBase` has no column for either, so both are accepted and dropped. + +## Phase 6 — saving an asset (steps 16-17) + +Added in A18. **The checklist had no step that saved an asset**, which is why steps 1-15 all passed while +`PATCH /item` was silently discarding every asset id a wearable save sent. Renaming an item exercises the same +route and does persist, so nothing here caught it; the defect surfaced during Q-11 preparation instead. Any +future route that stores something needs a step that reads it back **after the viewer has been made to forget**, +not just after the operation. + +### 16. Edit a wearable, change a colour, save — the colour persists + +**Do:** Appearance → Edit an item you can modify (a skirt, a shirt). Change its colour. **Save**. Close the +Appearance floater, then re-open it and look at the item again. + +**Expected:** the colour you saved. Re-opening is the point: it makes the viewer re-read the item rather than +draw from its own cache, which is where the change lives until the server has actually stored it. + +**Also check, if you have the log and the database:** + +- the region log shows `ASSET XFER ... uploaded ` and then a `PATCH .../item/` answering 200; +- the item's `assetID` column equals **that** uploaded asset, not the one it had before; +- the `_updated_category_versions` in the PATCH response is **higher** than the folder's previous version. If the + same version comes back twice for two different saves, nothing was written — that is the A18 signature. + +### 17. Edit a WORN wearable, change a colour, save — the sim rebakes that channel + +**Do:** wear the item first. Appearance → Edit it → change its colour → **Save**. Stay in world and watch +yourself; do not relog. + +**Expected:** within about **7 s** the colour changes in-world, on you and to everyone else, with no relog. The +region log shows one `[SSB]` `reason=CofChanged` bake in which **that wearable's channel is `Baked` and the +others are `Reused`** — the whole point is that only what changed is recomputed. + +Added in S9. Step 16 checks the asset is *stored*; this checks the region *acts* on it. They are different +failures and step 16 passed while this one did not: on 2026-09-05 four edits stored correctly and produced no +bake at all, because an edit moves neither the worn set nor any signal the region was watching. + +**If nothing bakes,** the thing to check is whether the AIS `UpdateItem` for that item reported an asset change — +the region logs `item ... is worn by ... and its asset changed ...; queueing an appearance save` at DEBUG. No +such line means the item was not in the presence's wearables, which is a different bug from this one. + +### 18. Add a SECOND wearable of a type — the sim layers both, newer on top + +**Do:** wear a shirt. Then right-click a *different* shirt in inventory and choose **Add** (not Wear — Wear +replaces, Add layers). Stay in world. + +**Expected:** you are wearing both, with the added one **on top**. The region log shows **one** `[SSB]` +`reason=CofChanged` bake with `Upper=Baked` — not `Reused` — and the other channels reused. + +Added in S10. Before it, this failed silently and looked like a bake that had nothing to do: on 2026-09-06 at +10:09:52 both shirts were linked in the COF, the bake reported **`reused 6/6`**, and the `Avatars` record held +`Wearable 4:0` alone. The second shirt never reached the sim's wearables, because nothing on a bit-0 region +turned a COF link into one. + +**Also check, if you have the log and the database:** + +- the `Avatars` record for the agent now has **both** `Wearable 4:0` and `Wearable 4:1`; +- the two COF link items' `description` columns are `@400` and `@401` — that is the viewer's ordering + information (`"@" + type * 100 + index`), and it is what the sim layers by, higher index on top; +- the region log carries `[SSB]: 's worn set from the COF: … Shirt x2 …` at DEBUG. + +**If the bake reuses everything,** the derivation did not see the second link. Check that DEBUG line first: no +line at all means the COF read found nothing to change, and a warning about links *"this region cannot resolve +as a wearable"* means the link's target did not come back from inventory — a different fault, and the S8 rule +deliberately keeps the old wearables in that case rather than emptying the slot. + +Then swap which shirt is on top (take both off, add them in the other order) and confirm the bake follows. + +### 19. Fetch ANOTHER resident's folder through your own AIS cap — 404 + +**Do:** log in as **Truly Bazar**. Get **Aleric**'s Current Outfit folder UUID out of the database (or out of +Aleric's own region log line, `FetchCOF resolved "current" to `), then ask Truly's own `InventoryAPIv3` +cap for it directly — a `GET /category//children`. The cap URL is in Truly's seed +response; `curl` or the viewer's own debug console will do. + +**Expected: 404**, with an error body and **no** `_embedded`. Nothing of Aleric's may appear, and Truly's own +inventory must be unaffected. + +**Then the same UUID against the mutating routes**, one at a time, and each must also be refused with Aleric's +row unchanged afterwards: `PATCH /category/` with `{"name":"x"}`, `DELETE /category//children`, and `PUT /category//links` with an empty array. Check Aleric's folder name, +version and link rows in the database before and after. + +**Why this step exists.** AIS-SEC-1 (ledger row A20): the region backend was a pure pass-through to +`IInventoryService`, which resolves by UUID and disregards the principal it is handed — so until +`1.1.-alpha+f8263e22b8` every one of those requests **succeeded**, and the last of them would have stripped +another resident's outfit. The suite never saw it because every HTTP fixture ran on `FakeAisBackend`, which +enforces the scoping the real backend lacked. + +**This is the "did it land" check for the AIS-SEC-1 deploy** (procedure step 11) and it fails differently from +the bug it replaces: before the fix the request answers 200 and does the work, after it the request answers 404 +and does nothing. A hash cannot tell those apart. + +### 20. A malformed slam body is refused, and the outfit survives it + +**Do:** as **Truly**, get her own `InventoryAPIv3` cap URL from the seed response and `curl` a deliberately +broken body at her **own** Current Outfit links route: + +``` +curl -X PUT -H "Content-Type: application/llsd+xml" \ + --data-binary '' \ + "/category//links" +``` + +**Expected: 400**, with `"message"` reading `malformed LLSD body`. Then reopen **Appearance**: the outfit is +**unchanged** — same garments, same order. Nothing was written. + +**Then prove the legitimate empty slam still works**, because the fix must not have bought safety by breaking +it: `PUT` a body of exactly `[]` to the same URL. That answers **200** and empties the COF links, which is +what taking off the last garment does. **Re-wear from Outfits afterwards** to put Truly back. + +**Also worth one run each**, same URL, all expected to answer 400 and leave the outfit alone: no body at all +(`--data-binary ''`), and `{}` (`--data-binary ''`). + +**Why this step exists.** AIS-SEC-2 (ledger row A21). `AisHandler.ReadBodyOsd` ended in +`catch { return new OSDMap(); }` and returned that same empty map for an absent body, and +`AisSlam.ParseBody` read an empty map as an empty slam — so a truncated `PUT`, and a dropped connection is +enough, was read as *"replace every link with none"* and emptied the wearer's Current Outfit. Before +`1.1.359-alpha+6c37b5e9d5` the first command above returned **200** and Truly came back naked. + +**The 400 and the `[]` case must both be observed.** Either alone proves nothing: refusing everything would +also pass the first half, and it would break the way an outfit is taken off. + +### 21. Truly and Aleric after the duplicate-folder cleanup + +**Do:** log in as **Truly**, then as **Aleric**. For each: wear one item and remove one item. Open +**Appearance** and confirm it still works. Show the caps list and `curl` their own +`/category//links` — the outfit comes back. + +**Then the database**, per agent: + +```sql +SELECT f.folderID, f.version, + CASE WHEN f.parentFolderID = r.folderID THEN 'ROOT' ELSE 'suitcase' END AS location +FROM inventoryfolders f JOIN inventoryfolders r ON r.agentID=f.agentID AND r.type=8 +WHERE f.type=46 AND f.agentID=''; +``` + +**Expected: exactly one type-46 row parented to ROOT. The suitcase COF is expected and must still be +present** — for Truly that is `52c327c4-cb7d-4365-a7f0-62a6f7545265`, for Aleric `88028d53-4a08-473c-ac52-fb301727edb8`. +Two rows total per agent, one of each location. **A missing suitcase row is a failure, not a success.** + +**Also check `Textures`**, which is what AIS-COF-1 actually cleaned: + +```sql +SELECT COUNT(*) FROM inventoryfolders f JOIN inventoryfolders r ON r.agentID=f.agentID AND r.type=8 +WHERE f.type=0 AND f.parentFolderID=r.folderID AND f.agentID=''; +``` + +**Expected 1.** Aleric had **nine**; Legion Hienrichs had two, and his surviving one must still hold its +**102 items**. + +**And the new WARN must be silent.** After both logins, `grep '\[XINVENTORY\]: agent' ` returns +nothing. A line there means a root-level duplicate has come back, which after this session should be +impossible on a single Robust. + +### 22. Two slams at once on Truly's COF: one outfit wins, never both + +**Do:** as **Truly**, run `two-slam-race.sh` from the AIS-SEC-3 handoff +(`D:\legiongrid\_ops\handoffs\HANDOFF-AIS-SEC-3-20260912.md`). Fill in only the **cap path** and the **two +link sets**; the script targets `category/current/links`, so the COF id is resolved server-side and is not a +placeholder. It fires both `PUT`s concurrently and prints both status codes. + +**Expected:** + +- the two status codes are **both 200**, or **one 200 and one 503** (with `Retry-After: 2`). A 503 is a pass, + not a failure - it means the second slam waited 15 s for the folder and declined to proceed unserialised. +- `GET /category/current/links` afterwards returns **exactly one of the two sets** - never the union, + and never a mixture. +- the region log shows two `SlamFolder ->` lines whose statuses match the two above. If one is 503, it is + preceded by a `WARN [AIS]: SlamFolder on folder for agent waited 15s for the folder lock`. + +**Then confirm the ordinary path still works:** a viewer **Replace Outfit** between two saved outfits, twice. +Both must apply, with `SlamFolder -> 200` each time and SSB following. A lock that serialises correctly and a +lock that deadlocks look identical until you try the normal case. + +**Why this step exists.** AIS-SEC-3 (ledger row A23). A slam snapshots a folder's links, creates the wanted +set, then deletes the snapshot; nothing ordered two of them. Two slams that both snapshotted the old links, +both created their own set and both deleted only what they saw left the folder holding the **union of two +outfits** - reproduced deterministically in `AisConcurrencyHttpTests`. Before +`1.1.367-alpha+24fedc52b9` the script above would leave Truly wearing both sets at once. + +### 23. A legacy WindLight setting applies, and a malformed one leaves the environment alone + +**Not an AIS step** - it belongs to ENV-1 and lives here because this is the grid's only live checklist. +Needs an **estate manager** and, for the first half, a viewer old enough to use the legacy WindLight route +(a modern viewer uses the checked ExtEnvironment handler at `EnvironmentModule.cs:637` instead and will not +exercise this path at all). + +**Do, part 1 - the ordinary case still works.** As an estate manager on Ebony, apply a legacy WindLight +environment setting (Region/Estate > Environment on an older viewer). **Expected:** it applies, and the region +log shows `New Environment settings has been saved from agentID in region Ebony`. + +**Do, part 2 - a malformed body is refused and changes nothing.** Note the region's current environment first. +Then, as the same estate manager, `curl` a deliberately broken body at the legacy setter: + +``` +curl -X POST -H "Content-Type: application/llsd+xml" \ + --data-binary '' \ + "/EnvironmentSettings" +``` + +**Expected:** the response is the handler's ordinary refusal shape - `success: false` with a `fail_reason` of +*"Environment settings for region Ebony were not in the expected format, settings not saved."* - and the region +log carries a **WARN**: + +``` +[Environment ...]: rejected a legacy WindLight setting for region Ebony from agentID : +the body is Unknown, an LLSD array was expected +``` + +Then re-check the environment: **unchanged**. Before ENV-1 that request instead produced a +`NullReferenceException` in the log and a generic *"Environment Set for region ... has failed"* - it also left +the environment alone, but only because the exception happened to land before the write. + +**Why this step is worth running even though the outcome looks the same.** The fix removed a *throw* that was +accidentally protecting a write. The point of part 2 is that the refusal is now deliberate and legible - a WARN +naming the body's type - rather than a stack trace; and the point of part 1 is that the new type check did not +break the legitimate path. **Part 1 is the half that would catch a mistake here**, because refusing everything +would also satisfy part 2. + +See `Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md` §5 for how this was found, and ledger row A26. + +--- + +## The Robust question — RESOLVED, and it was never about step 7 + +> **Robust was reconciled and redeployed on 2026-09-04** (`1.1.208-alpha+a2c8fb63f3`), so `ONLYIFTRASH` is live. +> And step 7 turned out not to be reachable through any viewer, so this was never blocking a resident-visible +> operation — see step 7 above. The section below is kept because the wire-compatibility reasoning still governs +> the route. + + +A2b added an optional `ONLYIFTRASH` field to the inventory wire so that AIS could ask for a folder delete that is +not restricted to Trash. It was made backward-compatible in both directions on purpose: the simulator sends the +field only when it is `false`, and the Robust handler defaults it to `true` when it is absent +(`XInventoryServicesConnector.DeleteFolders`, `XInventoryInConnector.HandleDeleteFolders`). + +Legion Grid resolves inventory **remotely**: `config-include/Grid.ini:12` sets +`InventoryServices = "RemoteXInventoryServicesConnector"`, and `GridCommon.ini:39` points it at +`http://127.0.0.1:8003`. Every folder delete therefore crosses to Robust. + +~~So until the grid server is redeployed with the A2b change, step 7 will fail.~~ That was true of the wire, and +it is now moot twice over: Robust carries the change as of 2026-09-04, and no viewer can request the operation +anyway. + +Emptying Trash (step 8) and deleting a folder **inside** Trash were never affected: those satisfy the old gate, +and step 8 passed before the Robust deploy. + +**The honest summary:** A2b made the route correct; the Robust deploy made it live; neither changed anything a +resident can see. The Robust deploy's real value was ending the four-commit split (see +`../repo-audit/R1-ROBUST-RECONCILIATION.md`), not unblocking step 7. + +## Firestorm is the only client available here + +**Superseded by Ledger P-3 (A8, 2026-09-04).** This section was written expecting the LL viewer to be the +primary run and Firestorm a second pass. That is not possible on this grid: the stock LL viewer will not start +against it, because its Vivox voice component refuses to initialise outside SL. **Every run is a Firestorm run, +and there is no control.** + +Firestorm remains a test client and never an authority (Ledger P-1). What changes is the reading of a green +result: it means Firestorm is satisfied, not that the protocol is right. Anything observed only in Firestorm +must be checked against the LL viewer source before it is relied on — step 13's legacy fallback is the live +example (`A5-RUN-2026-09-04.md`). These are the steps where Firestorm's own machinery differs most, so they +carry the least transferable evidence: + +- **1** (full load) — Firestorm's fetch pacing differs; +- **9 and 10** (wear / take off) — Firestorm has its own outfit machinery and may still send + `AvatarNowWearing` over UDP alongside the slam (open question A-Q6 in the SSB ledger); +- **8** (Empty Trash); +- **12** (library copy). + +If Firestorm and the LL viewer **source** disagree on any of these, the source is right and the difference is +recorded, not fixed against Firestorm. The disagreement has to be found by reading the source, because the +viewer itself cannot be run here. + +--- + +## Stopping + +Turn `AIS_Enabled` back to `false` in the region section and restart the region. The caps disappear from the seed +response and every viewer returns to the legacy paths on its next login. Nothing in inventory needs undoing for the +flag itself — but anything steps 4–12 changed is real, which is what step 0d's backup is for. + +**Report:** for each step, pass / fail / not-run, and for any failure the log lines around it. That report is what +decides whether the flag goes anywhere near a second region. diff --git a/Docs/feature/ais-v3/A5-RUN-2026-09-04.md b/Docs/feature/ais-v3/A5-RUN-2026-09-04.md new file mode 100644 index 00000000000..80a8ea7628a --- /dev/null +++ b/Docs/feature/ais-v3/A5-RUN-2026-09-04.md @@ -0,0 +1,155 @@ +# A5 checklist — live run of 2026-09-04 + +**Region:** Ebony, `[Ebony] AIS_Enabled = true` (`config/OpenSim.ini:163`, section opens `:162`). +**Build:** `1.1.191-alpha+e800eb61b6` for the passing re-run of step 10; steps 1–9 and 11–13 were run against +`1.1.188-alpha+ee6d2a3554`, which differs only in the A7 resolution fix. +**Viewer:** Firestorm 7.2.5. **Avatar:** Truly Bazar (`a7d2ff2e-dc32-44d8-aa61-3d22070a4964`). + +**This is the first end-to-end run in which AIS actually served traffic.** The run before it (A6) never reached +the handler: every request 404'd inside the HTTP server. + +--- + +## Standing constraint — the stock LL viewer cannot be used on this grid + +**Every live checklist from here on is a Firestorm result, and there is no stock-viewer control.** The stock +Second Life viewer will not start against this grid: its Vivox voice component refuses to initialise outside +SL, and that stops the viewer coming up at all. It is not a matter of preference or convenience — the control +run is unavailable. + +This does not change Ledger **P-1**: the LL viewer *source* remains the authority for the AIS v3 protocol, and +Firestorm remains a test client only. What changes is what a green checklist is worth. A pass here means "Firestorm +is satisfied", not "the protocol is right": + +- Where Firestorm and the source disagree, **the source wins** and the difference is recorded, never fixed + against Firestorm. +- Any behaviour observed **only** in Firestorm — step 13's fallback is the important one — must be checked against + the LL viewer source before it is relied on, because Firestorm may have its own path where the LL viewer has + none. +- Behaviour the LL viewer has and Firestorm does not will simply never be exercised here. That is an accepted, + permanent gap in this grid's coverage, not a to-do. + +--- + +## Results + +| # | Step | Result | Observed | Evidence | +|---|---|---|---|---| +| 1 | Full inventory load after cache clear | **pass** | The tree filled in and the item count settled. **Each folder was fetched exactly once** — no folder re-requested. | The A-R3 signature (a folder requested over and over because it was returned without all three `_embedded` collections) did **not** appear. | +| 2 | Open a deep folder | **pass** | Nested folders four levels deep both create and open: `Objects/Test/Test2/Test3`. | Created and opened in-world during the run. | +| 3 | Current outfit reads back | **pass** | Appearance → Wearing lists every worn item under its real name, not "(loading)". | `GET /category/current/links` carried the link targets in `_embedded.items`. | +| 4 | Rename an item | **pass** | A linkset was renamed; the new name survived a relog. | `PATCH /item`. | +| 5 | Rename a folder | **pass** | Folder rename via `UpdateCategory`; survived a relog. | `PATCH /category`. See the double-send note below — it is not a fault. | +| 6 | Delete an item | **pass** | The item was in Trash after a relog, **and no duplicate was left behind in the source folder** — the removal delta was correct. | The stale-copy-in-place symptom would be a missing `_category_items_removed` entry (spec §1d-bis). It did not occur. | +| 7 | Delete a folder **outside** Trash | **not reachable** | **A resident cannot request this.** The viewer's only folder-removal routes are move-to-Trash (a `PATCH`, not a delete), purge a single item in Trash, and Empty Trash. There is no shift-delete for folders. | Verified in-world 2026-09-04: two folders deleted, one nested and one at root — **both moved to Trash** and were still there after a restart. The day's AIS log shows `CreateInventory` and `UpdateCategory`, and **no `RemoveCategory` at any point**. A2b's `ONLYIFTRASH` remains correct and is now live, but was never gating a resident-visible operation. | +| 8 | Empty Trash | **pass** | Trash emptied **completely, including the contents of a subfolder**, and stayed empty through a relog. | Confirms the purge enumeration against real content: unlike a folder delete, nothing viewer-side sweeps the children, so the response has to **enumerate** the direct children itself (spec §1d-bis). A partial empty would have left survivors named in the response; none were left. | +| 9 | Wear an outfit (slam) | **pass** | An outfit was saved and then worn. The slam fired and the outfit applied. The avatar was gray for a few seconds while **Firestorm rebaked client-side**. | The sim did **not** bake, and there is no `[SSB]` line — **correct**: SSB's own triggers are not built yet (that is the L-2 track), so a client-side rebake is the expected path today. Not a defect and not evidence about SSB either way. | +| 10 | Take off a garment | **pass** — on the third attempt, with the cause fixed | Clean run on `1.1.202-alpha+bfb50070d8`: garment removed, ~20 s in-world, logout, relog — **it stayed off**. No slam in between. | Cause established and fixed: not COF resolution (A7, withdrawn by A9/A10) and not a rejected delta, but a **deferred appearance save dropped when the agent left inside the 5 s window** (`A12`, fixed in `dc4e417bb3`). See the clean-run trace below and `A13-STEP10-CLOSED.md`. | +| 11 | Create a folder | **pass** | The folder appeared, kept its name and survived a relog. | `POST /category`. | +| 12 | Copy a library outfit | **pass** | A Library item copied into inventory, is **wearable**, and its permissions came across intact. | `COPY /category` with the destination in the `Destination` header. Permissions intact means the copy carried the source's own masks rather than `NextPermissions`. | +| 13 | Create an inventory **item** | **pass**, and see below | New Notecard: created normally over the legacy UDP path; a resident sees nothing wrong. **AIS was never asked** — corrected by A11, see below. | The outcome the checklist called "the good outcome and worth recording", reached by a route the checklist did not anticipate. | +| 14 | Hypergrid folder deletion — expected refusal | **not run** | Not exercised; no HG visitor in the run. | Known limitation: `HGInventoryService` / `HGSuitcaseInventoryService` answer NOGO for folder deletion whatever the flag says. | +| 15 | Folder thumbnail and favourite — silently dropped | **not run** | Not exercised. | Known limitation: `InventoryFolderBase` has no column for either, so both are accepted and dropped. | + +**Tally:** 12 pass, **1 not reachable (7)**, 2 not run (14, 15). + +### Step 10, closed — the clean run + +Two earlier "passes" were not clean: the first was attributed to a COF-resolution fix that turned out to fix +nothing here, and the second had a full wear-outfit slam between the take-off and the relog, which rewrote COF and +masked the state. This one has neither. + +**Build `1.1.202-alpha+bfb50070d8`** (AIS plus the appearance-save flush), no slam, ~20 s in-world before logout: + +| time | line | +|---|---| +| 17:15:18,402 | `DELETE /item/a4cb683b-…` → RemoveItem | +| 17:15:18,432 | `RemoveItem -> 200 _removed_items=[a4cb683b-…] _updated_category_versions={71c3c184-…:503}` | +| ~17:15:23 | the 5 s timer fires and writes — inferred: no drop WARN, and the change persisted | +| 17:15:38 | logout; attachpoints 40, 7, 8 saved; **no flush line** | +| relog | **the dress stayed off** | + +**The absent flush line is the fix working, not the fix missing.** `FlushAppearanceSaveOnClose` only writes when the +queue actually holds an entry — "a close with nothing queued writes nothing" is its cost guarantee, and it has a +test of its own. Here the timer had already drained the queue at ~17:15:23, fifteen seconds before logout, so there +was nothing left to flush. The flush exists for the *other* case: a logout **inside** the five-second window, which +is exactly what happened at 14:09:40 on the failing run and is what lost the dress. + +--- + +## Step 13 removes the main argument against widening the flag + +The A5 checklist named step 13 as **"the single biggest argument against flipping this flag more widely"**: the +viewer's own AIS path for notecard creation is compiled out (`USE_AIS_FOR_NC`) and expects the server to mint the +asset, which this region does not do, so the route answers 501. The open worry was that the viewer would treat the +501 as a hard failure and the notecard would simply never appear. + +**It does not — and the reason is better than a fallback.** The item is created over the legacy +`CreateInventoryItem` UDP path and the resident sees an ordinary notecard and no error. + +**Corrected by A11: AIS is never asked.** This section previously said "AIS answered 501, and the viewer fell back". +That was an inference, and it is wrong. In `create_inventory_item` +(`F:\viewer-develop\indra\newview\llviewerinventory.cpp`) the entire AIS arm is inside +`#ifdef USE_AIS_FOR_NC` (`:1120`-`:1166`), and that macro is **not defined**. The arm is compiled out, so control +falls unconditionally to `msg->newMessageFast(_PREHASH_CreateInventoryItem)` at **`:1169`**. There is no runtime +decision and therefore no fallback: item creation simply never goes through AIS. + +**The log confirms it.** At the notecard creation there is exactly one AIS line, and it is not a create: + +| time | line | +|---|---| +| 13:19:56,389 | `[INVENTORY ACCESS MODULE]: Received request to create inventory item "New Note" in folder "bb7d5f74-…", transactionID "00000000-…"` | +| 13:19:56,467 | AIS **`FetchItem`** on the newly created item — the viewer syncing the result | + +No `POST /category/{id}` → `CreateInventory` appears anywhere in that window. Our 501 route was never reached, so +the "AIS answered 501" claim had no evidence behind it and has been removed. + +So: the legacy UDP path really did create the item server-side, and the viewer then pulled it back through AIS. + +So the 501 is a **degraded path, not a broken one**, and the specific objection recorded in the checklist is +withdrawn. Two limits on that conclusion, both real: + +- ~~Whose fallback it is remains unconfirmed~~ — **closed by A11 (A-Q15).** The code is in the **LL tree**, not + Firestorm's: `llviewerinventory.cpp:1120` opens the `#ifdef USE_AIS_FOR_NC` arm and `:1169` is the legacy send + that follows it. Because the macro is undefined the arm never compiles, so **every** viewer built from this + source — stock included — creates items over UDP and never asks AIS. This is stronger than a fallback holding + under test: there is no branch to take. Stock-viewer residents get exactly the behaviour observed here, and + P-3's "no control run" caveat does not bite, because the answer came from the source rather than from a run. +- It removes *this* objection **fully**, for every viewer rather than just for Firestorm. It still does not make + the case for widening on its own — steps 7, 14 and 15 are unrun, and A-R1 (no + fallback for deletes, purges and slams — those really are AIS-only) is untouched by it. + +--- + +## Firestorm sends `UpdateCategory` twice for one folder rename + +A single folder rename produced **two identical `PATCH /category` requests**, milliseconds apart: + +| | | +|---|---| +| first | 11:52:32 | +| second | 11:52:41 | + +**Harmless, and not a loop.** The operation is idempotent: the second request sets the folder to the name it +already has and produces the same delta. It is recorded here and as ledger **A-Q14** for one reason — so that a +later reader looking at a log full of doubled `PATCH /category` lines does not mistake it for the A-R3 re-fetch +loop, which looks superficially similar and is a genuine fault. The distinguishing feature is that A-R3 repeats +*without bound* while this repeats exactly once. + +No action taken. It is a client behaviour, not a server one, and nothing on the server should be keyed to it +(Ledger P-1). + +--- + +## What this run does and does not establish + +**Establishes:** AIS serves real inventory traffic to a real viewer for reads, renames, deletes, purges, outfit +slams, folder creation and library copies, with correct deltas and no re-fetch loop. Step 8 in particular confirms +the purge enumeration against real nested content, and step 13 confirms server-side that a 501 falls back to a +real legacy creation rather than a silent loss. + +**Does not establish:** anything about the stock LL viewer, including **whose** fallback step 13 exercised (A-Q15); +anything about steps 7, 14, 15; anything about +behaviour under concurrent users — the run was one avatar; or that the A7 duplicate-COF data fault is gone. It is +not. Resolution now picks correctly, but seven accounts still hold two Current Outfit folders each and will keep +logging the A7 WARN until the dedupe is run. diff --git a/Docs/feature/ais-v3/A6-LIVE-FAILURE.md b/Docs/feature/ais-v3/A6-LIVE-FAILURE.md new file mode 100644 index 00000000000..e37abd641f9 --- /dev/null +++ b/Docs/feature/ais-v3/A6-LIVE-FAILURE.md @@ -0,0 +1,117 @@ +# A6 — the first live run failed, and why + +**Date:** 2026-09-04. **Region:** Ebony. **Viewer:** Firestorm 7.2.5. **Avatar:** Truly Bazar. +**Symptom:** every inventory folder empty and stayed empty; "worn folder could not be found so clothing could not +be downloaded"; avatar a cloud. The region log shows the startup line once and then **no AIS lines at all**. + +## The short version + +**The caps were advertised correctly. Every request to them then 404'd inside the HTTP server, before the handler +was ever entered.** AIS is the first capability in this tree whose URLs carry sub-paths, and it was registered in +the dictionary that only matches a path *exactly*. + +## Why "no AIS lines" proved nothing + +The request path had **no logging whatsoever** — not at registration, not at handler entry, not on error. So +"zero AIS lines" was equally consistent with "no request arrived" and with "every request arrived and was +rejected before reaching us". It could not distinguish them, which is why item (d) of the brief exists and why +the fix adds both log points. + +The in-world symptom does distinguish them, and it points the other way from the brief's leading hypothesis. **If +the caps had been missing from the seed response, nothing would have broken**: `AISAPI::isAvailable()` would have +returned false and Firestorm would have used the legacy `FetchInventoryDescendents2` path exactly as it did the +day before. Inventory came up empty *because* the viewer got the caps, switched everything to AIS, and then got +nothing back. + +## (a) Where AIS registers, and the side-by-side that matters + +Registration itself is correct and identical to a cap that works. + +| | `FetchInventory2Module` (works) | `AISv3Module` (failed) | +|---|---|---| +| Subscribes | `s.EventManager.OnRegisterCaps += RegisterCaps` in `RegionLoaded` (`FetchInventory2Module.cs:112`) | same, in `RegionLoaded` (`AISv3Module.cs`) | +| Registers | `caps.RegisterSimpleHandler("FetchInventory2", new SimpleOSDMapHandler("POST", "/" + UUID.Random(), …))` (`:141-149`) | `caps.RegisterSimpleHandler(CapName, new AisHandler("/" + UUID.Random(), …))` | +| Reaches the seed? | yes | **yes — this was never the problem** | +| **URL shape the viewer uses** | the cap URL **exactly**: `POST ` | the cap URL **plus a sub-path**: `GET /category/{id}/children`, `/item/{id}`, `/orphans` | + +That last row is the whole bug. + +Both go `Caps.RegisterSimpleHandler` (`Source/OpenSim.Capabilities/Caps.cs:196-200`) → +`CapsHandlers.AddSimpleHandler` (`CapsHandlers.cs:94-100`) → `m_httpListener.AddSimpleStreamHandler(handler)`. +That call takes a second parameter which neither `Caps` nor `CapsHandlers` exposes: + +```csharp +public void AddSimpleStreamHandler(ISimpleStreamHandler handler, bool varPath = false) // BaseHttpServer.cs:358-364 +{ + if (varPath) + m_simpleStreamVarPath.TryAdd(handler.Path, handler); + else + m_simpleStreamHandlers.TryAdd(handler.Path, handler); +} +``` + +It defaults to **false**, so AIS landed in `m_simpleStreamHandlers`. And that dictionary is matched **exactly**: + +```csharp +private bool TryGetSimpleStreamHandler(string uripath, out ISimpleStreamHandler handler) // BaseHttpServer.cs:1109-1123 +{ + if (m_simpleStreamHandlers.TryGetValue(uripath, out handler)) // exact match only + return true; + + // look only for keyword before second slash ( /keyword/someparameter/... ) + handler = null; + if (uripath.Length < 3) return false; + int indx = uripath.IndexOf('/', 2); + if (indx < 0 || indx == uripath.Length - 1) return false; + return m_simpleStreamVarPath.TryGetValue(uripath[..indx], out handler); // sub-paths live HERE +} +``` + +The dispatcher matches on `Util.TrimEndSlash(request.UriPath)` (`BaseHttpServer.cs:702-704`), i.e. the path +without the query string. For a cap registered at `/`: + +- `POST /` — FetchInventory2's shape — hits the exact-match branch. Works. +- `GET //category//children` — AIS's shape — misses the exact match, falls to the var-path branch, + where `uripath.IndexOf('/', 2)` lands on the slash after the 36-character UUID and looks up `/` in + `m_simpleStreamVarPath` — **which is empty for us**. Returns false. The server answers 404 and the handler is + never entered. + +So the var-path branch is exactly the mechanism AIS needs, and the key it would look up is exactly the path AIS +registered. Only the dictionary was wrong. + +**Precedent:** every handler in this tree that serves sub-paths already passes `varPath: true` — +`GatekeeperServerConnector.cs:70`, `UserAgentServerConnector.cs:107`, `NeighbourServiceInConnector.cs:61`, +`SimulationServiceInConnector.cs:50-51`, `XBakesHandler.cs:62`. None of them is a **cap**, which is why +`Caps.RegisterSimpleHandler` never needed the parameter until now. AIS is the first sub-path cap in the tree. + +## (b) Timing — not the cause + +`RegionLoaded` ran: the startup line printed once, and it is emitted *after* `OnRegisterCaps += Handler`. The +subscription is taken on the same `Scene` object the log line names (`scene.EventManager`, with the scene captured +in the closure that `RemoveRegion` later unsubscribes). Truly logged in 18 minutes later, so there is no race. +`TriggerOnRegisterCaps` (`EventManager.cs:2119-2138`) invokes each delegate in a try/catch and logs +`[EVENT MANAGER]: Delegate for TriggerOnRegisterCaps failed` on a throw — no such line appeared either. + +## (c) Cap names — not the cause + +`AISv3Module.CapName = "InventoryAPIv3"` and `LibraryCapName = "LibraryAPIv3"`, matching `llaisapi.cpp:48-49` +character for character, and pinned by a test. `SeedCapRequest` adds every requested name to `validCaps` with no +whitelist (`BunchOfCaps.cs:340-376`), and `GetCapsDetailsLLSDxml` emits a URL for any name present in either +handler dictionary (`CapsHandlers.cs`), so a registered name is advertised. + +## (d) Why nothing could be seen + +Two log points were missing and are added by the fix: + +- **at registration**, DEBUG, naming the agent and the URL produced — so a live run shows registration per agent + rather than only the once-per-region startup line; +- **at handler entry**, DEBUG, naming the verb, path and resolved operation — so a request that arrives and fails + is distinguishable from one that never arrives. + +Had either existed, this would have been a one-minute diagnosis instead of a code read. + +## What this says about the test suite + +114 tests passed while this was completely broken. They drive `AisHandler.Handle(request, response)` directly, so +they exercise routing, envelopes and every operation — and never touch how the handler is bound to a URL. The bug +lived entirely in the two lines between `RegisterCaps` and the HTTP server, which no test observed. diff --git a/Docs/feature/ais-v3/A7-DUPLICATE-COF.md b/Docs/feature/ais-v3/A7-DUPLICATE-COF.md new file mode 100644 index 00000000000..3bf12327db3 --- /dev/null +++ b/Docs/feature/ais-v3/A7-DUPLICATE-COF.md @@ -0,0 +1,159 @@ +# A7 — two Current Outfit folders, and which one wins + +> **CORRECTED 2026-09-04 by A9. The central claim of this document was wrong.** +> +> There are **no duplicate Current Outfit folders on this grid**. The version-1 folders are the Current Outfit +> folders of the **HG suitcase skeleton** — parented to `My Suitcase` (type 100) and created by +> `HGSuitcaseInventoryService.CreateSystemFolders` (`:172-186`). Sixteen system folder types show the same +> pattern across the same seven accounts, which are exactly the seven accounts that have a suitcase. Counting +> agents with more than one type-46 folder *outside* any suitcase returns **zero**. +> +> Evidence and the full before-state table: `A9-SUITCASE-NOT-DUPLICATE.md`. +> +> **Also corrected:** the failure this document was written to explain was **not** a resolution failure. The +> region log shows every slam went to `71c3c184…`, the correct root COF. Step 10's real cause is re-diagnosed in +> `A10-STEP10-REDIAGNOSIS.md`. +> +> **What still stands:** the resolution in `AisInventory.GetSystemFolder` (`6cd13a3645`) is sound and worth +> keeping — `folders[0]` over an unordered query genuinely is non-deterministic, and a deterministic rule with a +> WARN is better than a coin flip. It simply was not fixing the bug we thought it was. See **A-Q16** for a latent +> risk the correct picture exposes. +> +> The original text follows, kept because the reasoning error in it is worth reading. **Sections (a)–(c) below +> are superseded.** + +--- + +**Date:** 2026-09-04. **Region:** Ebony, `AIS_Enabled = true`. **Avatar:** Truly Bazar +(`a7d2ff2e-dc32-44d8-aa61-3d22070a4964`). **Checklist step:** 10, take off a garment. + +**Symptom:** the skirt came off and was back after a relog. The viewer slammed +`71c3c184-410b-4dae-b20a-855741cf1faf` twice (12:36, 12:37); at login 12:38 it fetched `/category/current/links` +and immediately rebuilt links in `71c3c184…`. The avatar has two type-46 folders: + +| folder | name | type | version | ~~claim~~ **actual (A9)** | +|---|---|---|---|---| +| `71c3c184-410b-4dae-b20a-855741cf1faf` | Current Outfit | 46 | 457 (now 500) | the real COF, under `My Inventory` | +| `52c327c4-cb7d-4365-a7f0-62a6f7545265` | Current Outfit | 46 | 1 | ~~the one we returned~~ **the suitcase's COF, under `My Suitcase`** | + +## ~~The short version~~ — SUPERSEDED + +> ~~Our `"current"` alias resolves to an arbitrary one of the agent's type-46 folders, and it picked the wrong +> one. The take-off did happen — it was written to a folder no viewer reads.~~ +> +> **It did not pick the wrong one.** The log shows the slam went to `71c3c184…` every time. + +## (a) What the resolution actually promises: nothing — STILL TRUE, but not the cause + +This section's *description of the code* is accurate and unchanged. What was wrong was the inference drawn from it. + +`AisInventory.GetCurrentOutfit` (`AisInventory.cs:106-107`) → `IAisInventoryBackend.GetFolderForType` +(`IAisInventoryBackend.cs:20`) → `InventoryServiceBackend` (`AISv3Module.cs:172`) → `IInventoryService`. On Legion +Grid inventory is remote (`RemoteXInventoryServiceConnector.cs:175-178` → `XInventoryServicesConnector.cs:186-195`, +`METHOD=GETFOLDERFORTYPE`) → Robust `XInventoryInConnector.cs:260` → `XInventoryService.GetFolderForType`. + +Robust runs the plain service for the region-facing port (`Robust.ini:107-108`), so this is the code that answers: + +```csharp +private InventoryFolderBase GetSystemFolderForType(InventoryFolderBase rootFolder, FolderType type) // XInventoryService.cs:272-294 +{ + if (type == FolderType.Root) + return rootFolder; + + XInventoryFolder[] folders = m_Database.GetFolders( + ["agentID", "parentFolderID", "type"], + [rootFolder.Owner.ToString(), rootFolder.ID.ToString(), ((int)type).ToString()]); + + if (folders.Length == 0) + return null; + + return ConvertToOpenSim(folders[0]); // first row wins. No ordering, no tie-break, no warning. +} +``` + +`folders[0]`, and the query behind it has **no `ORDER BY` and no `LIMIT`**: `MySQLXInventoryData.GetFolders` +(`:56-59`) → `MySqlFolderHandler` (no `Get` override, `:250-256`) → `MySQLGenericTableHandler.Get(string[], +string[])` (`:154-157`), which delegates with `options = String.Empty` (`:159-185`). The schema permits duplicates: +`inventoryfolders` (`InventoryStore.migrations:30-40`) has `PRIMARY KEY (folderID)` and non-unique keys only. + +**All of that remains true.** It is a real latent fragility and the reason `6cd13a3645` is worth keeping. It was +simply not what broke step 10. + +### The reasoning error, stated plainly + +> **A consequence worth stating:** this query filters on `parentFolderID = rootFolder.ID`. Two rows can only both +> match if **both COFs are direct children of the same root folder**. `52c327c4…` therefore certainly is. + +The first sentence is correct. **The last sentence does not follow, and it is where this went wrong.** + +The filter means `GetSystemFolderForType` can only ever return a folder parented to the root. I combined that with +an unverified premise — "AIS returned `52c327c4…`" — and concluded `52c327c4…` must be under root. The premise was +never checked; it came from the brief's own reading of the symptom and I adopted it as fact. + +Run the other way, the same filter refutes the premise: `52c327c4…` is under `My Suitcase`, so +`GetSystemFolderForType` **could not have returned it**, so AIS was never resolving to it. One read-only query +would have caught this before a line of code was written. The lesson is not "check parentage" but: **when a +diagnosis rests on an assumed observation, verify the observation first, especially when it is the one fact that +makes the rest of the argument work.** + +## (b) Every site that creates a type-46 folder — table still correct, conclusion inverted + +| # | Site | When it fires | Guarded? | Parents to | +|---|---|---|---|---| +| 1 | `XInventoryService.CreateUserInventory` `:132-133` | account creation `UserAccountService.cs:817`; `RemoteAdminPlugin.cs:2630`; every Direct Delivery `DirectDeliveryPostHandler.cs:134, :217`; `XInventoryInConnector.cs:205` | yes — `GetSystemFolders` `:194-208` scans all root children | the real root | +| 2 | `HGSuitcaseInventoryService.CreateSystemFolders` `:185-186` | once per user, from `GetRootFolder` `:136-170` when they have no suitcase yet | yes, same shape | **the suitcase** | +| 3 | `HGInventoryService` `:103-115` | HG visitor with no suitcase | — | creates only a suitcase | +| 4 | IAR load | — | — | creates no type-46 | + +~~Sites 2, 3 and 4 are excluded by the parentage consequence… That leaves site 1 as the only in-tree path that can +put a second type-46 under the root.~~ + +**Inverted by A9. Site 2 is the answer, and it is not a bug.** Site 2 created the version-1 folders, on purpose, +as part of the suitcase skeleton. Site 1 created nothing extra — the `EnsureSystemFolder` narrowing committed in +`68bfa60735` is defensible hardening of a real read-then-write race, but **no observed duplicate is attributable to +it**, and it should not be described as fixing one. + +## (c) Does this predate AIS? — the question was moot + +The section argued that every `GetFolderForType` caller inherits the same coin flip and that AIS merely made a +latent data fault visible. The first half stands as a statement about the code. The second half does not: **there +was no data fault.** + +## The rule chosen, and why — STILL THE RULE, with a caveat + +Highest `Version`, ties on lowest folder id. The justification from the tree is unchanged and still holds: + +- version is bumped on every child add/remove (`MySqlFolderHandler.Store :283-291`, `MoveFolder :258-281` → + `IncrementFolderVersion`, `MySQLXInventoryData.cs:303-317`) and never decreases (`UpdateFolder :423-427`, + `:439-440`); +- creation order is not available — `XInventoryFolder` has six fields (`IXInventoryData.cs:32-45`) and the table + has no timestamp; +- descendant count is unusable — a legitimately emptied COF has none. + +**Caveat added by A9, tracked as A-Q16.** Resolution scans the skeleton, which for a local user includes the +suitcase subtree. Suitcase COFs sit at version 1 today, so the root COF always wins — but nothing enforces that. +If a suitcase COF ever overtook the root COF, a local user's outfit would resolve into their suitcase. + +## ~~What would confirm the creation site~~ — RUN, AND IT REFUTED THIS DOCUMENT + +The query this section proposed was run in A9. Its own stated failure branch was the one that came true: + +> If the second row's parent is the suitcase, the parentage argument in (a) is wrong and the diagnosis must be +> reopened. + +It is, it was, and it has been. + +## ~~What the dedupe should do~~ — DO NOT RUN A DEDUPE + +There is nothing to deduplicate. Running the proposed dedupe would have deleted a live part of the HG suitcase +skeleton for seven accounts, and it would **not** have grown back: `CreateSystemFolders` is only called when the +suitcase itself is missing (`GetRootFolder :152-165`). + +## Ledger items + +- **A-R8** — corrected. A unique index on `(agentID, type)` **must not be added**: it would reject the legitimate + suitcase skeleton and break suitcase creation grid-wide. See the ledger for the candidate shape, which is not + settled. +- **A-Q13** — unchanged. `InventoryFolderBase.Version` is `ushort` (`InventoryFolderBase.cs:67`) against an + `int(11)` column. +- **A-Q16** — new, the suitcase-overtakes-root risk described above. diff --git a/Docs/feature/ais-v3/A9-SUITCASE-NOT-DUPLICATE.md b/Docs/feature/ais-v3/A9-SUITCASE-NOT-DUPLICATE.md new file mode 100644 index 00000000000..8a249ebf5f1 --- /dev/null +++ b/Docs/feature/ais-v3/A9-SUITCASE-NOT-DUPLICATE.md @@ -0,0 +1,108 @@ +# A9 — the "duplicate Current Outfit folders" are the HG suitcase skeleton + +**Date:** 2026-09-04. **Purpose of the session:** deduplicate the seven accounts believed to hold two Current +Outfit folders each, per the proposal in `A7-DUPLICATE-COF.md`. + +**Outcome: stopped at the plan step with the database untouched. There is nothing to deduplicate.** The session +issued only `SELECT`s and one `mysqldump`. No `UPDATE`, `DELETE` or `INSERT` was executed. + +**Backup taken before any inspection:** `D:\legiongrid\_backup\legiongrid-predupe-20260904-1332.sql`, +2,685,971,589 bytes, no BOM, terminating in `-- Dump completed on 2026-09-04 18:34:21`. + +> **Backup hygiene, worth carrying forward.** The first attempt piped `mysqldump` through PowerShell's +> `Set-Content -Encoding utf8`. It produced a 4.08 GB file with a UTF-8 BOM, and it was **corrupt**: PowerShell +> decodes and re-encodes the byte stream, replacing every byte that is not valid text with U+FFFD, which mangles +> binary column data. The 1.4 GB of inflation over the correct 2.50 GB was that damage. It was deleted and the +> dump re-taken with byte-exact shell redirection. **Any `.sql` in `_backup\` produced through a PowerShell text +> pipeline should be assumed unrestorable.** + +--- + +## Before-state survey + +Every agent with more than one type-46 folder, with each folder's id, version, parent and child counts. Keeper +(by the A7 rule: highest version) in **bold**. + +| agentID | folderID | version | parentFolderID | subFolders | items | +|---|---|---|---|---|---| +| 0f62cf39-71b8-49e1-94ea-ebdf54be01e2 | **2b74a4cd-e384-4778-86c4-80f057b713d1** | **115** | 0f62cf39-71b8-49e1-94ea-ebdf54be01e2 | 0 | 12 | +| 0f62cf39-71b8-49e1-94ea-ebdf54be01e2 | 2eb36833-915b-4076-9226-671ec914bf96 | 1 | 8ee583b3-9259-40e4-989a-431492d85768 | 0 | 0 | +| 47dd39a8-1261-45d2-9fc9-986def3a97b6 | **d2d5a12b-b2a0-412b-8c40-9a8e222dda12** | **9** | 97a4d413-5be4-4551-b977-470bd1f45e1b | 0 | 6 | +| 47dd39a8-1261-45d2-9fc9-986def3a97b6 | 08f89f73-f572-4382-bf09-1196f810ef7a | 1 | b36b7761-4da1-47d9-b138-1e15e20cb936 | 0 | 0 | +| 4dc144cb-4335-4d5f-ac2d-b2c87d0f67e9 | **509aa3ff-15be-4cc6-be8b-5c9aa8398a42** | **36** | 4dc144cb-4335-4d5f-ac2d-b2c87d0f67e9 | 0 | 15 | +| 4dc144cb-4335-4d5f-ac2d-b2c87d0f67e9 | 88028d53-4a08-473c-ac52-fb301727edb8 | 1 | 36b2d277-a69e-47ec-b835-140e20f42e09 | 0 | 0 | +| 4fbdfd2a-e0c6-4003-b2f8-8714fcc7b968 | **4161565b-b08b-490e-80d6-a5a61227bc0f** | **716** | dd748992-4298-4dc6-88b3-d5d72e00226c | 0 | 11 | +| 4fbdfd2a-e0c6-4003-b2f8-8714fcc7b968 | 2acd261c-af0b-4393-a183-5f2cfec6271d | 1 | aa02d15b-78d1-4c2e-9630-226db8a0f36e | 0 | 0 | +| 5266d93e-d723-4317-a653-227bd676dddd | **7853c313-6d58-4816-a1dc-66f7e9fb6d1b** | **9** | 88dcd9ee-7f7b-45b8-a323-8c055054a00a | 0 | 6 | +| 5266d93e-d723-4317-a653-227bd676dddd | 856550ad-84d5-4316-9898-67b020ab347a | 1 | 4f89c68d-8c50-4ddc-8ee3-0bf193dca988 | 0 | 0 | +| a7d2ff2e-dc32-44d8-aa61-3d22070a4964 | **71c3c184-410b-4dae-b20a-855741cf1faf** | **500** | bb7d5f74-a4cf-47cf-9f1d-96f60c1cd954 | 0 | 14 | +| a7d2ff2e-dc32-44d8-aa61-3d22070a4964 | 52c327c4-cb7d-4365-a7f0-62a6f7545265 | 1 | ec7a4f10-2307-4c23-857e-af0550216ea1 | 0 | 0 | +| c0b98d62-9705-4ca5-8f8a-902ad6ee9083 | **fb4e5690-4305-4863-b236-cb594aed5655** | **41** | c0b98d62-9705-4ca5-8f8a-902ad6ee9083 | 0 | 10 | +| c0b98d62-9705-4ca5-8f8a-902ad6ee9083 | 52f5dcdb-7ea7-4bbf-a015-78499f7ba46f | 1 | 40a7c741-4dc5-46fc-8df2-b9811d4ff7d5 | 0 | 0 | + +Seven accounts, as expected, and every version-1 folder is empty. **But the two folders in each pair have +different parents** — which `A7-DUPLICATE-COF.md` had asserted was impossible. That is what stopped the run. + +## Resolving the parents + +| agent | keeper's parent | loser's parent | +|---|---|---| +| all seven | `My Inventory`, **type 8**, `parentFolderID = 00000000-…` | `My Suitcase`, **type 100** | + +For Truly Bazar specifically: + +| folderID | version | parent | parentName | parentType | +|---|---|---|---|---| +| 52c327c4-cb7d-4365-a7f0-62a6f7545265 | 1 | ec7a4f10-2307-4c23-857e-af0550216ea1 | **My Suitcase** | **100** | +| 71c3c184-410b-4dae-b20a-855741cf1faf | 500 | bb7d5f74-a4cf-47cf-9f1d-96f60c1cd954 | **My Inventory** | **8** | + +The version-1 folders are the Current Outfit folders of the **HG suitcase skeleton**, created deliberately by +`HGSuitcaseInventoryService.CreateSystemFolders` (`:172-186`) when the suitcase is made (`GetRootFolder :152-165`). + +## The three confirmations + +**1. The affected accounts are exactly the accounts with a suitcase.** + +| query | result | +|---|---| +| accounts with a type-100 folder | **7** | +| overlap between "accounts with >1 type-46" and "accounts with a suitcase" | **7 of 7** | + +**2. It is not Current-Outfit-specific — sixteen system types show the same pattern, across the same seven +accounts.** That is the whole suitcase skeleton as `CreateSystemFolders` builds it. + +| type | accounts affected | | type | accounts affected | +|---|---|---|---|---| +| 1 | 7 | | 15 | 7 | +| 2 | **14** | | 16 | 7 | +| 3 | 7 | | 20 | 7 | +| 5 | 7 | | 21 | 7 | +| 6 | 7 | | 23 | 7 | +| 7 | 7 | | **46** | **7** | +| 10 | 7 | | 56 | 7 | +| 13 | 7 | | | | +| 14 | 7 | | | | + +Type 2 (Calling Cards) shows 14 because the skeleton nests `Friends` and `All` beneath it — three per tree, as +`CreateUserInventory :122-127` and `CreateSystemFolders` both do. + +**3. Genuine duplicates: zero.** Counting agents with more than one type-46 folder whose parent is *not* a +suitcase returns **0**. + +## What executing the dedupe would have done + +Deleted a live part of the HG suitcase skeleton for seven accounts, permanently: `CreateSystemFolders` is called +only when the *suitcase itself* is missing (`GetRootFolder :152-165`), so a deleted suitcase COF does not grow +back. Applied consistently the same rule would have taken the other fifteen types too — and the plan's own +"report any other duplicated type" step ran *after* the deletions, so it would have reported the damage rather +than prevented it. + +## Consequences for the record + +- `A7-DUPLICATE-COF.md` is corrected in place, with the superseded reasoning kept visible. +- **A-R8** is corrected: no unique index on `(agentID, type)`. It would reject the suitcase skeleton and break + suitcase creation grid-wide. +- **A-Q16** is opened: for a local user the A7 rule scans every type-46 folder including the suitcase's. +- The resolution fix `6cd13a3645` stands on its own merits — `folders[0]` over an unordered query really is + non-deterministic — but it did not fix step 10, because COF resolution was never wrong. Step 10 is + re-diagnosed in `A10-STEP10-REDIAGNOSIS.md`. diff --git a/Docs/feature/ais-v3/AIS-V3-SPEC.md b/Docs/feature/ais-v3/AIS-V3-SPEC.md new file mode 100644 index 00000000000..d0048d7f33b --- /dev/null +++ b/Docs/feature/ais-v3/AIS-V3-SPEC.md @@ -0,0 +1,541 @@ +# AIS v3 — the surface the LL viewer drives + +**Authority (Ledger P-1):** the LL viewer source at `F:\viewer-develop` (viewer 26.1.1). Every row below cites +the file and line it was read from. Files read, read-only: `indra/newview/llaisapi.h` (167 lines), +`indra/newview/llaisapi.cpp` (1798 lines), the AIS call site in `indra/newview/llinventorymodel.cpp` +(`:1025-1058`), and the AIS call sites `remove_inventory_item`, `remove_inventory_category`, +`purge_descendents_of`, `slam_inventory_folder` in `indra/newview/llviewerinventory.cpp`. Anything that could +not be pinned to a line in those files is marked **UNVERIFIED** and says which file would settle it. + +This document is the contract every A-session implements. Later sessions do not open the viewer tree. + +Cap names: `InventoryAPIv3` (`llaisapi.cpp:48`) and `LibraryAPIv3` (`:49`). The viewer asks the seed cap for +both (`AISAPI::getCapNames`, `:72-76`). HTTP timeout per request 180 s (`:50`). Maximum requested folder depth +50 (`MAX_FOLDER_DEPTH_REQUEST`, `:58`). + +## 1a. Operations + +`{inv}` = the InventoryAPIv3 cap URL, `{lib}` = the LibraryAPIv3 cap URL. `tid` is a fresh random UUID per call +(`LLUUID tid; tid.generate();`). Bodies are **LLSD XML** (A-Q2, resolved A1): every request body is serialised with `LLSDSerialize::toXML` +(`indra/llmessage/llcorehttputil.cpp:144` POST, `:169` PUT, `:193` PATCH) and every response parsed with +`LLSDSerialize::fromXML` (`:123`, `responseToLLSD`). `HttpCoroutineAdapter::checkDefaultHeaders` (`:1211-1229`) +sets both `Content-Type` and `Accept` to `HTTP_CONTENT_LLSD_XML` on every AIS request unless the caller +already set them; the literal is `application/llsd+xml` per the comments at `:478` and `:497`. The response +parse itself does **not** check the content type — it only gates a warning (`:495-500`) — so a response is +read as LLSD XML whatever it is labelled. (The file is at `indra/llmessage/`, not `indra/llcorehttp/`.) + +| # | Operation (`llaisapi.h`) | Verb | URL relative to the cap | Query | Body | Headers | Source | +|---|---|---|---|---|---|---|---| +| 1 | `CreateInventory(parentId, newInventory)` | POST | `{inv}/category/{parentId}` | `tid={uuid}` | `newInventory` map: `categories` (array of category maps) verified at `llinventorymodel.cpp:1035-1042`; `items` / `links` arrays **UNVERIFIED** (built in `llviewerinventory.cpp:1156,1370`, outside the permitted functions) | none | `llaisapi.cpp:99-143`, url `:115` | +| 2 | `SlamFolder(folderId, newInventory)` | PUT | `{inv}/category/{folderId}/links` | `tid={uuid}` | `contents` as passed by the caller (`slam_inventory_folder`, `llviewerinventory.cpp:1776-1784`); shape **UNVERIFIED** (built by `LLAppearanceMgr`, not permitted): expected `{ "links": [ link maps ] }` | none | `llaisapi.cpp:145-180`, url `:161` | +| 3 | `RemoveCategory(categoryId)` | DELETE | `{inv}/category/{categoryId}` | — | none | none | `:182-217`, url `:197` | +| 4 | `RemoveItem(itemId)` | DELETE | `{inv}/item/{itemId}` | — | none | none | `:219-252`, url `:234` | +| 5 | `CopyLibraryCategory(sourceId, destId, copySubfolders)` | COPY | `{lib}/category/{sourceId}` | `tid={uuid}` and, when `!copySubfolders`, the literal suffix `,depth=0` **appended to the tid value with a comma** (`url += ",depth=0"`, `:278`), i.e. `?tid=,depth=0` | none | destination = `destId.asString()` (`:282`) passed as the `copyAndSuspend` destination argument (`:294`), which appends it as the HTTP **`Destination`** header: `headers->append(HTTP_OUT_HEADER_DESTINATION, dest)` (`llcorehttputil.cpp:1135`) — A-Q2, resolved A1 | `:255-301`, url `:275` | +| 6 | `PurgeDescendents(categoryId)` | DELETE | `{inv}/category/{categoryId}/children` | — | none | none | `:303-339`, url `:318` | +| 7 | `UpdateCategory(categoryId, updates)` | PATCH | `{inv}/category/{categoryId}` | — | `updates` map of category fields (callers at `llviewerinventory.cpp:663,881,1455`, outside the permitted functions: field set **UNVERIFIED**) | none | `:341-374`, url `:355` | +| 8 | `UpdateItem(itemId, updates)` | PATCH | `{inv}/item/{itemId}` | — | the item's full `asLLSD()` with `asset_id`/`shadow_id` replaced by `hash_id` (callers `:454,1422,1434`; field set **verified**, see §1d-ter) | none | `:376-409`, url `:391` | +| 9 | `FetchItem(itemId, type)` | GET | `{inv|lib}/item/{itemId}` (`lib` when `type == LIBRARY`) | — | none | none | `:412-445`, url `:426` | +| 10 | `FetchCategoryChildren(catId, type, recursive, depth)` | GET | `{inv|lib}/category/{catId}/children` | `depth=N` where N = 50 if `recursive`, else `min(depth, 50)` (`:463-474`) | none (the viewer keeps `{"depth": N}` locally as `request_body` for error handling, `:490`) | none | `:447-498` | +| 11 | `FetchCategoryChildren(identifier, recursive, depth)` | GET | `{inv}/category/{identifier}/children` — `identifier` is any string, e.g. an alias | `depth=N` as above (`:527`) | none | none | `:500-549`, url `:514` | +| 12 | `FetchCategoryCategories(catId, type, recursive, depth)` | GET | `{inv|lib}/category/{catId}/categories` | `depth=N` (`:578`) | none | none | `:551-599`, url `:565` | +| 13 | `FetchCategorySubset(catId, specificChildren, type, recursive, depth)` | GET | `{inv|lib}/category/{catId}/children` | `depth=N&children={id1},{id2},...` (`:642-648`); the viewer warns above 2000 URL characters (`:651`) but still sends | none | none | `:601-678`, url `:628` | +| 14 | `FetchCOF()` | GET | `{inv}/category/current/links` | — (local `depth` 0, `:709`, not on the URL) | none | none | `:680-714`, url `:692` | +| 15 | `FetchCategoryLinks(catId)` | GET | `{inv}/category/{catId}/links` | — (local depth 0, `:745`) | none | none | `:716-751`, url `:728` | +| 16 | `FetchOrphans()` | GET | `{inv}/orphans` | — | none | none | `:753-784`, url `:765` | + +Every operation first resolves the cap (`getInvCap()` / `getLibCap()`, `:79-97`); with no cap the callback fires +with a null id and nothing is sent. Requests are coroutines throttled to 2048 in flight (`:54`, `:786-834`). + +The `simulate` query parameter mentioned in the session brief does **not** appear anywhere in `llaisapi.cpp`; +the viewer never sends it. **UNVERIFIED** whether any other viewer code path adds it. + +## 1b. Aliases + +- `current` — the Current Outfit folder, used as `{inv}/category/current/links` by `FetchCOF` (`:692`). This is + the only alias literal in `llaisapi.cpp`. The string-identifier overload of `FetchCategoryChildren` (`:500-549`) + accepts any identifier, so `current/children` is a legal request shape; which callers use it is **UNVERIFIED** + (callers are outside the permitted files). +- No other alias appears in the permitted files. + +## 1c. Response envelope + +Every response, success or error, goes through `AISUpdate` (`onUpdateReceived`, `:836-849` → `AISUpdate::doUpdate`). +`parseUpdate` = `parseMeta` then `parseContent` (`:1094-1099`). The viewer distinguishes **fetch** commands +(`FETCHITEM, FETCHCATEGORYCHILDREN, FETCHCATEGORYCATEGORIES, FETCHCATEGORYSUBSET, FETCHCOF, FETCHCATEGORYLINKS, +FETCHORPHANS`, `:1028-1034`) from **mutations**; the difference matters for filtering below. + +### Meta keys (`parseMeta`, `:1101-1177`) — all top-level in the response map + +| Key | LLSD type | What the viewer does | +|---|---|---| +| `_categories_removed` | array of uuid | each known category: parent's descendent delta −1, id queued for deletion (`:1105-1120`) | +| `_category_items_removed` | array of uuid | each known item: parent delta −1, queued for deletion (`:1123-1140`) | +| `_removed_items` | array of uuid | same handling as `_category_items_removed` (`:1124`) | +| `_broken_links_removed` | array of uuid | same handling (`:1142-1157`) | +| `_created_items` | array of uuid | the set of item/link ids the viewer will accept from `_embedded` on a mutation (`:1159`); also drives per-id callbacks for `CREATEINVENTORY` (`:995-1004`) | +| `_created_categories` | array of uuid | the set of category ids accepted from `_embedded` on a mutation (`:1162`); per-id callbacks for `CREATEINVENTORY` (`:984-993`) | +| `_updated_category_versions` | map uuid → integer | the authoritative folder versions after the operation (`:1165-1176`); see §1e | + +Keys the session brief named that this viewer does **not** read: `_updated_items`, `_updated_categories`, +`_removed_categories` — there is no reference to them in `llaisapi.cpp` (the removal key is +`_categories_removed`). Emitting them is harmless; relying on them is wrong. + +### Content keys (`parseContent`, `:1179-1214`) — top-level + +| Condition | Handling | +|---|---| +| `linked_id` **and** `parent_id` present | the response itself is a link: `parseLink` (`:1185-1188`) | +| else `item_id` **and** `parent_id` | the response is an item: `parseItem` (`:1189-1192`) | +| `FETCHCATEGORYSUBSET` | the top-level category is ignored (incomplete); `_embedded` parsed at `depth-1` (`:1194-1202`) | +| else `category_id` **and** `parent_id` | the response is a category: `parseCategory` (`:1203-1206`) | +| else | `_embedded` parsed if present (`:1207-1213`) | + +Callback ids (`InvokeAISCommandCoro`, `:953-1011`): fetch-category commands and `COPYLIBRARYCATEGORY` return +`category_id`; `FETCHITEM` returns `item_id`, overridden by `linked_id` if present ("Error message might contain an +item_id", `:972-980`); `CREATEINVENTORY` fires once per `_created_categories` / `_created_items` entry. + +### `_embedded` (`parseEmbedded`, `:1484-1508`) — a map with up to five keys + +| Key | LLSD type | Where it appears | Handling | +|---|---|---|---| +| `categories` | map: category uuid string → category map | inside a category | `parseEmbeddedCategories` (`:1586-1604`): each parsed at `depth` | +| `items` | map: item uuid string → item map | inside a category | `parseEmbeddedItems` (`:1554-1572`) | +| `links` | map: link item uuid string → link map | inside a category | `parseEmbeddedLinks` (`:1523-1540`): each `parseLink` at `depth` | +| `item` | single item map | inside a link | `parseEmbeddedItem` (`:1542-1552`) | +| `category` | single category map | inside a link | `parseEmbeddedCategory` (`:1574-1584`) | + +**Links are a separate collection, not items.** A folder's `_embedded` carries `items` and `links` as sibling maps; +a link's own `_embedded` may carry the linked `item` or `category`. On a mutation (non-fetch) response the viewer +ignores any embedded item/link/category whose id is not listed in `_created_items` / `_created_categories` +(`:1531-1534`, `:1547`, `:1562-1565`, `:1579`, `:1594-1597`); on a fetch it accepts everything. + +Descendent count (`parseDescendentCount`, `:1466-1482`): known only when `_embedded` has **all three** of +`categories`, `links`, `items` (sum of their sizes), or, on a fetch of a `FT_CURRENT_OUTFIT` / `FT_OUTFIT` folder, +when it has `links` alone (links-only folders). A folder returned without all three collections gets no +descendent count and therefore no version (see §1e) — the viewer will keep re-fetching it. + +## 1d. Item, link and category maps as the viewer reads them + +Verified in the permitted files: + +| Object | Key | Type | Required | Source | +|---|---|---|---|---| +| item | `item_id` | uuid | yes (selects `parseItem`) | `:1189`, `:1217` | +| item | `parent_id` | uuid | yes | `:1189`; a null parent puts the item in Lost And Found (`:1236`, `:1695-1706`) | +| link | `linked_id` | uuid | yes (selects `parseLink`) | `:1185` | +| link | `item_id`, `parent_id` | uuid | yes | `:1262`, `:1274` | +| link | (permissions, sale info) | — | ignored: the viewer overwrites them with defaults (`:1278-1283`, `:1303-1307`) | | +| category | `category_id` | uuid | yes | `:1203`, `:1328` | +| category | `parent_id` | uuid | yes | `:1203` | +| category | `version` | integer | optional; −1 = unknown | `:1332-1335`, `:1441-1445` | +| category | `agent_id` | uuid | optional; owner of a newly created category (`:1358-1366`) | | +| category | `_embedded` | map | optional | `:1379`, `:1460` | + +### A-Q1 resolved (A1): the field set `fromLLSD` reads + +`llaisapi.cpp` hands each object map to `LLViewerInventoryItem::unpackMessage(const LLSD&)` / +`LLViewerInventoryCategory::unpackMessage(const LLSD&)` (`:1223`, `:1268`, `:1368`). There is **no** +`unpackMessage(const LLSD&)` in `indra/llinventory/llinventory.cpp`; the LLSD readers there are +`LLInventoryItem::fromLLSD` (`:984-1183`) and `LLInventoryCategory::fromLLSD` (`:1289-1352`). That the viewer +subclasses' `unpackMessage(const LLSD&)` delegate to these is **UNVERIFIED** — `llviewerinventory.cpp` is not a +permitted read — but they are the only LLSD readers for these types in the permitted file, and the label +constants below are theirs. + +**Item** (`fromLLSD`, label constants at `:45-63`). Any key not listed is ignored by the loop: + +| Key | Type | Line | Notes | +|---|---|---|---| +| `item_id` | uuid | `:1004` | | +| `parent_id` | uuid | `:1010` | | +| `thumbnail` | map with `asset_id` | `:1016-1035` | or `thumbnail_id` (uuid) at `:1037` | +| `favorite` | map with `toggled` (bool) | `:1043-1051` | | +| `permissions` | map | `:1054` | inner keys read by `LLPermissions::importLLSD`, **UNVERIFIED** (`llpermissions.cpp` not permitted) | +| `sale_info` | map | `:1060` | inner keys read by `LLSaleInfo::fromLLSD`, **UNVERIFIED** | +| `shadow_id` | uuid | `:1087` | XOR-obfuscated asset id; an alternative to `asset_id` | +| `asset_id` | uuid | `:1094` | | +| `linked_id` | uuid | `:1100` | read **into the asset id**; its presence is also what selects `parseLink` (§1c) | +| `type` | string **or** integer | `:1106-1120` | asset type; `LLAssetType::lookup` for a string | +| `inv_type` | string **or** integer | `:1122-1135` | inventory type | +| `flags` | integer or binary | `:1137-1148` | | +| `name` | string | `:1150` | non-standard ASCII and `|` replaced with spaces | +| `desc` | string | `:1156` | | +| `created_at` | integer | `:1162` | | + +**Category** (`fromLLSD`, `:1289-1352`): + +| Key | Type | Line | Notes | +|---|---|---|---| +| `category_id` | uuid | `:1293` | the constant is `INV_FOLDER_ID_LABEL_WS` = `"category_id"` (`:67`) | +| `parent_id` | uuid | `:1297` | | +| `thumbnail` / `thumbnail_id` | map with `asset_id` / uuid | `:1303-1318` | | +| `favorite` | map with `toggled` | `:1321-1331` | | +| `type` | integer | `:1333-1338` | folder type | +| `type_default` | integer | `:1339-1344` | `INV_ASSET_TYPE_LABEL_WS` (`:66`); read after `type`, so it wins | +| `name` | string | `:1346` | | + +It reads neither `version` nor a descendent count — `llaisapi.cpp` reads those itself (§1e). Note `cat_id` +(`INV_FOLDER_ID_LABEL`, `:46`) is **not** read by `fromLLSD`; the category id key is `category_id`. + +**What the server emits (A1 decision).** Integers for `type`, `inv_type` and `sale_type`, since `fromLLSD` +accepts either and integers are what this tree already sends over FetchInventoryDescendents2 +(`Source/OpenSim.Capabilities/LLSDInventoryItem.cs:33-68`) and the LL viewer already accepts. The `permissions` +and `sale_info` inner key sets are taken from that same file for the same reason, their readers being +unverifiable this session. Golden fixtures under +`Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures` pin the result. + +### A-Q3 closed (A5): the categories-create map + +`LLInventoryCategory::asAISCreateCatLLSD` (`indra/llinventory/llinventory.cpp:1256-1276`) is what +`llinventorymodel.cpp:1040` puts in `new_inventory["categories"]`. It is a **base-class** method, which is why A4 +could not find it in `llviewerinventory.cpp` and left this UNVERIFIED. It emits, in order: + +| Key | Line | Value | +|---|---|---| +| `category_id` | `:1259` | `mUUID` — **null on a create**: the viewer constructs the category with `LLUUID::null` (`llinventorymodel.cpp:1038`), so the server assigns the id | +| `parent_id` | `:1260` | `mParentUUID` — the same folder the POST is addressed to | +| `type_default` | `:1261-1262` | `(S8)mPreferredType`, an **integer** folder type | +| `name` | `:1263` | | +| `thumbnail` | `:1265-1268` | `{ asset_id: mThumbnailUUID }`, **only when non-null** | +| `favorite` | `:1270-1273` | `{ toggled: mFavorite }`, **only when true** | + +Nothing else. This is the same key set `fromLLSD` reads (§1d) minus the read-only `type` alias, so A4's +inference was right in substance; the one refinement A5 made is that the server now honours the body's +`parent_id` when it names a folder, instead of always using the one in the URL. `thumbnail` and `favorite` are +accepted and dropped: this tree's `InventoryFolderBase` has no column for either. + +### A-Q3, partially resolved (A1): the link map the viewer builds + +`LLAppearanceMgr` builds a SlamFolder body as an **LLSD array** of link maps, each carrying exactly `name`, +`desc`, `linked_id` and `type` (`AT_LINK`, or `AT_LINK_FOLDER` for the base-outfit link) +(`indra/newview/llappearancemgr.cpp:2209-2245`). That is the shape A2 must accept on +`PUT /category/{id}/links`. The `UpdateItem` / `UpdateCategory` / `CreateInventory` bodies are still +**UNVERIFIED**: their callers are elsewhere in `llviewerinventory.cpp`. +## 1d-bis. The delta contract (A2): what the viewer applies from a mutation response + +Extracted from `AISUpdate::parseMeta` / `parseContent` / `parseItem` / `parseCategory` / `doUpdate` in +`llaisapi.cpp` and `LLInventoryModel::onObjectDeletedFromServer` in `llinventorymodel.cpp`. This is the table +the mutation routes implement. + +### The complete set of delta keys + +Read by `parseMeta` (`:1101-1177`) and **nothing else**. A0's list is confirmed against the source: the removal +key is `_categories_removed`, and `_updated_items`, `_updated_categories` and `_removed_categories` appear +nowhere in the file. + +| Key | LLSD | Line | What the viewer does | +|---|---|---|---| +| `_categories_removed` | array of uuid | `:1104-1119` | for each id **it already has**: parent descendent delta −1, id queued for deletion | +| `_category_items_removed` | array of uuid | `:1122-1139` | same, for items; merged into the same id set as the next row | +| `_removed_items` | array of uuid | `:1124` | parsed into the *same* list as `_category_items_removed` — the two are interchangeable | +| `_broken_links_removed` | array of uuid | `:1141-1156` | same handling again | +| `_created_items` | array of uuid | `:1159` | the ids the viewer will accept from `_embedded` on a mutation; drives per-id callbacks for CreateInventory | +| `_created_categories` | array of uuid | `:1162` | same for categories | +| `_updated_category_versions` | map uuid → integer | `:1164-1176` | the folder versions the viewer will adopt, **and the gate on all descendent accounting** | + +### Updated objects are content, not a delta key + +There is no "updated" delta key. An updated item or category arrives as **top-level content**: `parseContent` +(`:1179-1212`) routes a body with `item_id` + `parent_id` to `parseItem`, and one with `category_id` + +`parent_id` to `parseCategory`. On a **mutation** response (`!mFetch`): + +- `parseItem` (`:1215-1258`): if the viewer already has the item it copies its current values first + (`copyViewerItem`, `:1222` — *"Default to current values where not provided"*), applies the map, and files it + under `mItemsUpdated`, **plus a zero delta for the parent** (`:1241-1245`). If it does **not** have the item, + the same body is treated as a creation: `mItemsCreated` and parent delta **+1** (`:1247-1252`). +- `parseCategory` (`:1327-1465`): the same, filing under `mCategoriesUpdated` with zero deltas for **both** the + parent and the category itself (`:1419-1428`). + +Two consequences for the server. A PATCH response may be sparse — only the changed fields plus `item_id` / +`category_id` and `parent_id` — because the viewer merges onto its own copy. And it must be **top level**: on a +mutation the viewer ignores any `_embedded` object whose id is not in `_created_items` / `_created_categories` +(§1c), so an updated object hidden in `_embedded` is silently dropped. + +### `_updated_category_versions` gates everything + +`doUpdate` (`:1606-1648`) walks the accumulated descendent deltas and **skips any category not listed in** +`_updated_category_versions` — *"Skipping version increment for non-updated category"* (`:1625-1629`). A folder +whose contents changed but which the response does not list keeps a stale descendent count and version forever. +Newly created categories are skipped too, deliberately (`:1618-1622`). + +At the end of the update (`:1755-1791`) each listed category has its local version **set to the server's value** +(`:1776`, *"the AIS version should be considered the true version"*); a listed version of −1 +(`VERSION_UNKNOWN`) instead triggers a re-fetch with a 360 s expiry (`:1779-1789`). + +> **Hazard (Ledger A-R6).** That loop does `cat->getVersion()` with **no null check** on +> `gInventory.getCategory(id)` (`:1760-1762`). Listing a folder the viewer has never fetched is a null +> dereference in the viewer. Only list folders the operation touched. + +### Per operation: what to send + +| Operation | Content | Delta keys | `_updated_category_versions` must list | +|---|---|---|---| +| `PATCH /item/{id}` | the item, top level (`item_id`, `parent_id`, changed fields) | none | the item's parent folder — the zero-delta entry `parseItem` creates is discarded without it | +| `PATCH /category/{id}` | the category, top level (`category_id`, `parent_id`, changed fields) | none | the category **and** its parent — `parseCategory` creates zero-delta entries for both | +| `DELETE /item/{id}` | none | `_removed_items` (or `_category_items_removed`) with the item id | the item's parent | +| `DELETE /category/{id}` | none | `_categories_removed` with the folder id **only** | the folder's parent | + +**Descendents of a deleted folder are implied, not enumerated.** +`LLInventoryModel::onObjectDeletedFromServer` (`llinventorymodel.cpp:2015-2041`) calls +`onDescendentsPurgedFromServer` first for a category — *"For category, need to delete/update all children +first"* — so naming the folder is enough and its children are purged locally. Enumerating them as well would be +harmless but pointless; enumerating them **instead** of the folder would leave the folder behind. + +### Two edge rules + +- **A delta naming an object the viewer does not have is dropped**, with a warning: every removal arm is inside + `if (cat)` / `if (item)` (`:1109`, `:1130`, `:1148`), so there is no descendent delta and no deletion. Sending + a removal for something the viewer never knew is therefore safe, and silently does nothing. +- **An absent delta key and an empty one are identical.** `parseUUIDArray` (`:1077-1088`) does nothing when the + key is absent and nothing when the array is empty; `_updated_category_versions` is guarded by `update.has` + (`:1165`). Emitting empty arrays is neither required nor harmful. + +### A-Q3, resolved for updates (A2) + +`UpdateItem`'s body is the item's **full** `asLLSD()` with `asset_id` and `shadow_id` removed and replaced by +`hash_id` (the transaction id) when it is set — `LLViewerInventoryItem::updateServer` +(`llviewerinventory.cpp:435-454`) and `update_inventory_item` (`:1399-1422`), identically. So the server receives +the whole item map of §1d, minus the asset id, and must ignore what it does not accept rather than fail. + +`UpdateCategory`'s body is the category's full `asLLSD()` for a rename (`LLViewerInventoryCategory::updateServer`, +`:651-665`) and for a type change (`changeType`, `:866-884`). For a **protected** folder type the viewer refuses +to send anything but a single-key `{thumbnail}` or `{favorite}` map (`update_inventory_category`, `:1436-1457`), +so those two are the only fields a protected system folder will ever be asked to change. + +`CreateInventory`'s `items` / `links` arrays remain **UNVERIFIED** — their callers are outside the permitted +functions — and are A4's problem, not A2's. + +### 1d-ter. `UpdateItem`'s field set, and what the server does with each (A18) + +`LLInventoryItem::asLLSD` (`llinventory.cpp:936-981`) is the whole body, minus the erase in `updateServer`. The +server's rule for each key, with the reason: + +| Key | Source | Server | +|---|---|---| +| `asset_id` | `:952-955` (unrestricted perms, or a null asset) | **applied** | +| `shadow_id` | `:956-963` (restricted perms; the asset XORed with `MAGIC_ID`) | **never arrives** — both update-body builders erase it (`llviewerinventory.cpp:445-452`, `:1414-1421`) | +| `hash_id` | not from `asLLSD`; put in place of the two above when the transaction id is set | **applied**, by handing the transaction to the region's asset-transaction module — only it knows which asset the xfer produced (`Scene.Inventory.cs:579-582`) | +| `permissions` | `:939` `ll_fill_sd_from_permissions` (`llpermissions.cpp:1082-1094`) | `next_owner_mask`, `everyone_mask`, `group_mask` **applied**, each masked by the item's own base; `base_mask` / `owner_mask` and the id fields ignored (`Scene.Inventory.cs:497-548`) | +| `name`, `desc` | `:979-980` | **applied** | +| `flags` | `:976` | **applied** | +| `sale_info` | `:977` (`llsaleinfo.cpp:97-107`: `sale_type`, `sale_price`) | **applied** | +| `parent_id` | `:938` | **ignored** — a move changes two folders' versions and is not this route's job | +| `type`, `inv_type` | `:966-975` | **ignored** — invariants; `XInventoryService.UpdateItem` refuses to change them anyway (`:558-585`) | +| `created_at` | `:981` | **ignored** — not mutable through this route | +| `item_id` | `:937` | **ignored** — it is the URL | +| `thumbnail`, `favorite` | `:942-951` | **ignored** — no column in this tree | + +**Why it matters that anything is applied at all.** The data layer bumps the parent folder's version on every +item store (`MySQLXInventoryData.cs:238-246`). A PATCH that changes nothing writes nothing, so no version moves, +so the viewer never re-reads the item — which is exactly how A18's asset loss stayed invisible until a relog. + +## 1e-bis. GET /orphans scope (A1, recorded A2) + +`/orphans` reports **folder orphans only**: folders whose `ParentID` names a folder absent from the agent's +inventory skeleton. `IInventoryService` has no item-orphan query and finding orphaned items would mean listing +the contents of every folder (tree state T5), so items are never reported. **An empty response means "no orphan +folders", not "no orphans of any kind".** + +## 1c-bis. The depth contract (A2b), settled from the fetch path + +A1 implemented `depth=N` as "N counts generations expanded below the requested folder" and marked it UNVERIFIED, +expecting a live SL capture to settle it. It is settled from the source instead, and the implementation matches. + +### (a) Every URL the viewer builds with a depth + +Only two call sites in `llinventorymodelbackgroundfetch.cpp`, and both pass a literal `0` for the depth argument +while varying the *recursive* flag: + +| Call | Line | Arguments | +|---|---|---| +| `AISAPI::FetchCategorySubset(cat_id, children, item_type, true, cb, 0)` | `:937` | recursive **true**, depth 0 | +| `AISAPI::FetchCategoryChildren(cat_id, item_type, type == FT_RECURSIVE, cb, 0)` | `:994` | recursive from the queue entry, depth 0 | + +The depth that reaches the URL is computed in `llaisapi.cpp`: `depth = MAX_FOLDER_DEPTH_REQUEST` when recursive, +else `llmin(depth, MAX_FOLDER_DEPTH_REQUEST)` (`:463-474` for children, `:517-527` for the string-identifier form, +`:630-637` for the subset), with `MAX_FOLDER_DEPTH_REQUEST = 50` (`:58`). **So the only depths the viewer ever +sends are `depth=50` (recursive) and `depth=0` (not).** No other value is reachable from these call sites. + +### (b) What the viewer does with the response + +Two mechanisms, and the second is the one that matters: + +1. **It re-queues descendants regardless.** `onAISContentCalback` (`llinventorymodelbackgroundfetch.cpp:579-625`) + walks the direct descendant categories of every folder in the response and pushes each back on + `mFetchFolderQueue` as `FT_RECURSIVE` — the comment says why: *"push descendant back to verify they are fetched + fully (ex: didn't encounter depth limit)"* (`:610`). The viewer never assumes the server honoured the depth. +2. **"Fetched" means "has a version".** The queue drain skips a child category when + `VERSION_UNKNOWN != child_cat->getVersion()` (`:894-898`, again at `:948-953`); only unversioned children are + put in a subset request. + +And a folder only gets a version when the response let the viewer count its descendents: `parseCategory` sets it +inside `if (mCatDescendentsKnown.find(category_id) != end)` **and** `depth >= 0` — *"set version only if we are +sure this update has full data and embeded items since viewer uses version to decide if folder and content still +need fetching"* (`llaisapi.cpp:1380-1407`). `mCatDescendentsKnown` is filled only for a category whose +`_embedded` carries all three collections, or `links` alone for a Current Outfit / Outfit folder (`:1466-1482`). + +### (c) The rule the server must follow + +The depth in the parse decrements as it descends: `parseContent` parses the top-level category at `mFetchDepth` +(`:1205`), which is the `depth` the viewer kept locally, defaulting to 50 (`:1036-1040`); `parseCategory` then +parses its `_embedded` at `depth - 1` (`:1461-1464`). Combined with the `depth >= 0` gate above: + +> **The rule.** `depth=N` licenses the server to expand up to **N generations below the requested folder**. The +> requested folder is parsed at N, its children at N−1, and so on; a category that arrives deeper than N is parsed +> at a negative depth and **never gets a version**, so expanding further than N is wasted work that the viewer +> will re-fetch anyway. Expanding **fewer** generations than N is always safe: the unversioned folders are simply +> queued and fetched on the next round (b1), at the cost of a round trip. + +Two corollaries the implementation must respect, and does: + +- Every category the server **expands** must carry all three `_embedded` collections *and* a `version`, or the + viewer cannot count its descendents, will not version it, and will re-request it forever (risk A-R3). +- Every category the server **does not** expand must be a stub with no `_embedded`. Sending `version` on a stub is + harmless — the version gate is inside the descendents-known branch, so an unexpanded category is left + `VERSION_UNKNOWN` whatever version accompanies it — which is exactly the "come back for this one" signal. + +At `depth=0` this produces precisely the viewer's intent: `mFetchDepth = 0`, the requested folder is versioned, +its `_embedded` is parsed at −1 so no child is versioned, and every child comes back on the queue. + +### (d) Limits + +| Limit | Value | Source | +|---|---|---| +| Maximum depth requested | 50 | `MAX_FOLDER_DEPTH_REQUEST`, `llaisapi.cpp:58`; every depth is clamped to it | +| Children per subset request | `BatchSizeAIS3`, default **20**, clamped to [1, 40] | `llinventorymodelbackgroundfetch.cpp:883-885` | +| More children than the batch | parent re-queued as `FT_CONTENT_RECURSIVE` to collect the rest | `:909-913`, `:955-960` | +| URL length | warns above **2000** characters and **still sends** | `llaisapi.cpp:651-654` | +| Marketplace listings | never fetched by this path | `:900-904` | + +### Conformance + +`AisInventory.Walk(backend, agent, root, depth)` expands the requested folder plus `depth` further generations, +breadth first, and `AisHandler.FetchChildren` emits every walked folder with all three collections and every +unwalked child as a stub. That is the rule above, with no off-by-one: our deepest expanded generation is parsed +by the viewer at exactly `depth − depth = 0`, the last value that still versions. The **UNVERIFIED** marker is +removed. The handler additionally clamps a requested depth to 50, matching the viewer's own ceiling, so a client +asking for more cannot make the region walk further than the viewer would ever use. + +## 1d-ter. Protected folders (A3), from the viewer's own table + +`LLFolderType::lookupIsProtectedType` looks the type up in `LLFolderDictionary` and returns that entry's +PROTECTED flag, **returning `true` for any type the table does not contain** +(`indra/llinventory/llfoldertype.cpp:154-162`). The table is at `:85-127`. So the honest way to express it is an +allow-list of the unprotected types with a protected default — which is what the handler implements. + +**Unprotected** (PROTECTED = `false` in the table): + +| Viewer type | Line | This tree | +|---|---|---| +| `FT_NONE` | `:126` | `FolderType.None` (−1) — an ordinary user folder | +| `FT_ENSEMBLE_START`..`FT_ENSEMBLE_END` | `:106-109` | no member; handled as the numeric range 26–45. The viewer's own comment says *"Not used"* | +| `FT_OUTFIT` | `:112` | `FolderType.Outfit` — a saved outfit | +| `FT_MARKETPLACE_LISTINGS` | `:122` | `FolderType.MarketplaceListings` | +| `FT_MARKETPLACE_STOCK` | `:123` | `FolderType.MarkplaceStock` (the spelling is this tree's) | +| `FT_MARKETPLACE_VERSION` | `:124` | **no equivalent** — falls through to the protected default; see below | + +**Protected**: everything else in the table — `FT_TEXTURE`, `FT_SOUND`, `FT_CALLINGCARD`, `FT_LANDMARK`, +`FT_CLOTHING`, `FT_OBJECT`, `FT_NOTECARD`, `FT_ROOT_INVENTORY`, `FT_LSL_TEXT`, `FT_BODYPART`, `FT_TRASH`, +`FT_SNAPSHOT_CATEGORY`, `FT_LOST_AND_FOUND`, `FT_ANIMATION`, `FT_GESTURE`, `FT_FAVORITE`, `FT_CURRENT_OUTFIT`, +`FT_MY_OUTFITS`, `FT_MESH`, `FT_INBOX`, `FT_OUTBOX`, `FT_BASIC_ROOT`, `FT_SETTINGS`, `FT_MATERIAL` +(`:87-102`, `:111`, `:113-121`, `:125`) — **and every type the table omits**. + +**Mismatches between the two trees.** + +- `FT_MARKETPLACE_VERSION` (55) is unprotected in the viewer but has no `FolderType` member here, so it takes the + protected default. Nothing in OpenSim creates it; the practical effect is nil. +- `FolderType.Suitcase` (100) is this tree's, not the viewer's. The viewer's table has no entry, so + `lookupIsProtectedType` would return `true` for it — and so does the server rule. That is the right answer for + the HG suitcase folder independently. +- The ensemble range exists in the viewer only as a numeric span with no member here; the server matches it + numerically. + +**What changed from A2b's guess.** A2b protected "the root, or any system type except `FolderType.Outfit`". The +real table is **strictly more permissive**: it additionally leaves the marketplace types and the ensemble range +deletable. No folder that A2b allowed became protected, so the change cannot break anything that worked; it only +stops refusing four classes of folder the viewer never considered protected. The root remains refused, by type +(`FT_ROOT_INVENTORY`) and structurally (a folder with no parent). + +## 1e. Version semantics + +- Folder versions arrive in two places: `version` on a category map (fetch and mutation responses) and + `_updated_category_versions` (mutation responses). +- **Fetch:** `parseCategory` sets the local version from `version` only when the descendent count is known from + `_embedded` (§1c) and `depth >= 0` (`:1389-1407`); it refuses ("Got stale folder", `:1338-1348`) a category whose + `version` is lower than the version it already holds, and logs a stale-known-folder when the server's is higher + (`:1409-1416`, "Version was" `:1396`). A newly created category gets its version only with a known descendent count (`:1434-1447`). +- **Mutation:** descendent deltas (±1 per created/removed child, `:1112`, `:1131`, `:1149`, `:1250`, `:1310`, + `:1450`) are applied only to categories listed in `_updated_category_versions` (`doUpdate`, `:1606-1650`: + "Skipping version increment for non-updated category"). Afterwards each listed category's local version is + **set to the server's value** ("the AIS version should be considered the true version", `:1757-1795`, set at `:1776`); a listed + version of −1 instead triggers a re-fetch with a 360 s expiry (`:1771-1790`). +- **Consequence for the server:** every mutation must list, in `_updated_category_versions`, every folder whose + contents it changed (the parent of a created item/link/category; the old and new parent of a move; the parent + of a removed object; the slammed folder), with the post-operation version. A folder changed but not listed + leaves the viewer's descendent count and version stale. +- Which folders each operation is expected to bump (from the accounting rules above): CreateInventory → the parent; + SlamFolder → the slammed folder (and each removed link's parent, which is the same folder); RemoveItem / + RemoveCategory → the removed object's parent; PurgeDescendents → the purged folder; UpdateItem / UpdateCategory + → the parent when the update moves the object, otherwise the object's own folder is listed with delta 0 + (`:1245`, `:1298`, `:1427`); CopyLibraryCategory → the destination. Those are derived from the viewer's + accounting, not from an explicit table; the server rule in OpenSim is the data-layer increment recorded in + `S0a-VERIFICATION.md` V6. + +## 1f. HTTP status handling (`InvokeAISCommandCoro`, `:851-1011`) + +| Condition | Viewer behaviour | +|---|---| +| response body not an LLSD map | status forced to 500 "Malformed response contents" (`:882-885`); warn; the (non-map) result is still handed to `onUpdateReceived`, which finds nothing to do | +| 410 Gone, `REMOVECATEGORY` | warn; `fetchDescendentsOf(parent)`; the local folder is **not** deleted (`:886-903`) | +| 410 Gone, `REMOVEITEM` | warn; `fetchDescendentsOf(parent)`; local item deleted via `onObjectDeletedFromServer` (`:904-918`) | +| 403 Forbidden, `FETCHCATEGORYCHILDREN` with `depth == 0` | notification `InventoryLimitReachedAISAlert` (first time) / `InventoryLimitReachedAIS`; warn "content is over limit" (`:920-935`) | +| 403 Forbidden, `FETCHCATEGORYCHILDREN` with `depth > 0` | debug only: "recoverable by requesting with lower depth" (`:936-940`) — the caller is expected to retry with a smaller depth (retry logic outside the permitted files, **UNVERIFIED**) | +| any other failure (4xx/5xx, timeout) | warn with status and pretty-printed body (`:942-943`); no retry in `llaisapi.cpp` (transport-level retries in `llcorehttputil`, **UNVERIFIED**) | +| always, success or failure | `onUpdateReceived(result, type, body)` (`:946`): the body **is parsed as an update** even on error, so an error body must be a map and must not carry `item_id`/`category_id` + `parent_id` pairs it does not mean; then the completion callback fires at least once (`:953-1011`), with a null id unless the body carries the ids of §1c | + +What an error body should look like: an LLSD map. Nothing in the permitted files reads `error_code`, +`error_description` or `message`; they are conventional and safe because they are ignored. The library returns +them for logs. + +## 1g. The `isAvailable()` gate and its consequence + +`AISAPI::isAvailable()` (`:62-68`) is exactly `gAgent.getRegion()->isCapabilityAvailable("InventoryAPIv3")`: true +as soon as the current region's seed-cap response contains a URL for `InventoryAPIv3`. Nothing else is checked +(no version, no probe). The viewer requests that cap name on every seed (`:72-76`). + +Once true, the following go through AIS with **no fallback** (verified in the permitted functions): + +| Path | Behaviour when AIS available | Behaviour when not | Source | +|---|---|---|---| +| delete an item | `AISAPI::RemoveItem` | warns "Tried to use inventory without AIS API" and does **nothing** | `llviewerinventory.cpp:1497-1509` | +| delete a category | `AISAPI::RemoveCategory` (no `isAvailable` check at all) | request fails at the cap lookup, callback null | `:1545-1568` | +| purge a folder's descendents | `AISAPI::PurgeDescendents` | warns, does nothing | `:1630-1645` | +| slam a folder's links (outfit changes) | `AISAPI::SlamFolder` (no check) | fails at cap lookup | `:1776-1784` | +| create a category | `AISAPI::CreateInventory` | falls back to the legacy path below the `if` (`:1034`) | `llinventorymodel.cpp:1034-1042` | +| fetch a category's descendents | `AISAPI::FetchCategoryChildren` (seen at `llviewerinventory.cpp:694,727`, function not in the permitted list) | legacy cap | **UNVERIFIED** detail | +| background inventory fetch, item fetch, COF fetch, links, orphans, library copy | the remaining `AISAPI::Fetch*` / `CopyLibraryCategory` callers are in files not permitted this session | | **UNVERIFIED** | + +The consequence that matters: advertising `InventoryAPIv3` in the seed cap flips every path above at once. A +partial implementation returns errors for the paths it lacks, and for delete/purge/slam the LL viewer has no other +way to do them. Hence Ledger risk A-R1: partial AIS is worse than none. + +--- + +# Tree state (HEAD `db7c746248`, branch `feature/ais-v3`) + +| # | Finding | file:line | +|---|---|---| +| T1 | **Seed-cap request path.** `BunchOfCaps.SeedCapRequest` reads the requested cap names, adds every one to `validCaps` (the `switch` at `:340-374` only sets flags; `default: break;` then `validCaps.Add(cstr)` at `:375` — no whitelist), then `m_HostCapsObj.GetCapsDetailsLLSDxml(validCaps, sb)` at `:380`. `Caps.GetCapsDetailsLLSDxml` (`Source/OpenSim.Capabilities/Caps.cs:202-222`) delegates to `CapsHandlers.GetCapsDetailsLLSDxml` (`Source/OpenSim.Capabilities/CapsHandlers.cs`, method body: for each requested name, emit a URL only if a simple handler or a request handler is registered under that name), then poll handlers and external handlers, likewise only if requested. **For a new cap to reach the viewer it must be (1) registered on the agent's `Caps` under the exact name (`caps.RegisterSimpleHandler(name, ISimpleStreamHandler)` `Caps.cs:196` or `RegisterHandler` `:190`), at `OnRegisterCaps` time (`Scene/EventManager.cs:819`, fired per agent `:2119`), and (2) requested by the viewer in the seed body.** The viewer requests `InventoryAPIv3` and `LibraryAPIv3` (§1g), so (2) is satisfied; registration is the only region-side act, and the module does it per agent from `OnRegisterCaps`, as `FetchInventory2Module.RegionLoaded` → `RegisterCaps` does (`Source/OpenSim.Region.ClientStack.LindenCaps/FetchInventory2Module.cs`, `RegionLoaded` subscribes, `RegisterFetchCap` registers a `SimpleOSDMapHandler` at `"/" + UUID.Random()`). | `Source/OpenSim.Region.ClientStack.LindenCaps/BunchOfCaps/BunchOfCaps.cs:323-384`; `Source/OpenSim.Capabilities/Caps.cs:190-199, 202-222`; `Source/OpenSim.Capabilities/CapsHandlers.cs` (`GetCapsDetailsLLSDxml`) | +| T2 | **Current Outfit folder resolution:** `IInventoryService.GetFolderForType(userID, FolderType.CurrentOutfit)` (`Source/OpenSim.Services.Interfaces/IInventoryService.cs:68`); implemented by `XInventoryService.GetFolderForType` (`Source/OpenSim.Services.InventoryService/XInventoryService.cs:254`), which queries the user's folder of that type under the root; the folder is created at inventory creation (`:132-133`). Region-side call site that already does this: `AvatarFactoryModule.cs:1097` and `:1112`. The service is reached from a scene via `Scene.InventoryService` (a `LocalInventoryServicesConnector`, `RemoteXInventoryServicesConnector` or `HGInventoryBroker`, `Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/*.cs:41-43`). `FolderType.CurrentOutfit` = 46 (`SLUtil.cs:141,204` map it to `currentoutfitfolder`). | as listed | +| T3 | **CreateInventoryCategory cap:** registered at `BunchOfCaps.cs:264-265`, handler `:1138`; creates `new InventoryFolderBase(folderID, folderName, m_AgentID, (short)folderType, parentID, 1)` at `:1200` and calls `m_Scene.InventoryService.AddFolder(folder)` at `:1201`. Parent version bump confirmed by S0a V6: `XInventoryService.AddFolder` (`:369-407`) → `m_Database.StoreFolder` → `MySqlFolderHandler.Store` (`Source/OpenSim.Data.MySQL/MySQLXInventoryData.cs:283-288`) → `IncrementFolderVersion(folder.parentFolderID)`. | `BunchOfCaps.cs:264-265, 1138, 1200-1201`; `MySQLXInventoryData.cs:283-288` | +| T4 | **Where folder Version is read back:** `XInventoryService.GetFolder(principalID, folderID)` (`XInventoryService.cs:630-640`) → `ConvertToOpenSim(XInventoryFolder)` (`:677`: `Version = (ushort)folder.version`); `GetFolderContent` (`:296`) also fills `InventoryCollection.Version` (`:333`) and `InventoryCollection` carries `Version` and `Descendents` (`Source/OpenSim.Framework/InventoryCollection.cs:41-42`). The value is the DB column, so it is fresh on every call; nothing caches it region-side. | as listed | +| T5 | **Link-aware fetch:** `IInventoryService` has none. Its surface (`IInventoryService.cs:46-200`): `GetFolderContent`, `GetMultipleFoldersContent`, `GetFolderItems`, `GetItem`, `GetMultipleItems`, `GetFolder`, plus the mutators. `InventoryCollection` is `Folders` + `Items` (links are items with `AssetType.Link`). The existing descendents cap resolves link targets itself: `FetchInvDescHandler.ProcessLinks` (`Source/OpenSim.Capabilities.Handlers/FetchInventory/FetchInvDescHandler.cs:424-460`) collects `AssetType.Link` items and calls `GetMultipleItems` for the targets. **The AIS module must do the same** (one `GetFolderContent` + one `GetMultipleItems` per folder), and must split links out of `Items` into the `_embedded.links` collection itself. | as listed | +| T6 | **Existing inventory caps:** `FetchInventory2` / `FetchLib2` (`Source/OpenSim.Region.ClientStack.LindenCaps/FetchInventory2Module.cs`, `ISharedRegionModule`, config `[ClientStack.LindenCaps] Cap_FetchInventory2 = localhost`, registers a `SimpleOSDMapHandler("POST", "/" + UUID.Random(), ...)` per agent from `OnRegisterCaps`), `FetchInventoryDescendents2` / `WebFetchInventoryDescendents` (`WebFetchInvDescModule.cs`, a poll-service handler), `FetchLibDescModule.cs`; the request logic lives in `Source/OpenSim.Capabilities.Handlers/FetchInventory/{FetchInventory2Handler,FetchLib2Handler,FetchInvDescHandler}.cs` (`:41`, `:41`, `:42`) which are **plain classes with no shared base**: each is constructed with `(IInventoryService, agentID)` and exposes a request method. Modules are discovered through `PluginRegistration.RegisterPlugins` (`Source/OpenSim.Region.ClientStack.LindenCaps/PluginRegistration.cs:34-54`), not by reflection: a new module must be added there. There is no base class to reuse; the AIS module follows the same shape (module registers, handler class holds the logic) but puts the logic behind `IAisInventoryBackend` so Phase 2 can host it on Robust. | as listed | + +Home for the module: `Source/OpenSim.Region.ClientStack.LindenCaps/AIS/` (the project is named `LindenCaps`, not +`Linden.Caps`; the brief's path does not exist). It sits beside `FetchInventory2Module.cs` because that is where +every region-side inventory cap lives (T6), the project already references `OpenSim.Services.Interfaces` and the +HTTP server, and `PluginRegistration.cs` is the discovery point. Tests go to a new project, +`Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/` (NUnit 4, in the solution): the existing +`Tests/OpenSim.Region.ClientStack.LindenCaps.Tests` is not in `Tranquillity.sln` and does not compile at HEAD +(`EventQueue/Tests/EventQueueTests.cs`, syntax errors; its csproj also pins package versions below +`OpenSim.Tests.Common`), so it was left untouched. diff --git a/Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md b/Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md new file mode 100644 index 00000000000..90b789ae58f --- /dev/null +++ b/Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md @@ -0,0 +1,272 @@ +# AIS-AUDIT-1 — the malformed-LLSD blind spot, tree-wide + +**Date:** 2026-09-12 · **Tree:** `feature/ais-v3` at `73c8933253` · **Read-only sweep; no code changed.** + +**Bottom line: the blind spot is real, it is worse than A21 recorded, and no site in this tree is currently +exploitable through it.** 86 parse sites examined, 40 of them client-facing. **Zero DESTRUCTIVE.** One latent +defect that is one edit away from destructive is recorded in §5, and one correction to A21/A23's own wording +in §6. + +--- + +## 1. The parser's behaviour, measured + +**A21 said "LibreMetaverse". That is wrong** — see §6. The OSD types in this tree come from +**`UtopiaSkye.OpenMetaverse.StructuredData` 1.1.7** (`Directory.Build.props:13-15`). Everything below was +measured against that package, in a throwaway console probe outside the repo, not taken from A21. + +### 1a. `DeserializeLLSDXml` — A21 confirmed, and broader than recorded + +| input | result | +|---|---| +| `` (truncated) | `OSD` **Type=Unknown** | +| `` (well-formed, not LLSD) | `OSD` **Type=Unknown** | +| `""` (empty) | `OSD` **Type=Unknown** | +| `not xml at all` | `OSD` **Type=Unknown** | +| `name` (truncated map) | `OSD` **Type=Unknown** | +| `` (valid but empty document) | `OSD` **Type=Unknown** | +| `` | `OSDMap` Type=Map Count=0 | +| `` | `OSDArray` Type=Array Count=0 | + +Never throws, never returns null, for any malformed input. A *valid* empty map or array is correctly typed, so +**`Type == Unknown` is the only discriminator** — which is exactly what `AisHandler.ReadBody` now tests. + +Note the last-but-two row: a bare `` is syntactically fine and still yields Unknown, so "Unknown" +conflates *malformed* with *empty document*. Both should be refused on a mutating route, so that conflation is +harmless here — but it is worth knowing before anyone tries to distinguish them. + +### 1b. The siblings differ, and that is the part A21 did not cover + +**This is not one blind spot. It is four different ones.** + +| entry | truncated | not-LLSD | empty string | garbage text | +|---|---|---|---|---| +| `DeserializeLLSDXml` | **Unknown** | **Unknown** | **Unknown** | **Unknown** | +| `Deserialize` (auto-detect) | **Unknown** | **Unknown** | **Unknown** | throws `JsonException` | +| `DeserializeLLSDNotation` | **Unknown** | throws `OSDException` | **Unknown** | throws `OSDException` | +| `DeserializeJson` | throws `JsonException` | throws `JsonException` | **Unknown** | throws `JsonException` | +| `DeserializeLLSDBinary` | throws `OSDException` | throws `OSDException` | throws `OSDException` | throws `OSDException` | + +Consequences worth stating plainly: + +- **`Deserialize` (auto) is as dangerous as XML** and is used on 17 sites. A try/catch around it catches only + the garbage-text case. +- **`DeserializeLLSDNotation` and `DeserializeJson` are partially covered** by a try/catch — each still returns + Unknown for at least one input, and for both that input includes the **empty string**, i.e. *no body at all*. +- **`DeserializeLLSDBinary` is the only entry a try/catch fully covers.** +- For every text entry, **an absent body is indistinguishable from a malformed one**. That is the specific + conflation that made AIS-SEC-2 destructive. + +### 1c. What a degenerate OSD does downstream + +| expression | result | +|---|---| +| `(OSDMap)degenerate` — hard cast | **throws `InvalidCastException`** | +| `(OSDArray)degenerate` — hard cast | **throws `InvalidCastException`** | +| `degenerate as OSDMap` — soft cast | **null** | +| `degenerate.AsString()` | `""` | +| `degenerate.AsInteger()` | `0` | +| `degenerate.AsBoolean()` | `false` | +| `degenerate.AsUUID()` | `00000000-0000-0000-0000-000000000000` | +| `degenerate.AsReal()` | `0` | + +And, for comparison, `new OSDMap()["missing"].Type` is **also `Unknown`** — the same sentinel is used for "key +not present". That is why the type is easy to overlook: it is the universal "nothing here" value, not an error +marker. + +**This is the structural finding that makes the whole sweep tractable.** A hard cast *fails loudly*. A soft cast +yields null, which fails loudly on first use. Only **holding a bare `OSD` and calling accessors** proceeds +silently, with empty strings and zero UUIDs — and only that path can write wrong data without anyone noticing. + +--- + +## 2. The anti-pattern that actually caused AIS-SEC-2 — and it exists nowhere else + +The destructive ingredient in AIS was not the bare parse. It was +**`catch { return new OSDMap(); }`**: a failure converted into a *plausible, empty, correctly-typed* value, +which then passed every downstream check and was read as "the client asked for nothing". + +**Searched the whole of `Source/` and `Addons/` for that shape — a catch block substituting a default +`OSD`/`OSDMap`/`OSDArray`. Zero occurrences.** The only match in the tree is the doc comment in +`AisHandler.cs:419` describing the code that was removed. + +That is the single most reassuring result of this audit, and it is why there are no DESTRUCTIVE findings. + +--- + +## 3. Inventory + +86 non-comment `OSDParser.Deserialize*` call sites in `Source/` + `Addons/`, by entry point: + +| entry | sites | +|---|---| +| `DeserializeLLSDXml` | 40 | +| `DeserializeJson` | 18 | +| `Deserialize` (auto) | 17 | +| `DeserializeLLSDBinary` | 9 | +| `DeserializeLLSDNotation` | 3 | + +Split by trust boundary: + +- **40 client-facing** — parse an HTTP request stream, a response body from an external service, or a body + string. Analysed in §4. +- **46 out of scope** — parse data the server itself wrote: assets (`asset.Data`), config, the round-trip in + `SimulatorFeaturesModule.cs:267`, mesh/material blobs already validated on upload, `DAMap`, and + `PrimitiveBaseShape`. Flagged, not analysed, per the brief. + +**One nuance in that split, stated rather than hidden.** Several of the 46 parse **script-supplied** strings — +`OSSL_Api` (7 sites), `Phlox.ScriptEngine/LSLSystemAPI` (3), `JsonStore` (2). That is untrusted input from a +resident's script, a different trust boundary from an HTTP body but not a safe one. I checked the +fork-authored ones anyway because they use the mixed-blind-spot entries, and they are clean: + +- `LSLSystemAPI.cs:13996` — `as OSDMap` followed by `parsed != null`. SAFE. +- `LSLSystemAPI.cs:16206` — `if (osd is OSDMap map)`. SAFE. +- `JsonStore.cs:131` — guards `string.IsNullOrEmpty(value)` *before* parsing, which pre-empts the one case + `DeserializeJson` returns Unknown for. SAFE, and apparently deliberate. +- `JsonStore.cs:268` — try/catch, with a comment acknowledging the parser may crash on bad input. Covered, + since Json throws for everything except the empty string handled above. + +A full sweep of the OSSL script surface is **not** in this session's scope and is recommended as its own audit. + +--- + +## 4. Classification of the 40 client-facing sites + +Definitions used. The brief's DESTRUCTIVE definition was truncated in transmission; it is taken here as the +complement of HARMLESS — *unchecked, and a degenerate OSD reaches a write, so data can be lost or altered*. + +| class | count | meaning | +|---|---|---| +| **SAFE** | 15 | type-checked (`OSDType`, `is OSDMap`, or soft cast + null check) before use | +| **HARMLESS** | 25 | unchecked, but a degenerate OSD throws before any write, and the route answers an error | +| **DESTRUCTIVE** | **0** | — | + +### 4a. SAFE — the good pattern, already widespread + +| site | guard | +|---|---| +| `BaseHttpServer.cs:1499` | `if (llsdRequest is not OSDMap) return;` — **the framework's default LLSD dispatcher**, so this covers every handler behind it | +| `WebStatsModule.cs:473` | `if (message.Type != OSDType.Map) return ...`, and again on the nested `agent` map — the exact pattern AIS adopted, pre-existing | +| `EnvironmentModule.cs:581` | `if (req is OSDMap map)` | +| `FreeSwitchVoiceModule.cs:320` | `if (tmp is OSDMap map)` | +| `VivoxVoiceModule.cs:457` | `if (tmp is OSDMap map)` | +| `WebRtcVoiceRegionModule.cs:896` | `if (tmp is OSDMap map) return map;` else falls through to a logged failure | +| `AisHandler.cs:452-454` | `parsed is null \|\| parsed.Type == OSDType.Unknown` — AIS-SEC-2's own fix | +| `WebUtil.cs:501,598` · `Util.cs:2710,2726` | `responseOSD.Type == OSDType.Map` | +| `JanusAdminClient.cs:162` · `JanusPeerCtlBatchSink.cs:368` · `JanusMessages.cs:217` | `is OSDMap` pattern / soft cast + null check | + +### 4b. HARMLESS — 25 hard-cast sites + +All of the form `(OSDMap)OSDParser.DeserializeLLSDXml(httpRequest.InputStream)` or +`(OSDArray)OSDParser.Deserialize(...)`. On a degenerate OSD the cast throws `InvalidCastException` (§1c), +**before** the handler reaches any backend call. Either a local `catch` answers an error, or the exception +reaches `BaseHttpServer`, which answers 500. **No write occurs with degenerate data in either case**, and — +critically — none of them catches the exception and then continues with a substituted default (§2). + +Includes the write-capable routes, which are the ones that would matter if this were wrong: +`BunchOfCaps.cs:326,701,1163,1935,2067` (upload/caps), `MoapModule.cs:239,466` (media-on-a-prim), +`MaterialsModule.cs:478,558,883` (materials), `LandManagementModule.cs:2049` (parcel), +`GodsModule.cs:117`, `AgentPreferencesModule.cs:121`, `DisplayNameModule.cs:145`, +`ServerSideBakingModule.cs:325`, `ExperienceModule.cs:244`, `EstateChangeInfo.cs:155`, +`FetchInvDescHandler.cs:73` and `FetchLibDescHandler.cs:74` (read-only), `SimpleOSDMapHandler.cs:99`, +`FetchInventory2Handler.cs:58`, `Utils.cs:105` / `Simulation/Utils.cs:89`, `LLLoginHandlers.cs:236`, +`BaseHttpServer.cs:1429`, `GroupsModule.cs:464,514`. + +**"Harmless" here means no data is harmed, not that the behaviour is ideal.** A malformed body on these routes +produces an `InvalidCastException` in the log and a bare 500, where AIS now produces a Warning naming the route +and byte count plus a 400. That is a quality gap, not a security one, and it is a large mechanical change +across 25 sites — deliberately **not** proposed here. + +--- + +## 5. The one latent defect: `ViewerEnvironment.FromWLOSD` checks the wrong variable + +`Source/OpenSim.Framework/ViewerEnvironment.cs:91-101`: + +```csharp +public void FromWLOSD(OSD osd) +{ + OSDArray array = osd as OSDArray; // a degenerate OSD -> null + if (osd != null) // <-- checks osd, NOT array + { + Cycle = new DayCycle(); + Cycle.FromWLOSD(array); // passes null + } + InvalidateCaches(); +} +``` + +A degenerate `OSD` is **non-null**, so the guard passes and `null` is handed to +`DayCycle.FromWLOSD(OSDArray array)`. + +Reached from `EnvironmentModule.SetEnvironmentSettings` (`:757`), the legacy WindLight setter, which parses with +the auto-detect entry — one of the four with the blind spot — and holds the result as a bare `OSD` with **no +type check**: + +```csharp +ViewerEnvironment VEnv = new(); +OSD env = OSDParser.Deserialize(request.InputStream); +VEnv.FromWLOSD(env); +StoreOnRegion(VEnv); // <-- the write +``` + +**Why it is HARMLESS today:** `DayCycle.FromWLOSD` (`ViewerDaycycle.cs:65`) dereferences `array.Count` at +`:71`, before it reads anything else, so it throws `NullReferenceException` **before** `StoreOnRegion(VEnv)` is +reached. The +surrounding `catch` logs an error and answers `success: false` with a `fail_reason`. Nothing is written. + +**Why it is worth recording anyway:** it is harmless *by accident of evaluation order*, not by design. Make +`DayCycle.FromWLOSD` null-tolerant — a reasonable-looking hardening change — and `Cycle` becomes an empty +`DayCycle`, `StoreOnRegion` is reached, and **the region's environment is overwritten with a blank one** by a +truncated request. That is the AIS-SEC-2 failure mode exactly: a malformed body read as "the client asked for +nothing". + +Mitigating factors: the route is gated by `CanIssueEstateCommand`, so it needs an estate manager; and it is the +legacy WL path, superseded by the checked `:581` handler for modern viewers. **Not fixed in this session** — +read-only — but it is the one place where the blind spot is still load-bearing. + +--- + +## 6. Correction to A21 and A23: the package is not LibreMetaverse + +Ledger row **A21** says *"the shipped parser"* and the AIS-SEC-2 commit body says the same; row **A23** says +*"Measured on this tree's `UUID`"* and both the row and the handoff attribute it to LibreMetaverse. **The OSD +and UUID types in this tree come from `UtopiaSkye.OpenMetaverse.*` 1.1.7**, referenced in +`Directory.Build.props:13-17`. LibreMetaverse 3.1.4 *is* in the NuGet cache, but for a different project in +this workspace, not this tree. + +**The measurements in A21 and A23 stand** — both were taken through the tree's own test project, which +references the real package — so only the attribution was wrong. Corrected here rather than silently, because +anyone chasing the `UUID.GetHashCode` low-byte finding of A23 would otherwise go and read the wrong source. + +--- + +## 7. Recommendations, in priority order + +1. **`ViewerEnvironment.FromWLOSD` (§5): change `if (osd != null)` to `if (array != null)`.** One line, removes + the only load-bearing instance of the blind spot, and removes the trap that a future null-tolerance edit + would spring. Highest value per unit of risk in this document. +2. **Record the §1b table where the next author will find it.** The "four different blind spots" result is the + part most likely to be re-derived from scratch; `Deserialize` (auto) being as unsafe as XML is the + counter-intuitive half. +3. **Audit the OSSL / script-supplied parse surface** (§3) as its own session. Different trust boundary, 12+ + sites, out of scope here. +4. **Do not mass-convert the 25 hard-cast sites.** They are safe against data loss. Converting them to typed + checks with proper 400s is a quality improvement worth doing route-by-route when each is touched for another + reason, not as a sweep — a 25-site mechanical change to the cap surface carries more risk than the logging + it would improve. + +--- + +## 8. Method, so this can be repeated or disputed + +- Parser behaviour: a throwaway .NET 10 console project in the session scratchpad referencing + `UtopiaSkye.OpenMetaverse.StructuredData` / `.Types` 1.1.7, exercising all five entries against six malformed + and two valid inputs, plus the cast and accessor behaviour of §1c. **The repository was not modified at any + point in this session**, and `git status` was clean before and after. +- Inventory: `grep -rn 'OSDParser\.Deserialize' Source/ Addons/ --include=*.cs`, excluding `bin/`, `obj/`, + comment lines and tests. +- Trust split: sites reading `InputStream` / a request or response body treated as client-facing; sites reading + `asset.Data`, config, or data this server serialized treated as out of scope. +- Anti-pattern search: catch blocks substituting a default `OSD`/`OSDMap`/`OSDArray`. +- Every classification in §4 was read at the call site; none was inferred from the grep line alone. diff --git a/Source/OpenSim.Capabilities/Caps.cs b/Source/OpenSim.Capabilities/Caps.cs index 26c04ff883f..d8c1c77c961 100644 --- a/Source/OpenSim.Capabilities/Caps.cs +++ b/Source/OpenSim.Capabilities/Caps.cs @@ -193,10 +193,14 @@ public void RegisterHandler(string capName, IRequestHandler handler) m_capsHandlers[capName] = handler; } - public void RegisterSimpleHandler(string capName, ISimpleStreamHandler handler, bool addToListener = true) + /// + /// True when the capability serves paths below its own URL rather than only the URL itself. Every existing + /// cap answers on its exact URL and leaves this false; AIS v3 is the first that does not. + /// + public void RegisterSimpleHandler(string capName, ISimpleStreamHandler handler, bool addToListener = true, bool varPath = false) { //m_log.LogDebug("[CAPS]: Registering handler for \"{0}\": path {1}", capName, handler.Path); - m_capsHandlers.AddSimpleHandler(capName, handler, addToListener); + m_capsHandlers.AddSimpleHandler(capName, handler, addToListener, varPath); } public void RegisterPollHandler(string capName, PollServiceEventArgs pollServiceHandler) diff --git a/Source/OpenSim.Capabilities/CapsHandlers.cs b/Source/OpenSim.Capabilities/CapsHandlers.cs index 05edee0bd94..2a65df6bde3 100644 --- a/Source/OpenSim.Capabilities/CapsHandlers.cs +++ b/Source/OpenSim.Capabilities/CapsHandlers.cs @@ -91,12 +91,18 @@ public void Remove(string capsName) } } - public void AddSimpleHandler(string capName, ISimpleStreamHandler handler, bool addToListener = true) + /// + /// True when the capability serves paths *below* its own, as AIS v3 does + /// (<capurl>/category/{id}/children). The listener keeps those in a separate dictionary and matches + /// the segment before the second slash; the default dictionary is matched exactly, so a sub-path request + /// against it 404s before the handler is entered (BaseHttpServer.TryGetSimpleStreamHandler). + /// + public void AddSimpleHandler(string capName, ISimpleStreamHandler handler, bool addToListener = true, bool varPath = false) { if(ContainsCap(capName)) Remove(capName); if(m_capsSimpleHandlers.TryAdd(capName, handler) && addToListener) - m_httpListener.AddSimpleStreamHandler(handler); + m_httpListener.AddSimpleStreamHandler(handler, varPath); } public bool ContainsCap(string cap) diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AISv3Module.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AISv3Module.cs new file mode 100644 index 00000000000..65292cc2e58 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AISv3Module.cs @@ -0,0 +1,579 @@ +using System; +using System.Collections.Generic; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Services.Interfaces; +using Microsoft.Extensions.Logging; +using Caps = OpenSim.Framework.Capabilities.Caps; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// +/// Region-side host for the AIS v3 inventory cap (Ledger A-D1). Config: +/// +/// [AIS] +/// Enabled = false +/// +/// When enabled it registers InventoryAPIv3 for every agent from OnRegisterCaps (tree state T1: the +/// cap must be registered on the agent's Caps under that exact name; the viewer requests the name itself). When +/// disabled it registers nothing, so the viewer never sees the cap and keeps its legacy paths (risk A-R1). +/// LibraryAPIv3 is deliberately not registered (Ledger A-D3). +/// +public class AISv3Module : ISharedRegionModule +{ + private static readonly ILogger m_log = LoggerProvider.CreateLogger(typeof(AISv3Module)); + + public const string CapName = "InventoryAPIv3"; + /// The library cap. Same handler, library owner as the agent, mutations refused (John's Phase 1 ruling; supersedes A-D3). + public const string LibraryCapName = "LibraryAPIv3"; + public const string ConfigSection = "AIS"; + + /// + /// AIS caps must be registered as **variable-path** handlers. Every other capability in this tree answers on + /// its exact URL; AIS answers on paths below it — <capurl>/item/{id}, + /// <capurl>/category/{id}/children, <capurl>/orphans. The listener keeps exact and + /// variable-path handlers in different dictionaries and only the latter is matched by prefix + /// (BaseHttpServer.TryGetSimpleStreamHandler, AddSimpleStreamHandler), so registering the + /// default way makes every AIS request 404 before the handler is entered. That is the A6 live failure; + /// see Docs/feature/ais-v3/A6-LIVE-FAILURE.md. + /// + public const bool VarPath = true; + + /// The grid-wide default from [AIS] Enabled. A region may override it; see . + public bool Enabled { get; private set; } + + /// The scenes that resolved to enabled, with the handler they subscribed, so RemoveRegion can undo exactly what RegionLoaded did. + private readonly Dictionary m_enabledScenes = new(); + + public string Name => "AISv3Module"; + public Type ReplaceableInterface => null; + + public void Initialise(IConfigSource source) + { + IConfig config = source.Configs[ConfigSection]; + Enabled = config is not null && config.GetBoolean("Enabled", false); + if (Enabled) + m_log.LogInformation( + "[AIS]: [{Section}] Enabled = true: AIS v3 is on for every region this simulator runs. It routes ALL " + + "of an LL viewer's inventory traffic - fetch, delete, purge, slam and create - and the viewer has " + + "no fallback for the mutations (spec 1g, risk A-R1). A single region opts out with " + + "AIS_Enabled = false in its own section.", ConfigSection); + } + + public void PostInitialise() { } + public void Close() { } + + public void AddRegion(Scene scene) { } + + /// + /// Whether AIS is on for one region. The grid-wide [AIS] Enabled is the default, and a + /// [<Region Name>] section may override it with AIS_Enabled — the per-region idiom this tree + /// already uses (AutoBackupModule.cs:400-406 reads scene.Config.Configs[regionName] and takes + /// per-key defaults from the global setting). + /// + /// This exists because risk A-R1 makes a grid-wide flip unacceptable: turning AIS on hands the LL + /// viewer's entire inventory path to this code with no fallback, so it must be possible to try it on exactly + /// one region. Static and free of Scene so it can be tested with a plain config source. + /// + public static bool ResolveEnabled(bool gridDefault, IConfigSource sceneConfig, string regionName) + { + if (sceneConfig is null || string.IsNullOrEmpty(regionName)) return gridDefault; + IConfig regionConfig = sceneConfig.Configs[regionName]; + return regionConfig is null ? gridDefault : regionConfig.GetBoolean("AIS_Enabled", gridDefault); + } + + /// + /// S12: which config decided this region's flag - "region section" when the region's own section carries + /// an AIS_Enabled key, "global" otherwise. For the startup line only; see + /// ServerSideBakingRegion.EnabledSource, which answers the same question for the other lane in the same + /// words, because the flip verify reads both. + /// + public static string EnabledSource(IConfigSource sceneConfig, string regionName) + { + if (sceneConfig is null || string.IsNullOrEmpty(regionName)) return GlobalSource; + IConfig regionConfig = sceneConfig.Configs[regionName]; + return regionConfig is not null && regionConfig.Contains("AIS_Enabled") ? RegionSource : GlobalSource; + } + + /// The two answers gives. + public const string GlobalSource = "global"; + public const string RegionSource = "region section"; + + public void RegionLoaded(Scene scene) + { + if (scene is null) return; + + var regionName = scene.RegionInfo?.RegionName; + var enabled = ResolveEnabled(Enabled, scene.Config, regionName); + + // S12: one line per region, naming which config decided - the same shape the SSB lane logs, so the flip + // verify reads one console for both. + m_log.LogInformation("[AIS]: region {Region}: AIS v3 {State} ({Source})", + scene.Name, enabled ? "ON" : "off", EnabledSource(scene.Config, regionName)); + + if (!enabled) return; + + if (scene.InventoryService is null) + { + m_log.LogError("[AIS]: region {Region} has no inventory service; no AIS caps registered there", scene.Name); + return; + } + + void Handler(UUID agentID, Caps caps) => RegisterCaps(scene, agentID, caps); + lock (m_enabledScenes) + { + if (m_enabledScenes.ContainsKey(scene)) return; + m_enabledScenes[scene] = Handler; + } + scene.EventManager.OnRegisterCaps += Handler; + + var caps = scene.LibraryService is null + ? CapName + : CapName + ", " + LibraryCapName; + m_log.LogInformation( + "[AIS]: region {Region} advertises {Caps} to every agent. All LL-viewer inventory traffic there - fetch, " + + "delete, purge, slam and create - goes through AIS with no fallback (spec 1g).", scene.Name, caps); + } + + public void RemoveRegion(Scene scene) + { + if (scene is null) return; + EventManager.RegisterCapsEvent handler; + lock (m_enabledScenes) + { + if (!m_enabledScenes.Remove(scene, out handler)) return; + } + scene.EventManager.OnRegisterCaps -= handler; + } + /// + /// Both caps are registered together when enabled, and neither when disabled (tree state T1: a cap reaches + /// the viewer only if it is registered under its exact name and the viewer asked for it; the viewer asks for + /// both, `llaisapi.cpp:72-76`). LibraryAPIv3 runs the same handler over the library service with the library + /// owner as its agent id, and refuses every mutation with 405. + /// + private void RegisterCaps(Scene scene, UUID agentID, Caps caps) + { + var inventory = scene.InventoryService; + var library = scene.LibraryService; + + // AIS-SEC-1: the backend is bound to the agent this cap belongs to. The cap URL is unguessable, but a cap + // that leaked (or a client of the agent's own) could otherwise name any resident's object UUID and the + // inventory service would serve it, because it resolves by id and ignores the principal. + var invHandler = new AisHandler("/" + UUID.Random(), agentID, new InventoryServiceBackend(inventory, agentID, TransactionResolverFor(scene), WornAssetObserverFor(scene))); + caps.RegisterSimpleHandler(CapName, invHandler, varPath: VarPath); + m_log.LogDebug("[AIS]: registered {Cap} at {Path} for agent {Agent} in {Region}", + CapName, invHandler.CapPath, agentID, scene.Name); + + if (library is null) + { + m_log.LogWarning("[AIS]: region {Region} has no library service; {Cap} not registered", scene.Name, LibraryCapName); + return; + } + var libraryOwner = LibraryOwnerOf(library); + // COPY reads from the library and writes into the agent's inventory, so the library handler carries both + // sides: itself as the source, the agent's inventory as the destination. AIS-SEC-1 binds that destination + // to the agent too, so a COPY cannot be steered into another resident's folder by its Destination header. + // The source backend is the library's own and stays as it is: it is read-only by construction. + var libHandler = new AisHandler("/" + UUID.Random(), libraryOwner, new LibraryServiceBackend(library), AisMode.Library, + new InventoryServiceBackend(inventory, agentID, TransactionResolverFor(scene), WornAssetObserverFor(scene)), agentID); + caps.RegisterSimpleHandler(LibraryCapName, libHandler, varPath: VarPath); + m_log.LogDebug("[AIS]: registered {Cap} at {Path} for agent {Agent} in {Region}", + LibraryCapName, libHandler.CapPath, agentID, scene.Name); + } + + /// + /// Hands a hash_id to the region's asset-transaction module, which is the only thing that knows which + /// asset a transaction produced (A16). Kept here rather than in the backend so + /// stays free of Scene (Ledger P-2) and Phase 2 can host it on + /// Robust unchanged: there it simply has no resolver and the PATCH falls back to asset_id. + /// + /// + /// The two lookups are done per call, not captured: an agent's comes and goes with + /// the connection, and the module is registered on the scene. The call itself is the legacy route's, field + /// for field (Scene.Inventory.cs:579-582). + /// + /// + private static InventoryServiceBackend.AssetTransactionResolver TransactionResolverFor(Scene scene) + => (agentId, transactionId, item) => + { + var transactions = scene.RequestModuleInterface(); + if (transactions is null) + { + m_log.LogWarning("[AIS]: item {Item} carried hash_id {Transaction} but region {Region} has no asset transaction module; the asset was not applied", + item.ID, transactionId, scene.Name); + return AisAssetTransaction.NotResolvable; + } + if (!scene.TryGetClient(agentId, out var client) || client is null) + { + m_log.LogWarning("[AIS]: item {Item} carried hash_id {Transaction} but agent {Agent} has no client in {Region}; the asset was not applied", + item.ID, transactionId, agentId, scene.Name); + return AisAssetTransaction.NotResolvable; + } + // A19: the module's verdict, not an unconditional yes. It is false only when the referenced assets + // were validated and refused, which is the case the cap has to report rather than answer 200 to. + bool applied = transactions.HandleItemUpdateFromTransaction(client, transactionId, item); + if (!applied) + { + m_log.LogWarning("[AIS]: item {Item} carried hash_id {Transaction} but the asset transaction module REFUSED the update for agent {Agent} in {Region}; the item still points at its previous asset", + item.ID, transactionId, agentId, scene.Name); + return AisAssetTransaction.Refused; + } + return AisAssetTransaction.Applied; + }; + + /// + /// S9. Turns "this item's asset changed" into "rebake if it mattered". Kept here, not in the backend, for the + /// same reason as the transaction resolver: stays free of Scene + /// (Ledger P-2) and Phase 2 on Robust, which has no presence to update, simply has no observer. + /// + /// + /// Queuing rather than baking is deliberate and is the same ordering the cap uses (Q-16): the save resolves + /// every worn item to its current asset, persists the result and raises the S5 trigger, and the bake's own + /// per-channel input hash then decides what is recomputed. An edit that changed nothing visible costs one + /// hash check per channel. + /// + /// + private static InventoryServiceBackend.WornAssetObserver WornAssetObserverFor(Scene scene) + => (agentId, itemId, newAssetId) => + { + ScenePresence sp = scene.GetScenePresence(agentId); + if (sp is null || sp.IsChildAgent) return; // S8: a child presence never drives an appearance save + + if (!AisWornAssets.ApplyTo(sp.Appearance, itemId, newAssetId)) + return; // not worn here, or already carrying this asset + + m_log.LogDebug("[AIS]: item {Item} is worn by {Agent} and its asset changed to {Asset}; queueing an appearance save in {Region}", + itemId, agentId, newAssetId, scene.Name); + scene.AvatarFactory?.QueueAppearanceSave(agentId); + }; + + /// + /// The library's owner, as the tree defines it: ILibraryService.LibraryRootFolder.Owner, set by + /// LibraryService to Constants.m_MrOpenSimID for the root folder and every library folder and + /// item (Source/OpenSim.Services.InventoryService/LibraryService.cs:50, 100, 115-116, 176, 199-200). + /// Read off the service rather than hardcoded, so a grid that supplies its own library owner still works. + /// + public static UUID LibraryOwnerOf(ILibraryService library) => library?.LibraryRootFolder?.Owner ?? UUID.Zero; + + /// + /// Phase 1 backend: the region's IInventoryService, scoped to the one resident whose cap this is. + /// Nothing here knows about scenes (Ledger P-2), so Phase 2 hosts it on Robust unchanged. + /// + /// AIS-SEC-1: the scoping is this class's job, and nothing below it does any. + /// has always documented that GetFolder and GetItem return + /// null for an object that is "not the agent's", and until this was written that promise was not kept: the + /// class was a pass-through, and XInventoryService resolves by UUID alone and says so — GetItem + /// queries inventoryID (XInventoryService.cs:633-641), GetFolder queries folderID + /// (:653-663), GetFolderContent carries the comment "This method doesn't receive a valud + /// principal id from the connector. So we disregard the principal and look by ID" (:319-323), + /// DeleteFolders "Ignore principal ID, it's bogus at connector level" (:482-492) and + /// DeleteItems "Just use the ID... *facepalms*" (:602-631). A valid AIS cap plus another + /// resident's item or folder UUID could therefore read, rename, delete, purge, slam and create across the + /// boundary. + /// + /// Why the fix is here and not in the service. That behaviour is upstream and other connector + /// paths depend on it — the Robust connector really does pass a principal the service cannot trust, which is + /// what those comments are about. The cap, by contrast, knows exactly whose it is: it is registered per agent + /// from OnRegisterCaps, so the owner is a constructor argument and every call is checked against it. + /// + /// The rule, in one line: a read answers only for an object whose Owner is + /// , and a write happens only when the object and its parent folder are the + /// owner's. The agent id each interface method takes is still checked — it must be the owner — but it is never + /// trusted as the scope. The scope is the field, and it is the field that is handed to the service. + /// + /// What the handler sees. A foreign object is indistinguishable from an absent one, so the + /// handler's pre-existing not-found paths fire and the route answers 404. That is deliberate: a 403 + /// would tell a caller that a UUID it guessed belongs to somebody, which is a membership oracle over the whole + /// inventory keyspace. No status mapping was invented for the security case. + /// + public sealed class InventoryServiceBackend : IAisInventoryBackend + { + /// Hands a transaction id and the item to whatever knows about asset transactions (A16). + public delegate AisAssetTransaction AssetTransactionResolver(UUID agentId, UUID transactionId, InventoryItemBase item); + + /// Told that an item's asset changed, so a worn one can rebake (S9). + public delegate void WornAssetObserver(UUID agentId, UUID itemId, UUID newAssetId); + + private readonly IInventoryService m_service; + private readonly UUID m_ownerId; + private readonly AssetTransactionResolver m_transactions; + private readonly WornAssetObserver m_wornAssets; + + /// + /// The one resident this backend serves. Zero is refused rather than defaulted: a zero owner would scope + /// nothing, which is exactly the state AIS-SEC-1 fixed, and a quiet guard is how that state would come back. + /// + public InventoryServiceBackend(IInventoryService service, UUID ownerId, + AssetTransactionResolver transactions = null, WornAssetObserver wornAssets = null) + { + m_service = service ?? throw new ArgumentNullException(nameof(service)); + if (ownerId.IsZero()) + throw new ArgumentException("an AIS inventory backend must be bound to a non-zero owner", nameof(ownerId)); + m_ownerId = ownerId; + m_transactions = transactions; + m_wornAssets = wornAssets; + } + + /// The resident whose inventory this is, and the only principal this class ever passes down. + public UUID OwnerId => m_ownerId; + + private bool IsCaller(UUID agentId) => agentId == m_ownerId; + private bool IsOwned(InventoryFolderBase folder) => folder is not null && folder.Owner == m_ownerId; + private bool IsOwned(InventoryItemBase item) => item is not null && item.Owner == m_ownerId; + + // ---------------- reads: nothing unless the caller is the owner AND the row is theirs ---------------- + + public InventoryFolderBase GetFolderForType(UUID agentId, FolderType type) + { + if (!IsCaller(agentId)) return null; + var folder = m_service.GetFolderForType(m_ownerId, type); + return IsOwned(folder) ? folder : null; + } + + public InventoryFolderBase GetFolder(UUID agentId, UUID folderId) + { + if (!IsCaller(agentId)) return null; + var folder = m_service.GetFolder(m_ownerId, folderId); + return IsOwned(folder) ? folder : null; + } + + public InventoryItemBase GetItem(UUID agentId, UUID itemId) + { + if (!IsCaller(agentId)) return null; + var item = m_service.GetItem(m_ownerId, itemId); + return IsOwned(item) ? item : null; + } + + /// + /// The folder itself must pass , and the contents are filtered as well: a row whose + /// parent is the owner's folder but whose own Owner is somebody else is a data fault, and it is not + /// this cap's to hand out. The collection is rebuilt rather than edited so the caller is never handed the + /// service's own lists. + /// + public InventoryCollection GetFolderContent(UUID agentId, UUID folderId) + { + if (GetFolder(agentId, folderId) is null) return null; + var content = m_service.GetFolderContent(m_ownerId, folderId); + if (content is null) return null; + + var folders = new List(); + if (content.Folders is not null) + foreach (var folder in content.Folders) if (IsOwned(folder)) folders.Add(folder); + var items = new List(); + if (content.Items is not null) + foreach (var item in content.Items) if (IsOwned(item)) items.Add(item); + + return new InventoryCollection + { + OwnerID = m_ownerId, + FolderID = folderId, + Version = content.Version, + Descendents = folders.Count + items.Count, + Folders = folders, + Items = items, + }; + } + + public IReadOnlyList GetSubFolders(UUID agentId, UUID folderId) + { + var content = GetFolderContent(agentId, folderId); + return content?.Folders ?? (IReadOnlyList)Array.Empty(); + } + + /// + /// GetMultipleItems returns one slot per requested id and null where the id is unknown + /// (XInventoryService.cs:643-651), so this drops nulls as well as foreign rows. The count that comes + /// back is therefore meaningful, which is what relies on. + /// + public IReadOnlyList GetItems(UUID agentId, IReadOnlyList itemIds) + { + if (!IsCaller(agentId) || itemIds is null || itemIds.Count == 0) return Array.Empty(); + var ids = new UUID[itemIds.Count]; + for (var i = 0; i < ids.Length; i++) ids[i] = itemIds[i]; + var found = m_service.GetMultipleItems(m_ownerId, ids); + if (found is null) return Array.Empty(); + var owned = new List(found.Length); + foreach (var item in found) if (IsOwned(item)) owned.Add(item); + return owned; + } + + public IReadOnlyList GetInventorySkeleton(UUID agentId) + { + if (!IsCaller(agentId)) return Array.Empty(); + var skeleton = m_service.GetInventorySkeleton(m_ownerId); + if (skeleton is null) return Array.Empty(); + var owned = new List(skeleton.Count); + foreach (var folder in skeleton) if (IsOwned(folder)) owned.Add(folder); + return owned; + } + + // ---------------- creates: the new object and its parent must both be the owner's ---------------- + + public bool AddFolder(InventoryFolderBase folder) + { + if (!IsOwned(folder) || folder.ParentID.IsZero()) return false; + if (GetFolder(m_ownerId, folder.ParentID) is null) return false; + return m_service.AddFolder(folder); + } + + public bool AddItem(InventoryItemBase item) + { + if (!IsOwned(item) || item.Folder.IsZero()) return false; + if (GetFolder(m_ownerId, item.Folder) is null) return false; + return m_service.AddItem(item); + } + + // ---------------- updates: the row must exist, be the owner's, and land in the owner's folder ---------------- + + public bool UpdateItem(InventoryItemBase item) + { + if (!IsOwned(item)) return false; + if (GetItem(m_ownerId, item.ID) is null) return false; + if (GetFolder(m_ownerId, item.Folder) is null) return false; + return m_service.UpdateItem(item); + } + + /// + /// The parent is checked only when ParentID is non-zero: the agent's own inventory root legitimately + /// has none, and refusing that would refuse a rename of the root. + /// + public bool UpdateFolder(InventoryFolderBase folder) + { + if (!IsOwned(folder)) return false; + if (GetFolder(m_ownerId, folder.ID) is null) return false; + if (folder.ParentID.IsNotZero() && GetFolder(m_ownerId, folder.ParentID) is null) return false; + return m_service.UpdateFolder(folder); + } + + // ---------------- deletes ---------------- + + /// + /// The whole batch is refused unless every id resolves to an item the owner holds, and the check is + /// one call: wraps GetMultipleItems, so a slam removing ~20 links + /// costs a single round trip to Robust rather than twenty. The returned count equalling the requested count + /// is exactly the "all of them, and all mine" test, because the service returns one slot per id. + /// + public bool DeleteItems(UUID agentId, IReadOnlyList itemIds) + { + if (!IsCaller(agentId) || itemIds is null) return false; + if (GetItems(m_ownerId, itemIds).Count != itemIds.Count) return false; + return m_service.DeleteItems(m_ownerId, new List(itemIds)); + } + + /// Per id rather than batched: a folder delete carries one or two ids, never a slam's twenty. + public bool DeleteFolders(UUID agentId, IReadOnlyList folderIds, bool onlyIfTrash) + { + if (!IsCaller(agentId) || folderIds is null) return false; + foreach (var id in folderIds) + if (GetFolder(m_ownerId, id) is null) return false; + return m_service.DeleteFolders(m_ownerId, new List(folderIds), onlyIfTrash); + } + + public bool PurgeFolder(InventoryFolderBase folder) + { + if (!IsOwned(folder)) return false; + if (GetFolder(m_ownerId, folder.ID) is null) return false; + return m_service.PurgeFolder(folder); + } + + /// + /// Only a region with a transaction module and a connected client can resolve one; see the remarks on the + /// interface. AIS-SEC-1 makes it fail closed: an item that is not the owner's, or that is not in the + /// store, answers Refused rather than NotResolvable, because NotResolvable still + /// yields a 200 and applying a stranger's upload is not something to be relaxed about. A region with no + /// resolver at all still answers NotResolvable for the owner's own item, which is the documented + /// Phase 2 / library behaviour. + /// + public AisAssetTransaction ApplyAssetTransaction(UUID agentId, UUID transactionId, InventoryItemBase item) + { + if (!IsCaller(agentId) || !IsOwned(item) || GetItem(m_ownerId, item.ID) is null) + return AisAssetTransaction.Refused; + return m_transactions is null ? AisAssetTransaction.NotResolvable : m_transactions(m_ownerId, transactionId, item); + } + + /// + public void OnItemAssetChanged(UUID agentId, UUID itemId, UUID newAssetId) + { + if (m_wornAssets is null) return; + if (!IsCaller(agentId) || GetItem(m_ownerId, itemId) is null) return; + m_wornAssets(m_ownerId, itemId, newAssetId); + } + } + + /// + /// The LibraryAPIv3 backend: the shared library over , which holds the whole + /// tree in memory (GetAllFolders, InventoryFolderImpl.RequestListOfFolders/RequestListOfItems). + /// Read-only by construction — every mutator returns false and the handler answers 405 before reaching them + /// (John's Phase 1 ruling). The agent id is the library owner, so the same handler code needs no library + /// special case beyond its mode. + /// + public sealed class LibraryServiceBackend : IAisInventoryBackend + { + private readonly ILibraryService m_library; + public LibraryServiceBackend(ILibraryService library) { m_library = library ?? throw new ArgumentNullException(nameof(library)); } + + private InventoryFolderImpl Folder(UUID folderId) + { + var root = m_library.LibraryRootFolder; + if (root is null) return null; + if (root.ID.Equals(folderId)) return root; + return m_library.GetAllFolders().TryGetValue(folderId, out var folder) ? folder : null; + } + + /// The library has no per-agent system folders; only its root is addressable by type. + public InventoryFolderBase GetFolderForType(UUID agentId, FolderType type) + => type == FolderType.Root ? m_library.LibraryRootFolder : null; + + public InventoryFolderBase GetFolder(UUID agentId, UUID folderId) => Folder(folderId); + + public InventoryCollection GetFolderContent(UUID agentId, UUID folderId) + { + var folder = Folder(folderId); + if (folder is null) return null; + return new InventoryCollection + { + OwnerID = folder.Owner, + FolderID = folder.ID, + Version = folder.Version, + Folders = folder.RequestListOfFolders(), + Items = folder.RequestListOfItems(), + }; + } + + public IReadOnlyList GetSubFolders(UUID agentId, UUID folderId) + => Folder(folderId)?.RequestListOfFolders() ?? (IReadOnlyList)Array.Empty(); + + public IReadOnlyList GetInventorySkeleton(UUID agentId) + { + var all = m_library.GetAllFolders(); + var list = new List(all.Count); + foreach (var folder in all.Values) list.Add(folder); + return list; + } + + public IReadOnlyList GetItems(UUID agentId, IReadOnlyList itemIds) + { + var ids = new UUID[itemIds.Count]; + for (var i = 0; i < ids.Length; i++) ids[i] = itemIds[i]; + return m_library.GetMultipleItems(ids) ?? Array.Empty(); + } + + public InventoryItemBase GetItem(UUID agentId, UUID itemId) => m_library.GetItem(itemId); + + // read-only: the handler answers 405 for every mutation before it reaches these (AisMode.Library) + public bool AddFolder(InventoryFolderBase folder) => false; + public bool AddItem(InventoryItemBase item) => false; + public bool UpdateItem(InventoryItemBase item) => false; + public bool UpdateFolder(InventoryFolderBase folder) => false; + public bool DeleteItems(UUID agentId, IReadOnlyList itemIds) => false; + public bool DeleteFolders(UUID agentId, IReadOnlyList folderIds, bool onlyIfTrash) => false; + public bool PurgeFolder(InventoryFolderBase folder) => false; + /// The library is read-only and has no asset transactions. + public AisAssetTransaction ApplyAssetTransaction(UUID agentId, UUID transactionId, InventoryItemBase item) => AisAssetTransaction.NotResolvable; + /// Nothing in the library is worn. + public void OnItemAssetChanged(UUID agentId, UUID itemId, UUID newAssetId) { } + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisCopy.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisCopy.cs new file mode 100644 index 00000000000..17deca23a15 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisCopy.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using OpenMetaverse; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// What a library copy created at the destination. +public sealed record CopyOutcome( + IReadOnlyList Categories, + IReadOnlyList Items, + string Failure) +{ + public bool Ok => Failure is null; +} + +/// +/// COPY /category/{sourceId} on the library cap, with the destination folder in the Destination header +/// (A1: headers->append(HTTP_OUT_HEADER_DESTINATION, dest), llcorehttputil.cpp:1135). +/// +/// Permissions are the tree's existing library-copy rule, reused rather than reinvented. +/// Scene.Inventory.CopyInventoryItem finds the item in LibraryService.LibraryRootFolder first, and +/// when the source owner is the library owner it creates the copy with the source's **own** Base, Current, +/// EveryOne, Next and Group permissions — not degraded through NextPermissions, which is what the *other* branch +/// does for a copy between two residents (Source/OpenSim.Region.Framework/Scenes/Scene.Inventory.cs:1053-1064 +/// versus :1066-1078). A library item is copyable by construction, so there is no +/// PermissionMask.Copy check on that path either (:1046-1048 guards only the non-library case). +/// This copies that rule field for field. It cannot call CreateNewInventoryItem itself: that takes an +/// IClientAPI, and nothing in this handler may touch a scene (Ledger P-2). +/// +/// No rollback. Like a create and unlike a slam, a copy is purely additive: a failure part-way leaves +/// the folders and items already made, and nothing that existed before is at risk. The response reports the +/// failure and names what was made, so a client can retry or clean up. Rolling back would mean deleting objects a +/// concurrent operation may already have touched. +/// +public static class AisCopy +{ + /// + /// Copy and everything under it from into + /// in the agent's inventory. is false + /// when the viewer appended ,depth=0 to the tid (llaisapi.cpp:278). + /// + public static CopyOutcome Run(IAisInventoryBackend library, IAisInventoryBackend destination, + UUID libraryOwner, UUID agentId, UUID sourceId, UUID destinationParent, bool copySubfolders) + { + var source = library.GetFolder(libraryOwner, sourceId); + if (source is null) return new CopyOutcome(Array.Empty(), Array.Empty(), + $"no library category {sourceId}"); + + var categories = new List(); + var items = new List(); + var failure = CopyFolder(library, destination, libraryOwner, agentId, source, destinationParent, + copySubfolders, categories, items); + return new CopyOutcome(categories, items, failure); + } + + /// Copies one folder and its contents, recursing into sub-folders only when asked. + private static string CopyFolder(IAisInventoryBackend library, IAisInventoryBackend destination, + UUID libraryOwner, UUID agentId, InventoryFolderBase source, UUID parentId, bool copySubfolders, + List categories, List items) + { + var copy = new InventoryFolderBase(UUID.Random(), source.Name, agentId, source.Type, parentId, 1); + if (!destination.AddFolder(copy)) return $"could not create the folder {source.Name}"; + categories.Add(copy); + + var contents = library.GetFolderContent(libraryOwner, source.ID); + if (contents?.Items is not null) + { + foreach (var item in contents.Items) + { + var itemCopy = CopyItem(item, agentId, copy.ID); + if (!destination.AddItem(itemCopy)) return $"could not create the item {item.Name}"; + items.Add(itemCopy); + } + } + + if (!copySubfolders || contents?.Folders is null) return null; + foreach (var child in contents.Folders) + { + var error = CopyFolder(library, destination, libraryOwner, agentId, child, copy.ID, true, categories, items); + if (error is not null) return error; + } + return null; + } + + /// + /// One item, with the library-copy permission rule of Scene.Inventory.cs:1053-1064: creator and asset + /// preserved, owner becomes the agent, and every permission mask is carried over unchanged. + /// + private static InventoryItemBase CopyItem(InventoryItemBase source, UUID agentId, UUID folderId) + => new(UUID.Random(), agentId) + { + Folder = folderId, + Name = source.Name, + Description = source.Description, + AssetID = source.AssetID, + AssetType = source.AssetType, + InvType = source.InvType, + Flags = source.Flags, + CreatorId = source.CreatorId, + CreatorData = source.CreatorData, + CreationDate = Util.UnixTimeSinceEpoch(), + BasePermissions = source.BasePermissions, + CurrentPermissions = source.CurrentPermissions, + EveryOnePermissions = source.EveryOnePermissions, + NextPermissions = source.NextPermissions, + GroupPermissions = source.GroupPermissions, + SalePrice = source.SalePrice, + SaleType = source.SaleType, + }; +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisEnvelope.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisEnvelope.cs new file mode 100644 index 00000000000..c792f641c23 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisEnvelope.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// +/// Builds the LLSD maps the viewer parses: items, links, categories and the _embedded collections +/// (AIS-V3-SPEC.md §1c/§1d). +/// +/// Field set (A-Q1, resolved A1). LLInventoryItem::fromLLSD +/// (indra/llinventory/llinventory.cpp:984-1183) reads exactly: item_id (:1004), +/// parent_id (:1010), thumbnail{asset_id} (:1016) or thumbnail_id (:1037), +/// favorite{toggled} (:1043), permissions (:1054), sale_info (:1060), +/// shadow_id (:1087), asset_id (:1094), linked_id (:1100, also into the asset id), +/// type (:1106), inv_type (:1122), flags (:1137), name (:1150), desc (:1156) +/// and created_at (:1162). LLInventoryCategory::fromLLSD (:1289-1352) reads category_id +/// (:1293, the label constant INV_FOLDER_ID_LABEL_WS = "category_id", :67), parent_id (:1297), +/// thumbnail/thumbnail_id (:1303-1318), favorite{toggled} (:1321), +/// type **or** type_default as an integer folder type (:1333-1344, INV_ASSET_TYPE_LABEL_WS +/// = "type_default", :66) and name (:1346). It reads neither version nor a descendent count — +/// llaisapi.cpp reads those itself (spec §1d, §1e). +/// +/// Types are emitted as integers. fromLLSD accepts a string or an integer for type +/// and inv_type (:1108-1119, :1124-1135); integers are what this tree's own shipped LLSD inventory shape +/// uses (Source/OpenSim.Capabilities/LLSDInventoryItem.cs:33-68), which the LL viewer consumes today over +/// FetchInventoryDescendents2, so they are both authority-legal and already proven on the wire. +/// +/// UNVERIFIED. The inner key names of permissions and sale_info are read by +/// LLPermissions::importLLSD and LLSaleInfo::fromLLSD in llpermissions.cpp / +/// llsaleinfo.cpp, which are not permitted reads. This file emits the key set of +/// LLSDInventoryItem.cs for the same reason as above: it is what the region already sends and the viewer +/// already accepts. That LLViewerInventoryItem::unpackMessage(const LLSD&) (called at +/// llaisapi.cpp:1223) delegates to fromLLSD is likewise UNVERIFIED — llviewerinventory.cpp +/// is not permitted this session. +/// +public static class AisEnvelope +{ + public const string Categories = "categories"; + public const string Items = "items"; + public const string Links = "links"; + public const string Embedded = "_embedded"; + + /// True when the item is a link rather than a real item (links live in their own collection, §1c). + public static bool IsLink(InventoryItemBase item) + => item.AssetType == (int)AssetType.Link || item.AssetType == (int)AssetType.LinkFolder; + + /// The permissions sub-map, in the shape this tree already sends (LLSDInventoryItem.cs:51-62). + private static OSDMap Permissions(InventoryItemBase item) => new() + { + ["creator_id"] = item.CreatorIdAsUuid, + ["owner_id"] = item.Owner, + ["group_id"] = item.GroupID, + ["base_mask"] = (int)item.BasePermissions, + ["owner_mask"] = (int)item.CurrentPermissions, + ["group_mask"] = (int)item.GroupPermissions, + ["everyone_mask"] = (int)item.EveryOnePermissions, + ["next_owner_mask"] = (int)item.NextPermissions, + ["is_owner_group"] = item.GroupOwned, + }; + + private static OSDMap SaleInfo(InventoryItemBase item) => new() + { + ["sale_price"] = item.SalePrice, + ["sale_type"] = (int)item.SaleType, + }; + + /// + /// One item. is emitted as agent_id: the viewer's item parse does not read + /// it, but the fixtures and SL both carry it and it costs nothing. + /// + public static OSDMap Item(InventoryItemBase item, UUID agentId) + { + var map = new OSDMap + { + ["item_id"] = item.ID, + ["parent_id"] = item.Folder, + ["agent_id"] = agentId, + ["asset_id"] = item.AssetID, + ["name"] = item.Name ?? "", + ["desc"] = item.Description ?? "", + ["type"] = item.AssetType, + ["inv_type"] = item.InvType, + ["flags"] = (int)item.Flags, + ["created_at"] = item.CreationDate, + ["permissions"] = Permissions(item), + ["sale_info"] = SaleInfo(item), + }; + return map; + } + + /// + /// One link. Selected by linked_id + parent_id (spec §1d, llaisapi.cpp:1185); the target + /// id is the link row's asset id. The viewer overwrites a link's permissions and sale info with defaults + /// (llaisapi.cpp:1278-1283, :1303-1307), so they are not emitted. The remaining fields match the + /// link maps the viewer itself builds for SlamFolder — name, desc, linked_id, type + /// (llappearancemgr.cpp:2230-2234, A-Q3). + /// + public static OSDMap Link(InventoryItemBase link, UUID agentId) => new() + { + ["item_id"] = link.ID, + ["parent_id"] = link.Folder, + ["agent_id"] = agentId, + ["linked_id"] = link.AssetID, + ["name"] = link.Name ?? "", + ["desc"] = link.Description ?? "", + ["type"] = link.AssetType, + ["inv_type"] = link.InvType, + ["flags"] = (int)link.Flags, + ["created_at"] = link.CreationDate, + }; + + /// + /// One category. version is the folder's freshly read version (tree state T4); + /// is attached only when this category's contents are being expanded — a category + /// must carry all three collections or none, because the viewer derives its descendent count from having all + /// three and refuses to version a folder without one (spec §1c/§1e, risk A-R3). + /// + public static OSDMap Category(InventoryFolderBase folder, UUID agentId, OSDMap embedded = null) + { + var map = new OSDMap + { + ["category_id"] = folder.ID, + ["parent_id"] = folder.ParentID, + ["agent_id"] = agentId, + ["name"] = folder.Name ?? "", + ["type_default"] = (int)folder.Type, + ["version"] = (int)folder.Version, + }; + if (embedded is not null) map[Embedded] = embedded; + return map; + } + + /// + /// An _embedded map. Always all three collections, even when empty (risk A-R3): a category returned + /// without one never gets a descendent count on the viewer and is re-fetched forever. + /// + public static OSDMap EmbeddedMap(OSDMap categories, OSDMap items, OSDMap links) => new() + { + [Categories] = categories ?? new OSDMap(), + [Items] = items ?? new OSDMap(), + [Links] = links ?? new OSDMap(), + }; + + /// Items keyed by item id, links excluded — callers split them first. + public static OSDMap ItemsMap(IEnumerable items, UUID agentId) + { + var map = new OSDMap(); + if (items is null) return map; + foreach (var item in items) map[item.ID.ToString()] = Item(item, agentId); + return map; + } + + /// Links keyed by the link's own id (not the target's). + public static OSDMap LinksMap(IEnumerable links, UUID agentId) + { + var map = new OSDMap(); + if (links is null) return map; + foreach (var link in links) map[link.ID.ToString()] = Link(link, agentId); + return map; + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisFolderLocks.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisFolderLocks.cs new file mode 100644 index 00000000000..f83ef540b57 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisFolderLocks.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading; +using OpenMetaverse; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// +/// AIS-SEC-3. Serialises mutations that target the same (agent, folder), process-wide. +/// +/// The defect this exists for. snapshots a folder's links, creates the +/// wanted set, then deletes the snapshot. Create-before-delete is the right failure bias — the folder never holds +/// fewer links than it started with (A3) — but nothing ordered two mutations on one folder. Two slams that both +/// snapshot the old links, both create their own set and both delete only what they saw leave the folder holding +/// the union of two outfits. A purge racing a slam is the same shape from the other side: it decides what +/// to delete, the slam replaces all of it, and the purge's closing re-read then blames the slam's new links for a +/// partial purge it never caused. +/// +/// The viewer produces this on its own — an outfit change issued while a previous one is still in flight, or +/// two sessions — and one agent can hold caps from more than one region in this process, so it is not only +/// a cross-process concern. +/// +/// Striped, not per-key. A fixed array of semaphores allocates nothing per request, has nothing to +/// reference-count and nothing to leak, where a ConcurrentDictionary<key, SemaphoreSlim> must be +/// swept or it grows for the life of the process. The cost is that two unrelated keys can share a stripe and +/// serialise against each other for the length of one mutation. That is never wrong, only occasionally +/// slower, and with entries it is rare. is exposed so a test can +/// assert that the keys it uses do not collide, rather than depend on luck. +/// +/// Static, and that is the point. Every agent gets a fresh per cap +/// registration, and an agent present in two regions of one simulator holds two of them over the same inventory. +/// A per-handler lock would order nothing between those. +/// +/// What this is not. It is the single-process answer. It cannot order a mutation in this simulator +/// against one in another simulator or in Robust, because there is no shared lock and +/// IInventoryService offers no transaction and no batch write (tree state T5, Ledger A-R2/A-Q10). Phase 2, +/// which hosts these routes on Robust, needs a real inventory transaction — a service-side operation that +/// replaces a folder's links in one call — and until that exists the cross-process window stands. Saying so here +/// because a lock is exactly the kind of thing that gets mistaken for a full fix. +/// +public static class AisFolderLocks +{ + /// + /// How long a mutation waits for the folder before giving up. Generous next to a slam (a handful of inventory + /// round trips) and short enough that a wedged request does not hold an HTTP thread for a minute. + /// + public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(15); + + /// Stripe count. A power of two, far above the concurrent inventory mutations one simulator sees. + public const int Stripes = 256; + + private static readonly SemaphoreSlim[] s_locks = Build(); + + private static SemaphoreSlim[] Build() + { + var locks = new SemaphoreSlim[Stripes]; + for (int i = 0; i < locks.Length; i++) locks[i] = new SemaphoreSlim(1, 1); + return locks; + } + + /// + /// Which stripe a key lands on. Public so the concurrency tests can assert their keys do not collide; see the + /// remarks on the class. + /// + /// The combined hash is avalanched before it is truncated, and that is not decoration. Measured + /// on this tree's UUID: GetHashCode() has a low byte of 0x80 for every UUID of the form + /// xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx - the variant nibble lands there - so four structured ids that + /// differ everywhere a human looks gave the identical low 8 bits, and taking those bits as a bucket index put + /// every such key on stripe 0. Random UUIDs spread fine, which is exactly what makes it a trap: it + /// would have looked correct in production and collapsed to a single global lock for any sequential or + /// hand-built id family, of which this tree has several. The finalising mix below moves every input bit into + /// the low ones, so the truncation is sound whatever the ids look like. + /// + public static int StripeOf(UUID agentId, UUID folderId) + { + unchecked + { + uint h = (uint)((agentId.GetHashCode() * 397) ^ folderId.GetHashCode()); + h ^= h >> 16; + h *= 0x7feb352d; + h ^= h >> 15; + h *= 0x846ca68b; + h ^= h >> 16; + return (int)(h & (Stripes - 1)); // Stripes is a power of two, so the mask is exact + } + } + + /// + /// Takes the folder. Returns false on timeout, and the caller must then answer and not proceed — a + /// mutation that ran unlocked because the lock was busy would be the defect with extra steps. + /// + public static bool TryEnter(UUID agentId, UUID folderId) + => s_locks[StripeOf(agentId, folderId)].Wait(Timeout); + + public static void Exit(UUID agentId, UUID folderId) + => s_locks[StripeOf(agentId, folderId)].Release(); +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisHandler.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisHandler.cs new file mode 100644 index 00000000000..7e4c0e27cb7 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisHandler.cs @@ -0,0 +1,1200 @@ +using System; +using System.Collections.Generic; +using System.Net; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Framework.Servers.HttpServer; +using Microsoft.Extensions.Logging; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// Which cap this handler is mounted as. The routes are the same; the library one is read-only. +public enum AisMode +{ + /// InventoryAPIv3: the agent's own inventory. + Inventory, + /// LibraryAPIv3: the shared library, owned by the library owner, read-only (mutations answer 405). + Library, +} + +/// +/// The InventoryAPIv3 / LibraryAPIv3 cap handler: one per agent per cap, mounted at a random cap path. Parses the +/// request with and dispatches on . Holds only an agent id, a +/// backend and its cap path (Ledger P-2) so the same class can be mounted on Robust in Phase 2. +/// +/// A1 implemented the **read** surface: GET /item, /category/{id}/children (whole and subset), +/// /categories, /links, /category/current/links and /orphans. A2 adds the +/// **single-object mutations**: PATCH /item, PATCH /category, DELETE /item and +/// DELETE /category, each answering with the delta envelope of §1d-bis. SlamFolder, PurgeDescendents, +/// CreateInventory and COPY are still 501 (A3/A4). Every mutation is 405 on the library cap. +/// +/// Which collections a response carries. The viewer derives a folder's descendent count only when +/// _embedded has all three of categories, items, links — or, for a Current Outfit or +/// Outfit folder, from links alone (spec §1c, llaisapi.cpp:1466-1482). It then uses that count to +/// accept the folder's version. So a route that returns a folder's **complete** contents +/// (/children) emits all three, and a route that returns a **partial** view (/categories, +/// /links, a subset) emits only the collection it was asked for — emitting empty siblings there would make +/// the viewer compute a wrong descendent count and version a folder it has not actually seen. This refines risk +/// A-R3, which said "always all three". +/// +public sealed class AisHandler : SimpleStreamHandler +{ + private static readonly ILogger m_log = LoggerProvider.CreateLogger(typeof(AisHandler)); + + private readonly UUID m_agentId; + private readonly IAisInventoryBackend m_backend; + private readonly string m_capPath; + private readonly AisMode m_mode; + + /// + /// Where a library COPY writes, and as whom. Only the library cap has these: its own is + /// the library owner, so it needs the viewing agent's inventory to copy *into*. Null on the inventory cap, + /// where COPY is not an operation the viewer sends. + /// + private readonly IAisInventoryBackend m_destination; + private readonly UUID m_destinationAgentId; + + public AisHandler(string capPath, UUID agentId, IAisInventoryBackend backend, AisMode mode = AisMode.Inventory, + IAisInventoryBackend destination = null, UUID destinationAgentId = default) + : base(capPath, mode == AisMode.Library ? AISv3Module.LibraryCapName : AISv3Module.CapName) + { + m_capPath = capPath; + m_agentId = agentId; + m_backend = backend ?? throw new ArgumentNullException(nameof(backend)); + m_mode = mode; + m_destination = destination; + m_destinationAgentId = destinationAgentId; + } + + public UUID AgentId => m_agentId; + public string CapPath => m_capPath; + public AisMode Mode => m_mode; + + protected override void ProcessRequest(IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) + { + var route = AisRouter.Parse(httpRequest.HttpMethod, httpRequest.RawUrl ?? httpRequest.UriPath, m_capPath); + // A6: without this a request that arrives and fails is indistinguishable in the log from one that never + // arrived, which is exactly what made the first live run take a code read to diagnose. + if (m_log.IsEnabled(LogLevel.Debug)) + m_log.LogDebug("[AIS]: {Verb} {Url} -> {Operation} (cap {Mode}, agent {Agent})", + httpRequest.HttpMethod, httpRequest.RawUrl ?? httpRequest.UriPath, route.Operation, m_mode, m_agentId); + Dispatch(route, httpRequest, httpResponse); + } + + /// Dispatch a parsed route. Public so the HTTP-level tests can drive it without a scene. + public void Dispatch(AisRoute route, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) + { + var read = ReadBody(httpRequest); + + // AIS-SEC-2. A mutating route never runs on a body we could not read. The fetch routes are untouched: + // they ignore the body entirely, and refusing one over a body nobody looks at would break clients that + // send a stray one. Checked here, before the switch, so no handler can see a body it should not. + // + // The library cap is the exception, and the ordering matters: there every mutation but COPY is refused + // 405 because the cap is read-only, and that answer is true whatever the body says. Letting the body + // gate run first would turn "this cap does not do mutations" into "your body was malformed", which is + // both less informative and wrong about the reason. COPY keeps the gate - it is a genuine library + // operation, so its body is worth bounding. + var refusedAsReadOnly = m_mode == AisMode.Library && route.Operation != AisOperation.CopyCategory; + if (AisOperations.IsMutation(route.Operation) && !refusedAsReadOnly) + { + switch (read.Status) + { + case AisBodyStatus.TooLarge: + m_log.LogWarning("[AIS]: {Operation} on {Path} from agent {Agent} sent a body over the {Limit} byte limit ({Bytes} bytes and still coming); refused unread", + route.Operation, route.Path, m_agentId, MaxBodyBytes, read.Bytes); + WriteError(httpResponse, HttpStatusCode.RequestEntityTooLarge, + $"the request body exceeds the {MaxBodyBytes} byte limit", route); + return; + + case AisBodyStatus.ParseFailure: + // Byte count only, never the body: it is attacker-controlled and may carry anything. + m_log.LogWarning("[AIS]: {Operation} on {Path} from agent {Agent} sent a body that is not LLSD ({Bytes} bytes); refused without writing", + route.Operation, route.Path, m_agentId, read.Bytes); + WriteError(httpResponse, HttpStatusCode.BadRequest, "malformed LLSD body", route); + return; + + // A slam REPLACES a folder's links, so an absent body cannot be read as "replace them with + // nothing" - that is the defect this session closed. The other mutating routes keep today's + // empty-map behaviour deliberately: their semantics are not in scope here. + case AisBodyStatus.NoBody when route.Operation == AisOperation.SlamFolder: + m_log.LogWarning("[AIS]: SlamFolder on {Path} from agent {Agent} arrived with no body; refused rather than read as an empty slam", + route.Path, m_agentId); + WriteError(httpResponse, HttpStatusCode.BadRequest, "missing body", route); + return; + } + } + + var raw = read.Value ?? new OSDMap(); + var body = raw as OSDMap ?? new OSDMap(); + try + { + switch (route.Operation) + { + case AisOperation.Unknown: + WriteError(httpResponse, HttpStatusCode.NotFound, "no such AIS v3 route", route); + return; + + case AisOperation.FetchItem: FetchItem(route, httpResponse); return; + case AisOperation.FetchCategoryChildren: FetchChildren(route, httpResponse); return; + case AisOperation.FetchCategorySubset: FetchSubset(route, httpResponse); return; + case AisOperation.FetchCategoryCategories: FetchCategories(route, httpResponse); return; + case AisOperation.FetchCategoryLinks: + case AisOperation.FetchCOF: FetchLinks(route, httpResponse); return; + case AisOperation.FetchOrphans: FetchOrphans(route, httpResponse); return; + case AisOperation.CopyCategory: CopyCategory(route, httpRequest, httpResponse); return; + + case AisOperation.UpdateItem: + case AisOperation.UpdateCategory: + case AisOperation.RemoveItem: + case AisOperation.RemoveCategory: + case AisOperation.SlamFolder: + case AisOperation.CreateInventory: + case AisOperation.PurgeDescendents: + // COPY is the exception: it is a library-cap operation by design (the viewer sends it to + // {lib}, spec 1a row 5), and it writes into the *agent's* inventory, not the library. + if (m_mode == AisMode.Library) + { + WriteError(httpResponse, HttpStatusCode.MethodNotAllowed, $"{route.Operation} is not allowed on the library: LibraryAPIv3 is read-only", route); + return; + } + switch (route.Operation) + { + case AisOperation.UpdateItem: UpdateItem(route, body, httpResponse); return; + case AisOperation.UpdateCategory: UpdateCategory(route, body, httpResponse); return; + case AisOperation.RemoveItem: RemoveItem(route, httpResponse); return; + case AisOperation.SlamFolder: SlamFolder(route, raw, httpResponse); return; + case AisOperation.CreateInventory: CreateInventory(route, body, httpResponse); return; + case AisOperation.PurgeDescendents: PurgeDescendents(route, httpResponse); return; + default: RemoveCategory(route, httpResponse); return; + } + + default: + // Every mutation. On the library cap they are refused outright; on the inventory cap they are + // not implemented yet (A2). Both bodies are flat maps the viewer's update parser ignores (§1f). + if (m_mode == AisMode.Library) + WriteError(httpResponse, HttpStatusCode.MethodNotAllowed, $"{route.Operation} is not allowed on the library: LibraryAPIv3 is read-only", route); + else + WriteError(httpResponse, HttpStatusCode.NotImplemented, $"{route.Operation} is not implemented", route); + return; + } + } + catch (Exception ex) + { + // AIS-SEC-5. The exception goes to the LOG, never to the client. It used to be the other way round: + // `WriteError(..., ex.Message, ...)` with no logging at all, so a connector or database fault + // travelled to an untrusted client as text - credentials, host names, internal type names, whatever + // the message happened to carry - while the operator who needs the stack got nothing. Both halves + // were the same line. + // + // The client gets a fixed string. There is nothing useful it could do with the detail, the viewer + // does not read the message (spec 1f: nothing in the permitted files reads `message`), and the route + // and verb it already knows are echoed by ErrorBody anyway. + m_log.LogError(ex, + "[AIS]: {Operation} on {Path} for agent {Agent} failed with an unhandled exception; answered 500", + route.Operation, route.Path, m_agentId); + WriteError(httpResponse, HttpStatusCode.InternalServerError, + "the request could not be completed", route); + } + } + + // ------------------------------------------------------------------ the read routes + + /// GET /item/{id} — an item, or a link map when the row is a link (§1c: linked_id selects parseLink). + private void FetchItem(AisRoute route, IOSHttpResponse response) + { + var item = m_backend.GetItem(m_agentId, route.Id); + if (item is null) { WriteError(response, HttpStatusCode.NotFound, $"no item {route.Id}", route); return; } + var body = AisEnvelope.IsLink(item) ? AisEnvelope.Link(item, m_agentId) : AisEnvelope.Item(item, m_agentId); + Write(response, body, route); + } + + /// + /// GET /category/{id}/children?depth=N — the folder with its complete contents, expanded N generations + /// (). All three collections at every expanded level. + /// + private void FetchChildren(AisRoute route, IOSHttpResponse response) + { + // MAX_FOLDER_DEPTH_REQUEST (llaisapi.cpp:58): the viewer clamps every depth it sends to 50, so anything + // above that is a client we do not know asking the region to walk further than any viewer would use. + var depth = System.Math.Clamp(route.Depth, 0, AisInventory.MaxDepth); + var walked = AisInventory.Walk(m_backend, m_agentId, route.Id, depth); + if (walked.Count == 0) { WriteError(response, HttpStatusCode.NotFound, $"no category {route.Id}", route); return; } + + var expanded = new Dictionary(); + foreach (var c in walked) expanded[c.Folder.ID] = c; + // AIS-SEC-5: one visited set for the whole expansion, replacing the per-level dictionary clone. + Write(response, Expand(walked[0], expanded, new HashSet()), route); + } + + /// + /// A folder as a category map with all three collections; each sub-folder is expanded in turn when the walk + /// reached it, and appears as a bare category map (no _embedded) when it did not. + /// + /// + /// AIS-SEC-5: replaces the per-level dictionary clone this used to make. + /// + /// Why that substitution is equivalence and not an approximation. The old Without() + /// copied the whole expanded map minus the current folder and handed the same copy to every sibling, + /// so it implemented ancestor-path exclusion: a folder excluded down one branch was still available + /// to a sibling branch. A single shared set is stronger - global once-only. The two disagree exactly + /// when a folder is reachable by two distinct paths, i.e. a diamond. + /// InventoryFolderBase.ParentID is a single scalar and GetFolderContent selects children by + /// ParentID == folderId, so every folder is the child of exactly one parent: the graph is a forest + /// plus possible cycles, and a diamond cannot occur. AisErrorHygieneTraversalTests pins both the + /// cyclic output shape and that data-model property, so if multi-parenting ever arrives the assumption + /// fails loudly rather than silently. + /// + /// The clone was correct; it was just O(folders) of allocation at every level, which a deep tree paid + /// all the way down. The set is one allocation for the whole response. + /// + private OSDMap Expand(AisFolderContents contents, Dictionary expanded, HashSet visited) + { + visited.Add(contents.Folder.ID); + var categories = new OSDMap(); + foreach (var child in contents.SubFolders) + { + categories[child.ID.ToString()] = !visited.Contains(child.ID) && expanded.TryGetValue(child.ID, out var childContents) + ? Expand(childContents, expanded, visited) + : AisEnvelope.Category(child, m_agentId); + } + var embedded = AisEnvelope.EmbeddedMap(categories, AisEnvelope.ItemsMap(contents.Items, m_agentId), AisEnvelope.LinksMap(contents.Links, m_agentId)); + return AisEnvelope.Category(contents.Folder, m_agentId, embedded); + } + + /// + /// GET /category/{id}/children?depth=N&children=a,b,... — only the named children. The viewer ignores the + /// top-level category for a subset and parses _embedded one level shallower (§1c, + /// llaisapi.cpp:1194-1202), so the folder is still the envelope but its collections carry only what was + /// asked for. A named child that does not exist is simply absent: the viewer asked for it, so its absence is + /// the answer, and failing the whole request would lose the children that do exist. + /// + private void FetchSubset(AisRoute route, IOSHttpResponse response) + { + var contents = AisInventory.GetContents(m_backend, m_agentId, route.Id); + if (contents is null) { WriteError(response, HttpStatusCode.NotFound, $"no category {route.Id}", route); return; } + + var wanted = new HashSet(route.Children); + var categories = new OSDMap(); + foreach (var child in contents.SubFolders) + if (wanted.Contains(child.ID)) categories[child.ID.ToString()] = AisEnvelope.Category(child, m_agentId); + var items = new OSDMap(); + foreach (var item in contents.Items) + if (wanted.Contains(item.ID)) items[item.ID.ToString()] = AisEnvelope.Item(item, m_agentId); + var links = new OSDMap(); + foreach (var link in contents.Links) + if (wanted.Contains(link.ID)) links[link.ID.ToString()] = AisEnvelope.Link(link, m_agentId); + + Write(response, AisEnvelope.Category(contents.Folder, m_agentId, AisEnvelope.EmbeddedMap(categories, items, links)), route); + } + + /// GET /category/{id}/categories — sub-folders only, so _embedded carries categories alone. + private void FetchCategories(AisRoute route, IOSHttpResponse response) + { + var folder = m_backend.GetFolder(m_agentId, route.Id); + if (folder is null) { WriteError(response, HttpStatusCode.NotFound, $"no category {route.Id}", route); return; } + var categories = new OSDMap(); + foreach (var child in m_backend.GetSubFolders(m_agentId, route.Id) ?? (IReadOnlyList)Array.Empty()) + categories[child.ID.ToString()] = AisEnvelope.Category(child, m_agentId); + var embedded = new OSDMap { [AisEnvelope.Categories] = categories }; + Write(response, AisEnvelope.Category(folder, m_agentId, embedded), route); + } + + /// + /// GET /category/{id}/links and GET /category/current/links — the folder and everything its links point to. + /// _embedded carries links (the link rows) and, so the viewer has the targets it will need, + /// items holding the **link targets** — the items the links resolve to, not the links themselves. That + /// is what the existing descendents cap sends for the same reason ("viewers are lasy and want a copy of the + /// linked item sent before the link to it", FetchInvDescHandler.cs:429), and what + /// gathers. + /// + /// For a Current Outfit or Outfit folder the viewer takes the descendent count from links alone + /// (§1c), which is exactly this shape. + /// + private void FetchLinks(AisRoute route, IOSHttpResponse response) + { + var folderId = route.Id; + if (route.Operation == AisOperation.FetchCOF) + { + var cof = AisInventory.GetCurrentOutfit(m_backend, m_agentId); + if (cof is null) { WriteError(response, HttpStatusCode.NotFound, "the agent has no Current Outfit folder", route); return; } + folderId = cof.ID; + // A11: the resolution, per request. The A7 WARN only fires when there is more than one candidate, so + // in the ordinary case nothing recorded which folder "current" meant — which is what made A10 have to + // infer it from the mutation URLs. + if (m_log.IsEnabled(LogLevel.Debug)) + m_log.LogDebug("[AIS]: FetchCOF resolved \"current\" to {Folder} version {Version} for agent {Agent}", + cof.ID, cof.Version, m_agentId); + } + + var contents = AisInventory.GetContents(m_backend, m_agentId, folderId); + if (contents is null) { WriteError(response, HttpStatusCode.NotFound, $"no category {folderId}", route); return; } + + var targets = AisInventory.ResolveLinkTargets(m_backend, m_agentId, contents.Links); + var embedded = new OSDMap + { + [AisEnvelope.Links] = AisEnvelope.LinksMap(contents.Links, m_agentId), + [AisEnvelope.Items] = AisEnvelope.ItemsMap(targets, m_agentId), + }; + Write(response, AisEnvelope.Category(contents.Folder, m_agentId, embedded), route); + } + + /// + /// GET /orphans — folders whose parent no longer exists. No category_id/item_id at top level, so + /// the viewer parses _embedded straight (§1c). Orphaned items are not reported; see + /// for why. + /// + private void FetchOrphans(AisRoute route, IOSHttpResponse response) + { + var orphans = AisInventory.FindOrphans(m_backend, m_agentId); + var categories = new OSDMap(); + foreach (var folder in orphans.Folders) categories[folder.ID.ToString()] = AisEnvelope.Category(folder, m_agentId); + var embedded = new OSDMap + { + [AisEnvelope.Categories] = categories, + [AisEnvelope.Items] = AisEnvelope.ItemsMap(orphans.Items, m_agentId), + }; + Write(response, new OSDMap { [AisEnvelope.Embedded] = embedded }, route); + } + + // ------------------------------------------------------------------ AIS-SEC-3: the folder mutation lock + + /// + /// Runs holding the (agent, folder) lock, or answers 503 and runs nothing. + /// + /// Every mutating route takes exactly one key, so there is no lock-ordering problem to solve and + /// deadlock is structurally impossible rather than merely avoided. That holds because AIS-SEC-2 made + /// POST /category/{parent} refuse a body whose parent_id disagrees with the URL (400), so a + /// create can only ever write into the folder it addressed. If a future route genuinely needs two + /// folders - a create whose categories carry links destined for a child, say - take them in ascending + /// UUID string order and say so at the call site; that total order is what keeps it deadlock-free. + /// + /// 503 and not 409. 409 Conflict says the request disagrees with the current state and the client + /// must resolve it - re-fetch, merge, decide. Nothing is wrong with this request: it is valid and would + /// succeed, and the server simply declined to queue behind another change any longer. That is "temporarily + /// unavailable", which is 503, and it is the status that carries Retry-After. The viewer treats any + /// non-2xx here alike (llaisapi.cpp:851-951), so the header is for well-behaved clients and the + /// operator reading the log. + /// + private void WithFolderLock(UUID folderId, AisRoute route, IOSHttpResponse response, Action body) + => WithFolderLock(m_agentId, folderId, route, response, body); + + /// + /// As above, for the one route that writes as somebody other than this cap's owner: a library COPY writes into + /// the viewing agent's inventory, so its key is that agent and the destination folder. Keying it on the + /// library owner would order library copies against each other and not against the resident's own slams into + /// the same folder, which is the pairing that actually races. + /// + private void WithFolderLock(UUID lockAgentId, UUID folderId, AisRoute route, IOSHttpResponse response, Action body) + { + if (!AisFolderLocks.TryEnter(lockAgentId, folderId)) + { + m_log.LogWarning( + "[AIS]: {Operation} on folder {Folder} for agent {Agent} waited {Seconds}s for the folder lock and " + + "gave up; answered 503 rather than mutating unserialised", route.Operation, folderId, lockAgentId, + AisFolderLocks.Timeout.TotalSeconds); + response.AddHeader("Retry-After", "2"); + WriteError(response, HttpStatusCode.ServiceUnavailable, + $"another change to category {folderId} is already in progress; retry", route); + return; + } + + try { body(); } + finally { AisFolderLocks.Exit(lockAgentId, folderId); } + } + + // ------------------------------------------------------------------ the mutation routes (A2) + + /// + /// The largest AIS request body this handler will read. A real slam is a few kilobytes — the viewer sends one + /// link map per worn item — so a megabyte is far above anything legitimate and still small enough that + /// refusing it costs nothing. Public so the tests assert against the same number the handler enforces. + /// + public const int MaxBodyBytes = 1024 * 1024; + + /// What became of the request body. Four states, because three of them must not reach a handler. + private enum AisBodyStatus + { + /// The request carried no body at all. + NoBody, + /// The body parsed as LLSD. + Parsed, + /// There was a body and it is not LLSD. + ParseFailure, + /// The body exceeded and was not read to the end. + TooLarge, + } + + private readonly record struct AisBody(AisBodyStatus Status, OSD Value, long Bytes) + { + public static AisBody None() => new(AisBodyStatus.NoBody, null, 0); + public static AisBody Ok(OSD value, long bytes) => new(AisBodyStatus.Parsed, value, bytes); + public static AisBody Failure(long bytes) => new(AisBodyStatus.ParseFailure, null, bytes); + public static AisBody Oversize(long bytes) => new(AisBodyStatus.TooLarge, null, bytes); + } + + /// + /// The request body as LLSD, whatever its top-level type. A slam body is a bare **array** + /// (llappearancemgr.cpp:2209-2245, :1795-1833), so it cannot be forced to a map here. + /// + /// AIS-SEC-2: the guarantee is that a body which fails validation produces zero inventory writes. + /// This used to end in catch { return new OSDMap(); } and return that same empty map for an absent + /// body, which handed something it read as an intentional empty slam — so a + /// truncated PUT /category/{COF}/links, and a dropped connection is enough, emptied the wearer's + /// Current Outfit. A body that cannot be understood is now a distinct answer from a body that asks for + /// nothing, and only the caller decides what to do with each. + /// + /// Do not reduce the failure test to a try/catch and a null check. Verified against the shipped + /// parser on 2026-09-12: OSDParser.DeserializeLLSDXml neither throws nor returns null for truncated + /// XML or for well-formed XML that is not LLSD — it returns a bare whose + /// is OSDType.Unknown. That value is the actual signal, and without it a + /// malformed PATCH body still becomes an empty map and still answers 200. + /// + /// The ceiling is enforced while the stream is copied, never after: a body too large to trust is + /// also a body too large to hold, so reading stops the moment the limit is passed. + /// + private static AisBody ReadBody(IOSHttpRequest request) + { + var stream = request?.InputStream; + if (stream is null) return AisBody.None(); + try + { + var buffer = new byte[8192]; + using var ms = new System.IO.MemoryStream(); + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + if (ms.Length + read > MaxBodyBytes) return AisBody.Oversize(ms.Length + read); + ms.Write(buffer, 0, read); + } + if (ms.Length == 0) return AisBody.None(); + + var bytes = ms.ToArray(); + OSD parsed; + try { parsed = OSDParser.DeserializeLLSDXml(bytes); } + catch { return AisBody.Failure(bytes.Length); } + if (parsed is null || parsed.Type == OSDType.Unknown) return AisBody.Failure(bytes.Length); + return AisBody.Ok(parsed, bytes.Length); + } + catch { return AisBody.Failure(-1); } + finally { try { request.InputStream?.Dispose(); } catch { } } + } + + /// + /// PATCH /item/{id}. The updated item goes back as **top-level content** — on a mutation the viewer ignores + /// anything embedded that is not in _created_items (§1c) — and its parent folder is listed in + /// _updated_category_versions, without which the viewer discards even the zero-delta entry + /// parseItem creates (§1d-bis, llaisapi.cpp:1625-1629). Fields this tree cannot store are + /// ignored, not refused: the viewer sends the whole item map, so most keys carry unchanged values. + /// + /// + /// A16: the body's asset travels either as asset_id or, on the path a wearable save actually takes, as + /// hash_id — the id of the xfer transaction that uploaded it. The map fields are applied and stored + /// first and the transaction is handed over second, which is the order the legacy UDP route uses + /// (Scene.Inventory.cs:576 then :579-582); the transaction module stores the item again once the + /// xfer completes, so the item is re-read before the envelope is built. Both writes bump the parent folder's + /// version in the data layer (MySQLXInventoryData.cs:238-246), which is what makes the viewer re-read + /// the item at all — before A16 no write happened, so no version moved and the edit was never fetched back. + /// + /// + private void UpdateItem(AisRoute route, OSDMap body, IOSHttpResponse response) + { + var item = m_backend.GetItem(m_agentId, route.Id); + if (item is null) { WriteError(response, HttpStatusCode.NotFound, $"no item {route.Id}", route); return; } + // AIS-SEC-3: key on item.Folder - the item's parent folder, whose version the store bumps. + WithFolderLock(item.Folder, route, response, () => + { + + // S9: captured before ApplyToItem, which mutates the item in place. + var assetBefore = item.AssetID; + + var applied = AisMutation.ApplyToItem(body, item); + if (applied.Any && !m_backend.UpdateItem(item)) + { + WriteError(response, HttpStatusCode.InternalServerError, $"the inventory service refused the update of item {route.Id}", route); + return; + } + + if (applied.Transaction.IsNotZero()) + { + // Unknown transaction ids are not an error here: the module opens a pending uploader for one and the + // asset lands when the xfer does, exactly as it does for the legacy route (AgentAssetsTransactions.cs:68-90). + // A19: a refused transaction is a FAILED save and must be answered as one. Before this, the verdict + // was discarded and the cap answered 200 with the item's old asset id in the envelope, so the viewer + // recorded a save that had not happened - observed 2026-09-06 09:52:55, a wearable referencing a + // library texture refused by the uploader and reported as "UpdateItem -> 200". + // + // 403 rather than 500: the refusal is always a permission verdict on the referenced assets + // (AssetXferUploader.ValidateAssets), and the viewer treats a non-2xx as an error without special + // handling for this command (llaisapi.cpp:880-948). The error body carries no + // _updated_category_versions, so the folder version the viewer holds does NOT advance and its next + // fetch of that folder still sees the true state. + if (m_backend.ApplyAssetTransaction(m_agentId, applied.Transaction, item) == AisAssetTransaction.Refused) + { + WriteError(response, HttpStatusCode.Forbidden, + $"the asset uploaded by transaction {applied.Transaction} was refused for item {route.Id}; the item still points at its previous asset", route); + return; + } + item = m_backend.GetItem(m_agentId, route.Id) ?? item; + } + + // S9: an edit to a WORN wearable is the one appearance change nothing else tells the region about. The + // worn set does not move (the viewer keeps the item id), so no AgentIsNowWearing follows, and the + // UpdateAvatarAppearance POST is deferred behind pending uploads and can arrive stale. This PATCH is the + // moment the new asset exists and is known, so it is where the save is queued. + if (item.AssetID != assetBefore && item.AssetID.IsNotZero()) + m_backend.OnItemAssetChanged(m_agentId, route.Id, item.AssetID); + + var envelope = AisEnvelope.Item(item, m_agentId); + AisMutation.ReportVersion(envelope, m_backend.GetFolder(m_agentId, item.Folder)); + Write(response, envelope, route); + }); + } + + /// + /// PATCH /category/{id}. parseCategory creates zero-delta entries for the category **and** its parent + /// (§1d-bis, llaisapi.cpp:1419-1428), so both are listed. thumbnail and favorite have no + /// storage in this tree and are dropped. + /// + private void UpdateCategory(AisRoute route, OSDMap body, IOSHttpResponse response) + { + // AIS-SEC-3: key on route.Id - the folder itself. + WithFolderLock(route.Id, route, response, () => + { + var folder = m_backend.GetFolder(m_agentId, route.Id); + if (folder is null) { WriteError(response, HttpStatusCode.NotFound, $"no category {route.Id}", route); return; } + + var applied = AisMutation.ApplyToFolder(body, folder); + if (applied.Any && !m_backend.UpdateFolder(folder)) + { + WriteError(response, HttpStatusCode.InternalServerError, $"the inventory service refused the update of category {route.Id}", route); + return; + } + + var fresh = m_backend.GetFolder(m_agentId, route.Id) ?? folder; + var envelope = AisEnvelope.Category(fresh, m_agentId); + AisMutation.ReportVersion(envelope, fresh); + AisMutation.ReportVersion(envelope, m_backend.GetFolder(m_agentId, fresh.ParentID)); + Write(response, envelope, route); + }); + } + + /// + /// DELETE /item/{id}. No content: the removal travels as _removed_items and the parent's new version + /// as _updated_category_versions (§1d-bis). The parent is read **after** the delete, so the version is + /// the post-operation one the data layer bumped (S0a V6, tree state T3/T4). + /// + private void RemoveItem(AisRoute route, IOSHttpResponse response) + { + var item = m_backend.GetItem(m_agentId, route.Id); + if (item is null) { WriteError(response, HttpStatusCode.NotFound, $"no item {route.Id}", route); return; } + var parentId = item.Folder; + // AIS-SEC-3: key on parentId - the item's parent: its child list changes. + WithFolderLock(parentId, route, response, () => + { + + if (!m_backend.DeleteItems(m_agentId, new[] { route.Id })) + { + WriteError(response, HttpStatusCode.InternalServerError, $"the inventory service refused the delete of item {route.Id}", route); + return; + } + + var envelope = new OSDMap(); + AisMutation.ReportRemoved(envelope, AisMutation.RemovedItems, route.Id); + AisMutation.ReportVersion(envelope, m_backend.GetFolder(m_agentId, parentId)); + Write(response, envelope, route); + }); + } + + /// + /// COPY /category/{sourceId}?tid= — CopyLibraryCategory ( for the permission rule this + /// reuses and the no-rollback reasoning). + /// + /// The destination folder id travels in the HTTP Destination header + /// (llcorehttputil.cpp:1135, A1). The tid carries a quirk: when the viewer does **not** want + /// sub-folders it appends the literal ,depth=0 to the tid value rather than adding a query parameter + /// (llaisapi.cpp:275-278), which already splits out into the depth + /// query value — so depth == 0 here means "this folder only". + /// + /// The destination is **not** subjected to the protected-folder rule. Copying a library folder into + /// Clothing or into the inventory root is the ordinary case, and that rule governs moving, deleting and + /// retyping a folder, not adding children to it — the same reconciliation as slam (A3) and purge. What is + /// checked is the thing that matters: the destination must exist and be the agent's own folder. + /// + private void CopyCategory(AisRoute route, IOSHttpRequest request, IOSHttpResponse response) + { + if (m_mode != AisMode.Library || m_destination is null) + { + WriteError(response, HttpStatusCode.NotImplemented, + "COPY is a LibraryAPIv3 operation; this cap cannot serve it", route); + return; + } + + var destinationHeader = request.Headers["Destination"]; + if (!UUID.TryParse(destinationHeader, out var destinationId) || destinationId.IsZero()) + { + WriteError(response, HttpStatusCode.BadRequest, + "COPY needs a Destination header carrying the destination folder id", route); + return; + } + + var destinationFolder = m_destination.GetFolder(m_destinationAgentId, destinationId); + if (destinationFolder is null) + { + WriteError(response, HttpStatusCode.NotFound, $"no destination category {destinationId}", route); + return; + } + + // ",depth=0" on the tid means this folder only (llaisapi.cpp:275-278) + // AIS-SEC-3: COPY writes into the DESTINATION folder as the DESTINATION agent, not as this cap's + // owner (which is the library). So the key is (m_destinationAgentId, destinationId) - keying it on + // the library owner would order library copies against each other and not against the resident's + // own slams into the same folder, which is the pairing that actually races. + WithFolderLock(m_destinationAgentId, destinationId, route, response, () => + { + var copySubfolders = route.Depth != 0; + + var outcome = AisCopy.Run(m_backend, m_destination, m_agentId, m_destinationAgentId, + route.Id, destinationId, copySubfolders); + + var envelope = new OSDMap(); + var categoryIds = new OSDArray(); + var itemIds = new OSDArray(); + var embeddedCategories = new OSDMap(); + var embeddedItems = new OSDMap(); + foreach (var folder in outcome.Categories) + { + categoryIds.Add(OSD.FromUUID(folder.ID)); + embeddedCategories[folder.ID.ToString()] = AisEnvelope.Category(folder, m_destinationAgentId, + AisEnvelope.EmbeddedMap(new OSDMap(), new OSDMap(), new OSDMap())); + } + foreach (var item in outcome.Items) + { + itemIds.Add(OSD.FromUUID(item.ID)); + embeddedItems[item.ID.ToString()] = AisEnvelope.Item(item, m_destinationAgentId); + } + + if (!outcome.Ok) + { + // additive, so a partial copy leaves what it made and risks nothing that existed before + WriteError(response, HttpStatusCode.InternalServerError, + $"{outcome.Failure}; {categoryIds.Count} categories and {itemIds.Count} items were created before the failure", route); + return; + } + + if (categoryIds.Count > 0) envelope[AisMutation.CreatedCategories] = categoryIds; + if (itemIds.Count > 0) envelope[AisMutation.CreatedItems] = itemIds; + if (embeddedCategories.Count > 0 || embeddedItems.Count > 0) + { + var embedded = new OSDMap(); + if (embeddedCategories.Count > 0) embedded[AisEnvelope.Categories] = embeddedCategories; + if (embeddedItems.Count > 0) embedded[AisEnvelope.Items] = embeddedItems; + envelope[AisEnvelope.Embedded] = embedded; + } + AisMutation.ReportVersion(envelope, m_destination.GetFolder(m_destinationAgentId, destinationId)); + Write(response, envelope, route); + }); + } + /// + /// DELETE /category/{id}/children — empty the folder, keeping the folder (, which + /// documents who calls it, why the deltas must be enumerated and what the composition costs). + /// + /// The protected-folder rule is deliberately not applied: Trash and Lost and Found are protected types + /// and are precisely the folders this operation exists to empty (llinventorymodel.cpp:4125-4131). + /// + private void PurgeDescendents(AisRoute route, IOSHttpResponse response) + { + var folderId = route.Id; + if (route.IsAlias) + { + var cof = AisInventory.GetCurrentOutfit(m_backend, m_agentId); + if (cof is null) { WriteError(response, HttpStatusCode.NotFound, "the agent has no Current Outfit folder", route); return; } + folderId = cof.ID; + } + + var folder = m_backend.GetFolder(m_agentId, folderId); + if (folder is null) { WriteError(response, HttpStatusCode.NotFound, $"no category {folderId}", route); return; } + + // AIS-SEC-3: key on folderId - snapshot -> delete, the other side of the same race. + WithFolderLock(folderId, route, response, () => + { + var outcome = AisPurge.Run(m_backend, m_agentId, folder); + + var envelope = new OSDMap(); + foreach (var id in outcome.RemovedCategories) AisMutation.ReportRemoved(envelope, AisMutation.CategoriesRemoved, id); + foreach (var id in outcome.RemovedItems) AisMutation.ReportRemoved(envelope, AisMutation.RemovedItems, id); + AisMutation.ReportVersion(envelope, m_backend.GetFolder(m_agentId, folderId)); + + if (!outcome.Ok) + { + // Partly purged. A purge cannot be rolled back, so the honest answer is to say which children + // survived; re-issuing the purge finishes the job (see AisPurge.Run). + WriteError(response, HttpStatusCode.InternalServerError, + $"category {folderId} was only partly purged; these children remain: {string.Join(", ", outcome.Survivors)}", route); + return; + } + Write(response, envelope, route); + }); + } + /// + /// POST /category/{parentId}?tid= — create categories, items and links in that folder. + /// + /// The route is the parent category itself, not /children: AISAPI::CreateInventory + /// builds {inv}/category/{parentId} (llaisapi.cpp:115). + /// + /// The body is a map of arrays, and A4 pinned two of the three against their builders: + /// + /// links — verified. link_inventory_array builds each entry with exactly + /// linked_id, type (AT_LINK or AT_LINK_FOLDER), inv_type (the + /// target's inventory type), name and desc, and sends them as + /// new_inventory["links"] (llviewerinventory.cpp:1352-1370). No parent_id: the folder + /// is the one in the URL. + /// items — verified, and deliberately refused. The one builder wraps the item's whole + /// asLLSD() with a null item_id and a null asset_id"don't know yet, whenever + /// server creates it" — because the server is expected to mint the asset + /// (llviewerinventory.cpp:1124-1157). It sits inside #ifdef USE_AIS_FOR_NC, which is never + /// defined in that file, above the viewer's own comment "not yet implemented within AIS3" + /// (:1120-1121) — so a stock viewer never sends it. This handler answers **501** for a non-empty + /// items array rather than creating an item with no asset behind it, which is what A3's guess did. + /// + /// categories — verified (A5). LLInventoryCategory::asAISCreateCatLLSD + /// (indra/llinventory/llinventory.cpp:1256-1276) emits exactly category_id (null on a create, + /// since the viewer builds the category with LLUUID::null, llinventorymodel.cpp:1038), + /// parent_id, type_default as an **integer** preferred type, name, and — only when set — + /// thumbnail{asset_id} and favorite{toggled}. It is a base-class method, which is + /// why A4 could not find it in llviewerinventory.cpp. Everything it sends is accepted; + /// thumbnail and favorite have no column in this tree and are dropped, as they are for a + /// PATCH. + /// + /// + /// AIS-SEC-4: a failure partway reports what it already created, and does not roll back. + /// Categories are added one at a time and then links one at a time, so a refusal on the third write leaves the + /// first two in the database. Before this, the response carried only the error keys, so those objects were + /// invisible to the client: it could neither adopt nor remove them, and its retry made duplicates. The failure + /// path now returns the same delta envelope the success path builds — _created_categories, + /// _created_items, _embedded and a re-read _updated_category_versions — so a client parses + /// one shape either way, and the viewer genuinely adopts it: it applies every response body as an update, error + /// or not (onUpdateReceived, llaisapi.cpp:946, spec §1f), firing its completion callback per + /// created id (§1c). + /// + /// No rollback, deliberately, and is the precedent: a create is purely + /// additive, so a partial one leaves what it made and risks nothing that existed before, whereas a rollback + /// that itself failed would leave a worse and less describable state than the one it tried to repair — and it + /// would have to delete objects a concurrent operation may already have touched. Reporting beats repairing + /// here. A slam is the opposite case and does roll back (), because there the + /// dangerous outcome is a folder left with fewer links than it started with. + /// + /// The status stays 500. 207 would be a 2xx, and llaisapi would then treat the response as + /// success and never log the failure at all (§1f's table: "any other failure (4xx/5xx, timeout) → warn with + /// status and pretty-printed body"). 500 with a populated body keeps the failure visible and still hands the + /// client what it needs. + /// + private void CreateInventory(AisRoute route, OSDMap body, IOSHttpResponse response) + { + // AIS-SEC-3: key on route.Id - the addressed folder, and it is the only one written: AIS-SEC-2 refuses a body parent_id that disagrees (400). + WithFolderLock(route.Id, route, response, () => + { + var parent = m_backend.GetFolder(m_agentId, route.Id); + if (parent is null) { WriteError(response, HttpStatusCode.NotFound, $"no category {route.Id}", route); return; } + + // Refused before anything is written, so a mixed body does not half-succeed. See the remarks above: + // the viewer's own items builder is compiled out and expects the server to create the asset. + if (body["items"] is OSDArray requested && requested.Count > 0) + { + WriteError(response, HttpStatusCode.NotImplemented, + "creating inventory items through AIS is not implemented: the body carries a null asset_id for the server to fill, and this region does not create assets. The viewer's own path is disabled (USE_AIS_FOR_NC).", route); + return; + } + + // AIS-SEC-1. asAISCreateCatLLSD repeats the parent in the body (llinventory.cpp:1256-1276) and + // link_inventory_array sends none at all (llviewerinventory.cpp:1352-1370), so a body parent that + // disagrees with the URL is not something a viewer sends. The URL is the authority + // (AISAPI::CreateInventory builds {inv}/category/{parentId}, llaisapi.cpp:115) and the disagreement is + // refused rather than resolved: honouring the body would let a create addressed to a folder the caller + // owns plant objects in a folder they do not. The backend's own parent check would refuse the write in + // any case; this makes the refusal say why, and says it before anything is written, so a mixed body + // cannot half-succeed. + foreach (var key in new[] { "categories", "links" }) + { + if (body[key] is not OSDArray entries) continue; + foreach (var entry in entries) + { + if (entry is not OSDMap m || !m.ContainsKey("parent_id")) continue; + var named = m["parent_id"].AsUUID(); + if (named.IsZero() || named.Equals(route.Id)) continue; + WriteError(response, HttpStatusCode.BadRequest, + $"the body names parent_id {named} but the request addressed category {route.Id}; the URL is the authority", route); + return; + } + } + + var createdCategories = new OSDMap(); + var createdItems = new OSDMap(); + var createdLinks = new OSDMap(); + var categoryIds = new OSDArray(); + var itemIds = new OSDArray(); + + // AIS-SEC-4. Built the same way whether this request succeeds or fails partway, so a client parses + // one shape either way. See WriteErrorWithDeltas and the remarks on this method. + OSDMap BuildEnvelope() + { + var env = new OSDMap(); + if (categoryIds.Count > 0) env[AisMutation.CreatedCategories] = categoryIds; + if (itemIds.Count > 0) env[AisMutation.CreatedItems] = itemIds; + if (createdCategories.Count > 0 || createdItems.Count > 0 || createdLinks.Count > 0) + { + var emb = new OSDMap(); + if (createdCategories.Count > 0) emb[AisEnvelope.Categories] = createdCategories; + if (createdItems.Count > 0) emb[AisEnvelope.Items] = createdItems; + if (createdLinks.Count > 0) emb[AisEnvelope.Links] = createdLinks; + env[AisEnvelope.Embedded] = emb; + } + // Re-read: the parent's version moved with every write that landed, and without it the viewer + // skips the folder entirely (§1d-bis, llaisapi.cpp:1625-1629) - which on a partial failure would + // leave it never re-reading the folder it half-filled. + AisMutation.ReportVersion(env, m_backend.GetFolder(m_agentId, route.Id)); + return env; + } + + // Reports the failure with everything created before it. No rollback; see the method remarks. + void Fail(string what) + { + m_log.LogWarning( + "[AIS]: CreateInventory into folder {Folder} for agent {Agent} failed after creating " + + "{Categories} categories and {Items} items: {What}. The created objects are reported in the " + + "response so the client can reconcile; nothing is rolled back.", + route.Id, m_agentId, categoryIds.Count, itemIds.Count, what); + WriteErrorWithDeltas(response, HttpStatusCode.InternalServerError, what, route, BuildEnvelope()); + } + + if (body["categories"] is OSDArray categories) + { + foreach (var entry in categories) + { + if (entry is not OSDMap m) continue; + // asAISCreateCatLLSD sends parent_id alongside the URL parent; honour it when it names a real + // folder, and fall back to the folder the POST addressed, as an item create does. + var bodyParent = m["parent_id"].AsUUID(); + var folder = new InventoryFolderBase(UUID.Random(), m["name"].AsString() ?? "", m_agentId, + (short)(m.ContainsKey("type_default") ? m["type_default"].AsInteger() + : m.ContainsKey("type") ? m["type"].AsInteger() : -1), + bodyParent.IsZero() ? route.Id : bodyParent, 1); + if (!m_backend.AddFolder(folder)) + { + Fail($"could not create the category {folder.Name}"); + return; + } + categoryIds.Add(OSD.FromUUID(folder.ID)); + createdCategories[folder.ID.ToString()] = AisEnvelope.Category(folder, m_agentId, + AisEnvelope.EmbeddedMap(new OSDMap(), new OSDMap(), new OSDMap())); + } + } + + foreach (var (key, isLink) in new[] { ("links", true) }) + { + if (body[key] is not OSDArray array) continue; + foreach (var entry in array) + { + if (entry is not OSDMap m) continue; + var row = NewItem(m, isLink, route.Id); + if (!m_backend.AddItem(row)) + { + Fail($"could not create {(isLink ? "the link" : "the item")} {row.Name}"); + return; + } + itemIds.Add(OSD.FromUUID(row.ID)); + if (AisEnvelope.IsLink(row)) createdLinks[row.ID.ToString()] = AisEnvelope.Link(row, m_agentId); + else createdItems[row.ID.ToString()] = AisEnvelope.Item(row, m_agentId); + } + } + + Write(response, BuildEnvelope(), route); + }); + } + + /// An item or link row from a create body. Unknown keys are ignored, as they are for a PATCH. + private InventoryItemBase NewItem(OSDMap m, bool isLink, UUID parentId) + { + var assetType = m.ContainsKey("type") ? m["type"].AsInteger() + : isLink ? (int)AssetType.Link : (int)AssetType.Unknown; + var linked = m["linked_id"].AsUUID(); + return new InventoryItemBase(UUID.Random(), m_agentId) + { + // the body may name a parent; absent, the object goes in the folder the POST addressed + Folder = m.ContainsKey("parent_id") && !m["parent_id"].AsUUID().IsZero() + ? m["parent_id"].AsUUID() : parentId, + Name = m["name"].AsString() ?? "", + Description = m["desc"].AsString() ?? "", + AssetID = linked.IsZero() ? m["asset_id"].AsUUID() : linked, + AssetType = assetType, + InvType = m.ContainsKey("inv_type") ? m["inv_type"].AsInteger() : 0, + Flags = (uint)m["flags"].AsInteger(), + CreatorId = m_agentId.ToString(), + CreationDate = (int)Util.UnixTimeSinceEpoch(), + BasePermissions = (uint)OpenSim.Framework.PermissionMask.All, + CurrentPermissions = (uint)OpenSim.Framework.PermissionMask.All, + NextPermissions = (uint)OpenSim.Framework.PermissionMask.All, + }; + } + /// + /// PUT /category/{id}/links — replace the folder's links (, which documents the + /// ordering and the exact guarantee). current resolves to the Current Outfit folder as it does for a + /// fetch. + /// + /// A slam is deliberately **not** subject to the protected-folder rule. That rule is the viewer's + /// lookupIsProtectedType, which governs moving, deleting and retyping a folder + /// (llfoldertype.cpp:151-153) — the Current Outfit folder is protected by it, and slamming the Current + /// Outfit is the single most common thing the viewer does (llappearancemgr.cpp:2251). + /// + /// Only **links** are touched. Non-link items in the folder are left alone: the viewer builds the body + /// from link rows only (:1795-1833 switches on AT_LINK / AT_LINK_FOLDER and ignores + /// everything else), so a slam has nothing to say about them. + /// + /// The envelope: the created links are named in _created_items and carried in + /// _embedded.links — on a mutation the viewer accepts an embedded object only when its id is in + /// _created_items (§1c) — the removed ones in _removed_items, and the folder's fresh version in + /// _updated_category_versions. Each parsed link adds +1 to the folder's descendent count + /// (llaisapi.cpp:1310-1312) and each removal −1 (:1130), so the arithmetic closes. + /// + private void SlamFolder(AisRoute route, OSD rawBody, IOSHttpResponse response) + { + var folderId = route.Id; + if (route.IsAlias) + { + var cof = AisInventory.GetCurrentOutfit(m_backend, m_agentId); + if (cof is null) { WriteError(response, HttpStatusCode.NotFound, "the agent has no Current Outfit folder", route); return; } + folderId = cof.ID; + } + + // AIS-SEC-3: key on folderId - the whole snapshot -> create -> delete window, which is the AIS-SEC-3 defect itself. + WithFolderLock(folderId, route, response, () => + { + var contents = AisInventory.GetContents(m_backend, m_agentId, folderId); + if (contents is null) { WriteError(response, HttpStatusCode.NotFound, $"no category {folderId}", route); return; } + + var wanted = AisSlam.ParseBody(rawBody); + if (wanted is null) + { + WriteError(response, HttpStatusCode.BadRequest, + "a slam body must be an LLSD array of link maps (name, desc, linked_id, type)", route); + return; + } + + var outcome = AisSlam.Run(m_backend, m_agentId, folderId, contents.Links, wanted); + if (!outcome.Ok) + { + var detail = outcome.CompensationFailed + ? $"{outcome.Failure}; the rollback also failed and these links remain: {string.Join(", ", outcome.Leftover)}" + : outcome.Failure; + WriteError(response, HttpStatusCode.InternalServerError, detail, route); + return; + } + + var envelope = new OSDMap(); + var createdIds = new OSDArray(); + var links = new OSDMap(); + foreach (var link in outcome.Created) + { + createdIds.Add(OSD.FromUUID(link.ID)); + links[link.ID.ToString()] = AisEnvelope.Link(link, m_agentId); + } + if (createdIds.Count > 0) + { + envelope[AisMutation.CreatedItems] = createdIds; + envelope[AisEnvelope.Embedded] = new OSDMap { [AisEnvelope.Links] = links }; + } + foreach (var removed in outcome.Removed) AisMutation.ReportRemoved(envelope, AisMutation.RemovedItems, removed); + AisMutation.ReportVersion(envelope, m_backend.GetFolder(m_agentId, folderId)); + Write(response, envelope, route); + }); + } + /// + /// DELETE /category/{id}. Only the folder id goes in _categories_removed: the viewer purges the + /// descendents itself (LLInventoryModel::onObjectDeletedFromServer calls + /// onDescendentsPurgedFromServer for a category, llinventorymodel.cpp:2019-2023), so they are + /// implied rather than enumerated. + /// + /// Deletion is not restricted to Trash (A2b, Ledger A-Q9 resolved). The call passes + /// onlyIfTrash: false through the IInventoryService overload added in A2b, because the viewer + /// deletes any non-protected folder wherever it sits (llviewerinventory.cpp:1545-1568, read in A2). The + /// result is still verified by re-reading the folder rather than trusting the return value, since the service + /// returns true even when it deleted nothing. + /// + /// Protected folders are refused with 403. The viewer refuses to send RemoveCategory for a folder + /// whose type LLFolderType::lookupIsProtectedType accepts (llviewerinventory.cpp:1557-1561), so + /// this is defence in depth rather than a path the viewer exercises. That predicate's exact membership lives in + /// llfoldertype.cpp, which is not a permitted read, so the server rule is stated in our own terms and + /// marked UNVERIFIED against the viewer's list: a folder is protected when it is the agent's root or carries a + /// system type, **except** , which is an ordinary saved outfit that users delete + /// routinely. + /// + private void RemoveCategory(AisRoute route, IOSHttpResponse response) + { + var folder = m_backend.GetFolder(m_agentId, route.Id); + if (folder is null) { WriteError(response, HttpStatusCode.NotFound, $"no category {route.Id}", route); return; } + var parentId = folder.ParentID; + // AIS-SEC-3: key on parentId - the PARENT's child list is what changes, so the parent is the key - two deletes of siblings must order. + WithFolderLock(parentId, route, response, () => + { + + if (IsProtected(folder)) + { + WriteError(response, HttpStatusCode.Forbidden, + $"category {route.Id} is a protected system folder and cannot be deleted", route); + return; + } + + m_backend.DeleteFolders(m_agentId, new[] { route.Id }, onlyIfTrash: false); + if (m_backend.GetFolder(m_agentId, route.Id) is not null) + { + WriteError(response, HttpStatusCode.InternalServerError, + $"the inventory service did not delete category {route.Id}", route); + return; + } + + var envelope = new OSDMap(); + AisMutation.ReportRemoved(envelope, AisMutation.CategoriesRemoved, route.Id); + AisMutation.ReportVersion(envelope, m_backend.GetFolder(m_agentId, parentId)); + Write(response, envelope, route); + }); + } + + /// + /// The folder types the viewer does **not** protect, taken from `LLFolderDictionary` itself + /// (`indra/llinventory/llfoldertype.cpp:85-127`): every `addEntry` whose PROTECTED column is `false`. They are + /// `FT_NONE`, the unused ensemble range `FT_ENSEMBLE_START`..`FT_ENSEMBLE_END`, `FT_OUTFIT`, and the three + /// marketplace types. Everything else in the table is protected, and — importantly — + /// `lookupIsProtectedType` **returns true for any type the table does not contain** (`:154-162`), which is why + /// this is expressed as an allow-list with a protected default. + /// + private static readonly HashSet UnprotectedFolderTypes = new() + { + (short)FolderType.None, + (short)FolderType.Outfit, + (short)FolderType.MarketplaceListings, + (short)FolderType.MarkplaceStock, + // FT_MARKETPLACE_VERSION (55) is unprotected in the viewer's table but this tree's FolderType has no + // member for it, so it falls through to the protected default. It is a marketplace type no OpenSim grid + // creates; see the session decisions. + }; + + /// The unused ensemble range, entered as unprotected in the viewer's table (`llfoldertype.cpp:106-109`). + private const short EnsembleStart = 26; + private const short EnsembleEnd = 45; + + /// + /// A folder the server refuses to delete. This is the viewer's own rule: `LLFolderType::lookupIsProtectedType` + /// looks the type up in `LLFolderDictionary` and returns that entry's PROTECTED flag, **defaulting to true for + /// an unknown type** (`indra/llinventory/llfoldertype.cpp:154-162`). The viewer refuses to send RemoveCategory + /// for such a folder (`llviewerinventory.cpp:1557-1561`), so this is defence in depth — but it also means a + /// type this tree has and the viewer does not, such as `FolderType.Suitcase`, is protected automatically, + /// which is the safe answer. + /// + /// The agent's inventory root is refused as well. The viewer covers it by type + /// (`FT_ROOT_INVENTORY` is protected), and this adds the structural case of a folder with no parent, which is + /// either the root or an orphan and is not something a delete should walk into. + /// + public static bool IsProtected(InventoryFolderBase folder) + { + if (folder is null) return true; + if (folder.ParentID.IsZero()) return true; // the inventory root, or an orphan + if (folder.Type >= EnsembleStart && folder.Type <= EnsembleEnd) return false; + return !UnprotectedFolderTypes.Contains(folder.Type); // unknown types are protected, as the viewer's are + } + // ------------------------------------------------------------------ wire + + /// + /// 200 with an LLSD XML body. The viewer sends and reads LLSD XML and sets both Content-Type and + /// Accept to application/llsd+xml on every AIS request (A-Q2, resolved A1: + /// llcorehttputil.cpp:1219-1222 checkDefaultHeaders; bodies serialised with + /// LLSDSerialize::toXML at :144, :169, :193 and parsed with + /// LLSDSerialize::fromXML at :123). + /// + /// tid is echoed when the request carried one, so a client can correlate a response with the + /// transaction it asked for. Nothing in the permitted viewer files reads it back, so it is inert there. + /// + private static void Write(IOSHttpResponse response, OSDMap body, AisRoute route) + { + if (!route.Tid.IsZero()) body["tid"] = route.Tid; + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = "application/llsd+xml"; + response.RawBuffer = OSDParser.SerializeLLSDXmlBytes(body); + LogMutationResponse(route, (int)HttpStatusCode.OK, body); + } + + /// + /// A11: what a mutation actually answered — the status, and the delta keys with their contents. A10 could not + /// tell a response the viewer rejected from a response that was never wrong, because nothing recorded what we + /// sent back; this is that record. Fetches are skipped deliberately: their bodies are whole inventory + /// listings and logging them would bury the mutations that matter. + /// + /// Cost when DEBUG is off: one enum switch and one IsEnabled call, both of which run + /// before anything is built. is the only allocating work and it sits + /// in the argument list of a call that is never reached unless both predicates pass — so a production log + /// level pays two predicates and nothing else. No interpolated string is ever constructed: the message is a + /// constant template and the values are passed as arguments. + /// + private static void LogMutationResponse(AisRoute route, int status, OSDMap body) + { + if (!AisOperations.IsMutation(route.Operation)) return; + if (!m_log.IsEnabled(LogLevel.Debug)) return; + m_log.LogDebug("[AIS]: {Operation} -> {Status} {Deltas}", + route.Operation, status, AisMutation.SummariseDeltas(body)); + } + + /// The error body: an LLSD map with conventional keys the viewer ignores (spec §1f). + public static OSDMap ErrorBody(HttpStatusCode status, string message, AisRoute route) + { + return new OSDMap + { + ["error_code"] = (int)status, + ["error_description"] = status.ToString(), + ["message"] = message, + ["operation"] = route.Operation.ToString(), + ["verb"] = route.Verb, + ["path"] = route.Path, + }; + } + + /// + /// AIS-SEC-4. An error response that also carries a delta envelope — the objects a mutation had already + /// created when it failed. + /// + /// This is not a courtesy. The viewer parses every response body as an update, success or failure + /// (onUpdateReceived, llaisapi.cpp:946, spec §1f), and its completion callback fires per entry in + /// _created_categories / _created_items (§1c) — so ids placed here are genuinely adopted, and a + /// client that would otherwise retry and duplicate them does not have to. + /// + /// The error keys are layered under the envelope, so a delta key can never be shadowed by one of + /// them, and the envelope's own keys are the same ones the success path emits. Nothing adds a top-level + /// item_id or category_id, which §1f forbids in a body that does not mean them. + /// + private static void WriteErrorWithDeltas(IOSHttpResponse response, HttpStatusCode status, string message, + AisRoute route, OSDMap envelope) + { + var body = ErrorBody(status, message, route); + foreach (KeyValuePair kv in envelope) body[kv.Key] = kv.Value; + + response.StatusCode = (int)status; + response.ContentType = "application/llsd+xml"; + response.RawBuffer = OSDParser.SerializeLLSDXmlBytes(body); + + if (AisOperations.IsMutation(route.Operation) && m_log.IsEnabled(LogLevel.Debug)) + m_log.LogDebug("[AIS]: {Operation} -> {Status} {Message} {Deltas}", + route.Operation, (int)status, message, AisMutation.SummariseDeltas(body)); + } + + private static void WriteError(IOSHttpResponse response, HttpStatusCode status, string message, AisRoute route) + { + response.StatusCode = (int)status; + response.ContentType = "application/llsd+xml"; + var body = ErrorBody(status, message, route); + response.RawBuffer = OSDParser.SerializeLLSDXmlBytes(body); + + // A failed mutation is the case A10 most needed and least had: the status and the reason, on the same + // line shape as a success, so a log grep shows both. + if (AisOperations.IsMutation(route.Operation) && m_log.IsEnabled(LogLevel.Debug)) + m_log.LogDebug("[AIS]: {Operation} -> {Status} {Message}", route.Operation, (int)status, message); + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisInventory.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisInventory.cs new file mode 100644 index 00000000000..651b1424bac --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisInventory.cs @@ -0,0 +1,199 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using OpenMetaverse; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// One folder's direct contents, with links already split out of the item list (spec §1c). +public sealed record AisFolderContents( + InventoryFolderBase Folder, + IReadOnlyList SubFolders, + IReadOnlyList Items, + IReadOnlyList Links); + +/// Folders whose parent no longer exists (GET /orphans). +public sealed record AisOrphans(IReadOnlyList Folders, IReadOnlyList Items); + +/// +/// The composition the fetch routes need on top of the backend primitives: split links out of an item list, +/// resolve link targets, walk a folder to a depth, find orphans. Pure with respect to the backend — no Scene, no +/// ScenePresence (Ledger P-2) — so both the region host and Phase 2's Robust host share exactly this behaviour +/// rather than each re-deriving it. +/// +public static class AisInventory +{ + private static readonly ILogger m_log = LoggerProvider.CreateLogger(typeof(AisInventory)); + + /// A folder's contents with links separated from items; null when the folder is not the agent's. + public static AisFolderContents GetContents(IAisInventoryBackend backend, UUID agentId, UUID folderId) + { + var folder = backend.GetFolder(agentId, folderId); + if (folder is null) return null; + var collection = backend.GetFolderContent(agentId, folderId); + var items = new List(); + var links = new List(); + if (collection?.Items is not null) + foreach (var item in collection.Items) + (AisEnvelope.IsLink(item) ? links : items).Add(item); + var folders = (IReadOnlyList)(collection?.Folders ?? new List()); + return new AisFolderContents(folder, folders, items, links); + } + + /// + /// The items a set of links points at. There is no link-aware fetch in IInventoryService (tree state + /// T5), so this does what the existing descendents cap does: collect the AssetType.Link rows' asset ids + /// and resolve them with one GetMultipleItems + /// (Source/OpenSim.Capabilities.Handlers/FetchInventory/FetchInvDescHandler.cs:424-460, + /// ProcessLinks). Links to links are dropped for the same reason that handler drops them (:454-457): + /// they are not observed in practice and following them invites cycles. Broken links simply resolve to + /// nothing. + /// + public static IReadOnlyList ResolveLinkTargets(IAisInventoryBackend backend, UUID agentId, IEnumerable links) + { + var ids = new List(); + foreach (var link in links ?? Enumerable.Empty()) + if (link.AssetType == (int)AssetType.Link && !link.AssetID.IsZero() && !ids.Contains(link.AssetID)) + ids.Add(link.AssetID); + if (ids.Count == 0) return System.Array.Empty(); + + var resolved = backend.GetItems(agentId, ids) ?? (IReadOnlyList)System.Array.Empty(); + var targets = new List(resolved.Count); + foreach (var item in resolved) + if (item is not null && item.AssetType != (int)AssetType.Link) targets.Add(item); + return targets; + } + + /// The viewer's own ceiling on a requested depth: MAX_FOLDER_DEPTH_REQUEST (llaisapi.cpp:58). + public const int MaxDepth = 50; + /// + /// A folder and its descendants to levels below it. depth = 0 expands the + /// requested folder only: its own categories, items and links are listed, and each child + /// category appears as a bare map with no _embedded. Each further level of depth expands one more + /// generation. Returned in breadth-first order with the requested folder first. + /// + /// Settled in A2b from the viewer's own fetch path (spec §1c-bis): the viewer parses the requested + /// folder at the depth it asked for and each `_embedded` level one lower (`llaisapi.cpp:1205`, `:1461-1464`), + /// and versions a category only while that depth is still >= 0 and its descendent count is known + /// (`:1380-1407`). So N licenses exactly N generations below the requested folder — deeper is wasted work the + /// viewer will not version, shallower is safe because it re-queues every descendant regardless + /// (`llinventorymodelbackgroundfetch.cpp:610`). This implementation is that rule, with no off-by-one. + /// + + public static IReadOnlyList Walk(IAisInventoryBackend backend, UUID agentId, UUID rootId, int depth) + { + var expanded = new List(); + var root = GetContents(backend, agentId, rootId); + if (root is null) return expanded; + expanded.Add(root); + + // AIS-SEC-5: a visited set ALONGSIDE the depth cap, not instead of it. The cap bounds legitimate deep + // trees and is what the viewer's own MAX_FOLDER_DEPTH_REQUEST means; the visited set bounds a *cyclic* + // graph, which the cap previously only limited the damage of. A cycle A->B->A was re-walked once per + // level to the cap - 51 entries for two folders - and every one of those is a GetFolderContent, which on + // a grid deployment is a round trip to Robust. It is now two. + var visited = new HashSet { root.Folder.ID }; + + var frontier = new List { root }; + for (var level = 0; level < depth && frontier.Count > 0; level++) + { + var next = new List(); + foreach (var parent in frontier) + foreach (var child in parent.SubFolders) + { + if (!visited.Add(child.ID)) continue; // already expanded: a cycle, or a repeated row + var contents = GetContents(backend, agentId, child.ID); + if (contents is null) continue; + expanded.Add(contents); + next.Add(contents); + } + frontier = next; + } + return expanded; + } + + /// The agent's Current Outfit folder (spec §1b, tree state T2); null when the agent has none. + public static InventoryFolderBase GetCurrentOutfit(IAisInventoryBackend backend, UUID agentId) + => GetSystemFolder(backend, agentId, FolderType.CurrentOutfit); + + /// + /// The agent's folder of a system type, resolved **deterministically** even when the agent owns more than one + /// folder of that type. + /// + /// The service's own resolution is a coin flip: XInventoryService.GetSystemFolderForType returns + /// folders[0] from a query with no ORDER BY and no LIMIT + /// (MySQLGenericTableHandler.Get passes an empty options), and nothing in the schema forbids + /// duplicates — inventoryfolders has no unique key on (agentID, type). On Legion Grid seven + /// accounts carry two type-46 folders each, and picking the wrong one silently writes an outfit change into a + /// folder no viewer reads. That is the A7 live failure; see Docs/feature/ais-v3/A7-DUPLICATE-COF.md. + /// + /// The rule is **highest Version, lowest id on a tie**. A folder's version is incremented on + /// every child add or remove and never decreases, so the folder the viewer has been writing to is the folder + /// whose version climbed — which is exactly the ground truth we have to match. Creation order cannot be used: + /// the table has no timestamp. Descendant count cannot be used either: a legitimately emptied Current Outfit + /// has none, and emptying it is what "take off the last garment" does. + /// + /// Candidates come from the skeleton rather than from + /// because the skeleton is queried by agent alone and so sees every folder the viewer sees, including one that + /// is not a direct child of the root — which the service's own query structurally cannot return. When the + /// skeleton is unavailable, or holds no folder of this type, the backend's own answer is used unchanged. + /// + public static InventoryFolderBase GetSystemFolder(IAisInventoryBackend backend, UUID agentId, FolderType type) + { + var skeleton = backend.GetInventorySkeleton(agentId); + if (skeleton is null || skeleton.Count == 0) + return backend.GetFolderForType(agentId, type); + + List candidates = null; + foreach (var folder in skeleton) + if (folder.Type == (short)type) + (candidates ??= new List()).Add(folder); + + if (candidates is null) + return backend.GetFolderForType(agentId, type); + if (candidates.Count == 1) + return candidates[0]; + + var chosen = candidates[0]; + foreach (var folder in candidates) + if (folder.Version > chosen.Version || + (folder.Version == chosen.Version && folder.ID.CompareTo(chosen.ID) < 0)) + chosen = folder; + + // An operator has to be able to see this without opening the database, because the visible symptom is an + // outfit change that quietly does not stick. + m_log.LogWarning( + "[AIS]: agent {Agent} has {Count} folders of type {Type} ({Candidates}); using {Chosen} version {Version}. " + + "Duplicate system folders DIRECTLY UNDER THE ROOT are a data fault, not an AIS one - see " + + "Docs/feature/ais-v3/A7-DUPLICATE-COF.md. A second folder of this type inside My Suitcase is " + + "EXPECTED (HGSuitcaseInventoryService.CreateSystemFolders builds a full set there) and is not a " + + "fault; AIS-COF-1 found that every \"duplicate\" Current Outfit on this grid was the suitcase one", + agentId, candidates.Count, type, + string.Join(", ", candidates.Select(f => $"{f.ID} v{f.Version}")), + chosen.ID, chosen.Version); + + return chosen; + } + + /// + /// Folders whose ParentID names a folder that is not in the agent's skeleton — the only orphan class + /// this tree can find without walking every folder's contents. Orphaned **items** are not reported: the + /// inventory service has no query for them and finding them would mean listing every folder + /// (IInventoryService surface, tree state T5). An empty result is therefore "no orphan folders", not + /// "no orphans of any kind", and the route says so in its own documentation. + /// + public static AisOrphans FindOrphans(IAisInventoryBackend backend, UUID agentId) + { + var skeleton = backend.GetInventorySkeleton(agentId); + if (skeleton is null || skeleton.Count == 0) + return new AisOrphans(System.Array.Empty(), System.Array.Empty()); + + var known = new HashSet(); + foreach (var folder in skeleton) known.Add(folder.ID); + var orphans = new List(); + foreach (var folder in skeleton) + if (!folder.ParentID.IsZero() && !known.Contains(folder.ParentID)) orphans.Add(folder); + return new AisOrphans(orphans, System.Array.Empty()); + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisMutation.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisMutation.cs new file mode 100644 index 00000000000..59c67464d30 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisMutation.cs @@ -0,0 +1,270 @@ +using System.Collections.Generic; +using System.Text; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// +/// Applying a PATCH body to an item or a folder, and building the delta envelope the viewer applies +/// (AIS-V3-SPEC.md §1d-bis). Pure: no backend, no I/O, so the field rules are unit-testable on their own. +/// +public static class AisMutation +{ + /// + /// Fields of an item PATCH this tree can store. The viewer sends the item's **whole** asLLSD() map with + /// asset_id/shadow_id swapped for hash_id (A-Q3, llviewerinventory.cpp:435-454 and + /// :1399-1422), so most keys in a body are just the item as the viewer already had it. Anything not + /// listed here is ignored rather than refused — refusing would fail every ordinary rename. + /// + /// + /// hash_id is listed but is not applied here: it is a transaction id, and only the region's + /// asset-transaction module knows which asset it produced. reports it and the + /// handler hands it to (A16). + /// + /// + /// + /// shadow_id is deliberately absent. It is the obfuscated form asLLSD emits instead of + /// asset_id for a restricted-permission item (llinventory.cpp:952-964), and both of the two + /// callers that build an update body erase it before sending (llviewerinventory.cpp:445-452, + /// :1414-1421), so no PATCH this tree can receive from a stock viewer carries one. + /// + /// + public static readonly string[] ItemFields = { "name", "desc", "sale_info", "flags", "asset_id", "hash_id", "permissions" }; + + /// + /// Fields of a category PATCH this tree can store. The viewer sends the category's whole asLLSD() for a + /// rename or a type change, or a single-key {thumbnail} / {favorite} map for a protected folder + /// (llviewerinventory.cpp:651-665, :866-884, :1436-1457). thumbnail and + /// favorite have no column in this tree's inventory, so they are accepted and dropped. + /// + public static readonly string[] CategoryFields = { "name" }; + + /// What a patch actually changed, for the log and for deciding whether to write at all. + /// + /// The body's hash_id, if it carried one. Not a field this class can apply — resolving a transaction + /// id to the asset it uploaded needs the region's asset-transaction module — so it is reported for the + /// handler to hand to (A16). + /// + public sealed record Applied(IReadOnlyList Changed, IReadOnlyList Ignored, UUID Transaction = default) + { + public bool Any => Changed.Count > 0; + } + + /// + /// The permission masks a client may set, each first masked by the item's own BasePermissions. Base + /// and current are never taken from a request body. This is the tree's existing rule for a client-supplied + /// item update, lifted from the legacy UDP path (Scene.Inventory.cs:497-548) so both routes agree. + /// + /// + /// One simplification against that path, deliberate: it also forces full next-owner permissions when the + /// everyone mask gains Export, and denies the change outright for a non-creator. Here the export bit + /// is simply preserved as it stands — masking by base already prevents granting an export the item does not + /// have, and preserving it prevents removing one through a route that was never asked to manage exports. + /// + /// + private static bool ApplyPermissions(OSDMap perms, InventoryItemBase item) + { + const uint Export = (uint)OpenSim.Framework.PermissionMask.Export; // Scene.Inventory.cs uses this one + var next = perms.ContainsKey("next_owner_mask") ? (uint)perms["next_owner_mask"].AsInteger() : item.NextPermissions; + var everyone = perms.ContainsKey("everyone_mask") ? (uint)perms["everyone_mask"].AsInteger() : item.EveryOnePermissions; + var group = perms.ContainsKey("group_mask") ? (uint)perms["group_mask"].AsInteger() : item.GroupPermissions; + + next &= item.BasePermissions; + everyone &= item.BasePermissions; + group &= item.BasePermissions; + everyone = (everyone & ~Export) | (item.EveryOnePermissions & Export); // the export bit stands as it is + + if (next == item.NextPermissions && everyone == item.EveryOnePermissions && group == item.GroupPermissions) + return false; + + item.NextPermissions = next; + item.EveryOnePermissions = everyone; + item.GroupPermissions = group; + return true; + } + + /// + /// Applies the storable fields of to and reports what it did. + /// parent_id is deliberately **not** applied: a move changes two folders' versions and is not part of + /// A2 (see the session's decisions), so a body whose parent differs is treated as an unchanged parent. + /// + public static Applied ApplyToItem(OSDMap body, InventoryItemBase item) + { + var changed = new List(); + var ignored = new List(); + var transaction = UUID.Zero; + foreach (var key in body.Keys) + { + switch (key) + { + case "name": + var name = body["name"].AsString() ?? ""; + if (name != item.Name) { item.Name = name; changed.Add(key); } + break; + case "desc": + var desc = body["desc"].AsString() ?? ""; + if (desc != item.Description) { item.Description = desc; changed.Add(key); } + break; + case "flags": + var flags = (uint)body["flags"].AsInteger(); + if (flags != item.Flags) { item.Flags = flags; changed.Add(key); } + break; + case "asset_id": + // The asset the viewer wants this item to point at. A16: dropping this answered 200 and left + // the wearable pointing at its previous asset, so an outfit edit survived only in the + // viewer's cache until it re-read the item. + var assetId = body["asset_id"].AsUUID(); + if (assetId != item.AssetID) { item.AssetID = assetId; changed.Add(key); } + break; + case "hash_id": + // A transaction id, not an asset id (llviewerinventory.cpp:435-454). Reported, not applied. + transaction = body["hash_id"].AsUUID(); + break; + case "permissions": + if (body["permissions"] is OSDMap perms && ApplyPermissions(perms, item)) changed.Add(key); + break; + case "sale_info": + if (body["sale_info"] is OSDMap sale) + { + var price = sale.ContainsKey("sale_price") ? sale["sale_price"].AsInteger() : item.SalePrice; + var type = sale.ContainsKey("sale_type") ? (byte)sale["sale_type"].AsInteger() : item.SaleType; + if (price != item.SalePrice || type != item.SaleType) + { + item.SalePrice = price; + item.SaleType = type; + changed.Add(key); + } + } + break; + default: + ignored.Add(key); + break; + } + } + return new Applied(changed, ignored, transaction); + } + + /// Applies the storable fields of a category PATCH. type_default is ignored: see the decisions. + public static Applied ApplyToFolder(OSDMap body, InventoryFolderBase folder) + { + var changed = new List(); + var ignored = new List(); + foreach (var key in body.Keys) + { + if (key == "name") + { + var name = body["name"].AsString() ?? ""; + if (name != folder.Name) { folder.Name = name; changed.Add(key); } + } + else ignored.Add(key); + } + return new Applied(changed, ignored); + } + + // ------------------------------------------------------------------ the delta envelope + + public const string CategoriesRemoved = "_categories_removed"; + public const string RemovedItems = "_removed_items"; + public const string UpdatedCategoryVersions = "_updated_category_versions"; + public const string CreatedItems = "_created_items"; + public const string CreatedCategories = "_created_categories"; + public const string CategoryItemsRemoved = "_category_items_removed"; + public const string BrokenLinksRemoved = "_broken_links_removed"; + + /// + /// Every delta key of spec §1d-bis, in the order a reader wants them: what was made, then what went, then the + /// versions that gate both. The response logging walks this, so the log cannot drift from the contract. + /// + public static readonly string[] DeltaKeys = + { + CreatedCategories, CreatedItems, + CategoriesRemoved, RemovedItems, CategoryItemsRemoved, BrokenLinksRemoved, + UpdatedCategoryVersions, + }; + + /// + /// A one-line rendering of a mutation response's deltas, for the DEBUG log. Absent keys are omitted, so a + /// response that reported nothing prints as no deltas — which is the interesting case, because a + /// mutation that changed a folder and said nothing is exactly how a viewer's model goes stale (§1d-bis). + /// + /// Only ever call this inside an IsEnabled(LogLevel.Debug) guard. It allocates a builder + /// and walks the whole envelope; with DEBUG off none of it should run. + /// + public static string SummariseDeltas(OSDMap body) + { + if (body is null || body.Count == 0) return "empty body"; + + var sb = new StringBuilder(); + + // the content object a response carries at top level, when it has one + foreach (var idKey in new[] { "category_id", "item_id" }) + if (body.TryGetValue(idKey, out var id)) + sb.Append(idKey).Append('=').Append(id.AsUUID()).Append(' '); + + foreach (var key in DeltaKeys) + { + if (!body.TryGetValue(key, out var value)) continue; + sb.Append(key).Append('='); + switch (value) + { + case OSDArray array: + sb.Append('['); + for (var i = 0; i < array.Count; i++) + { + if (i > 0) sb.Append(','); + sb.Append(array[i].AsUUID()); + } + sb.Append(']'); + break; + case OSDMap map: // _updated_category_versions: folder -> version + sb.Append('{'); + var first = true; + foreach (string folder in map.Keys) + { + if (!first) sb.Append(','); + first = false; + sb.Append(folder).Append(':').Append(map[folder].AsInteger()); + } + sb.Append('}'); + break; + default: + sb.Append(value.AsString()); + break; + } + sb.Append(' '); + } + + return sb.Length == 0 ? "no deltas" : sb.ToString().TrimEnd(); + } + + /// + /// Adds 's freshly read version to the response's + /// _updated_category_versions. Every mutation must list every folder whose contents it changed, and the + /// zero-delta entries the viewer creates for an update are discarded unless the folder is listed + /// (llaisapi.cpp:1625-1629). Never list a folder the viewer may not have: that is a null dereference in + /// doUpdate (:1760-1762, Ledger A-R6). + /// + public static void ReportVersion(OSDMap response, InventoryFolderBase folder) + { + if (folder is null) return; + if (response[UpdatedCategoryVersions] is not OSDMap versions) + { + versions = new OSDMap(); + response[UpdatedCategoryVersions] = versions; + } + versions[folder.ID.ToString()] = (int)folder.Version; + } + + /// Adds an id to one of the removal arrays, creating it on first use. + public static void ReportRemoved(OSDMap response, string key, UUID id) + { + if (response[key] is not OSDArray array) + { + array = new OSDArray(); + response[key] = array; + } + array.Add(OSD.FromUUID(id)); + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisPurge.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisPurge.cs new file mode 100644 index 00000000000..6207f318bbf --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisPurge.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using OpenMetaverse; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// What a purge removed, and what it could not. +/// Direct sub-folders that are gone. +/// Direct items (links included) that are gone. +/// Direct children still present afterwards; empty on success. +public sealed record PurgeOutcome( + IReadOnlyList RemovedCategories, + IReadOnlyList RemovedItems, + IReadOnlyList Survivors) +{ + public bool Ok => Survivors.Count == 0; +} + +/// +/// DELETE /category/{id}/children — empty a folder, keeping the folder. +/// +/// Who calls it. `purge_descendents_of` (llviewerinventory.cpp:1590-1643), reached from +/// LLInventoryModel::callbackEmptyFolderType and emptyFolderType +/// (llinventorymodel.cpp:4125-4131, :4136-4158) — Empty Trash and Empty Lost and Found. +/// +/// The deltas must be enumerated, unlike a folder delete. A2 established that +/// DELETE /category/{id} need only name the folder, because +/// LLInventoryModel::onObjectDeletedFromServer calls onDescendentsPurgedFromServer for a category +/// and the viewer purges the children locally. That does **not** apply here: the only caller of +/// onDescendentsPurgedFromServer is that one line in onObjectDeletedFromServer +/// (llinventorymodel.cpp:2023), and nothing in llaisapi.cpp calls it. A purge does not delete the +/// folder, so nothing triggers the local sweep and the viewer would keep every child it already had. **The +/// response must therefore list the purged children itself**: direct sub-folders in _categories_removed +/// and direct items in _removed_items. Only the *direct* children need listing — each removed sub-folder +/// goes through onObjectDeletedFromServer in turn, which purges *its* descendents locally. +/// +/// The protected-folder rule does not apply, for the same reason it does not apply to a slam (A3): +/// lookupIsProtectedType governs moving, deleting and retyping a folder — *"you can't move, deleted, or +/// change certain properties such as their type"* (llfoldertype.cpp:151-153) — not emptying it. Trash and +/// Lost and Found are both protected (:97, :99) and are the only folders the verified callers purge. +/// Refusing them would refuse the operation's entire purpose. +/// +public static class AisPurge +{ + /// + /// Empty , and report what actually went by re-reading afterwards. + /// + /// What is composed, and why. IInventoryService.PurgeFolder(folder) is the one-argument + /// form, i.e. onlyIfTrash = true. Its gate is ParentIsTrashOrLost, which returns true when the + /// folder **itself** is Trash or Lost and Found (XInventoryService.cs:522-529), so it covers the two + /// callers the viewer actually has. For any other folder it refuses, so this falls back to composing the purge + /// from what A2b already exposed: DeleteItems for the direct items and + /// DeleteFolders(..., onlyIfTrash: false) for the direct sub-folders, which itself purges each + /// sub-folder recursively before removing it (XInventoryService.cs:459-478). + /// + /// The failure window. A purge is destructive by intent, so there is nothing to roll back to — + /// unlike a slam, a partially purged folder cannot be restored, and pretending otherwise would be a lie. What + /// this does instead is re-read and tell the truth: every child that survived is named in + /// and the route answers an error. A purge is idempotent, so re-issuing it + /// finishes the job; that is the recovery, and it is the client's to make. + /// + public static PurgeOutcome Run(IAisInventoryBackend backend, UUID agentId, InventoryFolderBase folder) + { + var before = AisInventory.GetContents(backend, agentId, folder.ID); + var categoriesBefore = new List(); + var itemsBefore = new List(); + if (before is not null) + { + foreach (var child in before.SubFolders) categoriesBefore.Add(child.ID); + foreach (var item in before.Items) itemsBefore.Add(item.ID); + foreach (var link in before.Links) itemsBefore.Add(link.ID); + } + + // 1. the service's own purge, which covers Trash and Lost and Found + backend.PurgeFolder(folder); + + // 2. whatever it declined to touch, remove with the operations A2b exposed + var after = AisInventory.GetContents(backend, agentId, folder.ID); + if (after is not null && (after.SubFolders.Count > 0 || after.Items.Count > 0 || after.Links.Count > 0)) + { + var remainingItems = new List(); + foreach (var item in after.Items) remainingItems.Add(item.ID); + foreach (var link in after.Links) remainingItems.Add(link.ID); + if (remainingItems.Count > 0) backend.DeleteItems(agentId, remainingItems); + + var remainingFolders = new List(); + foreach (var child in after.SubFolders) remainingFolders.Add(child.ID); + if (remainingFolders.Count > 0) backend.DeleteFolders(agentId, remainingFolders, onlyIfTrash: false); + } + + // 3. report only what really went + var final = AisInventory.GetContents(backend, agentId, folder.ID); + var stillThere = new HashSet(); + if (final is not null) + { + foreach (var child in final.SubFolders) stillThere.Add(child.ID); + foreach (var item in final.Items) stillThere.Add(item.ID); + foreach (var link in final.Links) stillThere.Add(link.ID); + } + + var removedCategories = new List(); + foreach (var id in categoriesBefore) if (!stillThere.Contains(id)) removedCategories.Add(id); + var removedItems = new List(); + foreach (var id in itemsBefore) if (!stillThere.Contains(id)) removedItems.Add(id); + + return new PurgeOutcome(removedCategories, removedItems, new List(stillThere)); + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisRouter.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisRouter.cs new file mode 100644 index 00000000000..5686470073a --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisRouter.cs @@ -0,0 +1,193 @@ +using System.Collections.Generic; +using OpenMetaverse; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// +/// The operations the LL viewer drives against the InventoryAPIv3 / LibraryAPIv3 caps, one per row of +/// Docs/feature/ais-v3/AIS-V3-SPEC.md §1a (viewer llaisapi.h COMMAND_TYPE plus the string-identifier fetch). +/// +public enum AisOperation +{ + /// The verb + path pair is not one the spec defines. + Unknown, + /// POST /category/{parent}?tid= — create categories / items / links under a parent. + CreateInventory, + /// PUT /category/{id}/links?tid= — replace the folder's links. + SlamFolder, + /// DELETE /category/{id}. + RemoveCategory, + /// DELETE /item/{id}. + RemoveItem, + /// DELETE /category/{id}/children. + PurgeDescendents, + /// PATCH /category/{id}. + UpdateCategory, + /// PATCH /item/{id}. + UpdateItem, + /// GET /item/{id}. + FetchItem, + /// GET /category/{id}/children?depth=N (no children= list). + FetchCategoryChildren, + /// GET /category/{id}/categories?depth=N. + FetchCategoryCategories, + /// GET /category/{id}/children?depth=N&children=a,b,c. + FetchCategorySubset, + /// GET /category/current/links — the Current Outfit folder's links. + FetchCOF, + /// GET /category/{id}/links. + FetchCategoryLinks, + /// GET /orphans. + FetchOrphans, + /// COPY /category/{source}?tid=[,depth=0] with a destination (library cap). + CopyCategory, +} + +/// Classification of the operations, kept beside the enum so a new one is hard to forget. +public static class AisOperations +{ + /// + /// True for the operations that change inventory — everything the viewer applies a delta envelope from. The + /// fetches are excluded deliberately: their bodies are whole inventory listings, and logging those would bury + /// the mutations that matter. + /// + public static bool IsMutation(AisOperation operation) => operation switch + { + AisOperation.CreateInventory or + AisOperation.SlamFolder or + AisOperation.RemoveCategory or + AisOperation.RemoveItem or + AisOperation.PurgeDescendents or + AisOperation.UpdateCategory or + AisOperation.UpdateItem or + AisOperation.CopyCategory => true, + _ => false, + }; +} + +/// +/// One parsed request: what the viewer asked for, with the ids and query values the spec defines. Never holds +/// anything scene-bound (Ledger P-2). +/// +public sealed record AisRoute( + AisOperation Operation, + string Verb, + string Path, + /// The category or item id from the path; UUID.Zero for aliases and id-less routes. + UUID Id, + /// The raw path segment the id came from ("current" for the COF alias), or "". + string Identifier, + /// True when the id segment is an alias such as "current" rather than a UUID. + bool IsAlias, + /// tid query value, or UUID.Zero. + UUID Tid, + /// depth query value; -1 when absent. + int Depth, + /// children= ids for FetchCategorySubset; empty otherwise. + IReadOnlyList Children, + /// Every query key and its raw value, for anything the spec does not name (e.g. simulate). + IReadOnlyDictionary Query) +{ + public static readonly AisRoute None = new(AisOperation.Unknown, "", "", UUID.Zero, "", false, UUID.Zero, -1, System.Array.Empty(), new Dictionary()); +} + +/// +/// Parses verb + path + query into an . The path is relative to the cap URL (everything after +/// the cap's own path segment); the parser is tolerant of a leading slash and of the cap prefix being present when +/// is supplied. Pure; unit-tested against every URL shape in AIS-V3-SPEC.md §1a. +/// +public static class AisRouter +{ + public const string CurrentOutfitAlias = "current"; + + public static AisRoute Parse(string verb, string pathAndQuery, string capPath = "") + { + if (string.IsNullOrEmpty(verb) || pathAndQuery is null) return AisRoute.None; + verb = verb.ToUpperInvariant(); + + var q = pathAndQuery.IndexOf('?'); + var path = q >= 0 ? pathAndQuery[..q] : pathAndQuery; + var queryText = q >= 0 ? pathAndQuery[(q + 1)..] : ""; + + if (capPath.Length > 0 && path.StartsWith(capPath, System.StringComparison.OrdinalIgnoreCase)) + path = path[capPath.Length..]; + path = path.Trim('/'); + + var query = ParseQuery(queryText); + var segments = path.Length == 0 ? System.Array.Empty() : path.Split('/', System.StringSplitOptions.RemoveEmptyEntries); + + // tid: the viewer's COPY appends ",depth=0" to the tid value itself (llaisapi.cpp:278) + var tid = UUID.Zero; + var depth = -1; + if (query.TryGetValue("tid", out var tidText)) + { + var comma = tidText.IndexOf(','); + if (comma >= 0) + { + var extra = tidText[(comma + 1)..]; + tidText = tidText[..comma]; + var eq = extra.IndexOf('='); + if (eq > 0) query[extra[..eq]] = extra[(eq + 1)..]; + } + UUID.TryParse(tidText, out tid); + } + if (query.TryGetValue("depth", out var depthText) && int.TryParse(depthText, out var d)) depth = d; + var children = new List(); + if (query.TryGetValue("children", out var childrenText)) + foreach (var part in childrenText.Split(',', System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries)) + if (UUID.TryParse(part, out var cid)) children.Add(cid); + + AisRoute Route(AisOperation op, string identifier, UUID id, bool alias) + => new(op, verb, "/" + path, id, identifier, alias, tid, depth, children, query); + + if (segments.Length == 1 && segments[0].Equals("orphans", System.StringComparison.OrdinalIgnoreCase)) + return verb == "GET" ? Route(AisOperation.FetchOrphans, "", UUID.Zero, false) : Route(AisOperation.Unknown, "", UUID.Zero, false); + + if (segments.Length is < 2 or > 3) return Route(AisOperation.Unknown, "", UUID.Zero, false); + + var collection = segments[0].ToLowerInvariant(); + var identifier = segments[1]; + var isAlias = !UUID.TryParse(identifier, out var objectId); + if (isAlias && !identifier.Equals(CurrentOutfitAlias, System.StringComparison.OrdinalIgnoreCase)) + return Route(AisOperation.Unknown, identifier, UUID.Zero, true); + if (isAlias) objectId = UUID.Zero; + var sub = segments.Length == 3 ? segments[2].ToLowerInvariant() : ""; + + AisOperation op = AisOperation.Unknown; + if (collection == "item" && sub.Length == 0) + { + op = verb switch { "GET" => AisOperation.FetchItem, "DELETE" => AisOperation.RemoveItem, "PATCH" => AisOperation.UpdateItem, _ => AisOperation.Unknown }; + } + else if (collection == "category") + { + op = (verb, sub) switch + { + ("POST", "") => AisOperation.CreateInventory, + ("DELETE", "") => AisOperation.RemoveCategory, + ("PATCH", "") => AisOperation.UpdateCategory, + ("COPY", "") => AisOperation.CopyCategory, + ("PUT", "links") => AisOperation.SlamFolder, + ("DELETE", "children") => AisOperation.PurgeDescendents, + ("GET", "children") => children.Count > 0 ? AisOperation.FetchCategorySubset : AisOperation.FetchCategoryChildren, + ("GET", "categories") => AisOperation.FetchCategoryCategories, + ("GET", "links") => isAlias ? AisOperation.FetchCOF : AisOperation.FetchCategoryLinks, + _ => AisOperation.Unknown, + }; + } + return Route(op, identifier, objectId, isAlias); + } + + private static Dictionary ParseQuery(string queryText) + { + var dict = new Dictionary(System.StringComparer.OrdinalIgnoreCase); + if (queryText.Length == 0) return dict; + foreach (var pair in queryText.Split('&', System.StringSplitOptions.RemoveEmptyEntries)) + { + var eq = pair.IndexOf('='); + var key = eq >= 0 ? pair[..eq] : pair; + var value = eq >= 0 ? pair[(eq + 1)..] : ""; + dict[System.Uri.UnescapeDataString(key)] = System.Uri.UnescapeDataString(value); + } + return dict; + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisSlam.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisSlam.cs new file mode 100644 index 00000000000..ca808b7ff58 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisSlam.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// One link as the viewer's slam body describes it. +public sealed record SlamLink(string Name, string Desc, UUID LinkedId, int AssetType); + +/// +/// What a slam did, so the handler can build the envelope and report failure honestly. +/// +/// The link rows created, in body order. +/// The ids of the links that were there before and have now gone. +/// Null on success; otherwise what went wrong, already compensated for. +/// +/// True when the rollback after a failure could not itself be completed. The folder then holds **more** links +/// than it started with — never fewer — and the ones in that could not be removed are named +/// in . +/// +public sealed record SlamOutcome( + IReadOnlyList Created, + IReadOnlyList Removed, + string Failure, + bool CompensationFailed, + IReadOnlyList Leftover) +{ + public bool Ok => Failure is null; +} + +/// +/// PUT /category/{id}/links — replace a folder's links. +/// +/// The body is a bare LLSD array of link maps, each carrying exactly name, desc, +/// linked_id and type (AT_LINK = 24, or AT_LINK_FOLDER = 25). Both builders agree: +/// LLAppearanceMgr::updateCOF (llappearancemgr.cpp:2209-2245) and +/// LLAppearanceMgr::slamCategoryLinks (:1795-1833). A0's spec guessed a {"links": [...]} +/// wrapper and marked it UNVERIFIED; the real body has no wrapper. This parser accepts the array and, tolerantly, +/// a map carrying the array under links or contents. +/// +/// Atomicity. There is none available: IInventoryService has no transaction and no batch write +/// (tree state T5), so a slam is several independent calls. What this implements instead is an ordering chosen so +/// that the dangerous failure cannot happen — see . +/// +public static class AisSlam +{ + /// + /// Parses a slam body. Returns null when it is not a shape we recognise, and the caller answers 400. + /// + /// AIS-SEC-2: the guarantee is that a body which fails validation produces zero inventory writes. + /// Validation completes before is entered, so a rejected body never reaches an + /// AddItem or a DeleteItems at all. It is not a write that failed; it is a write not attempted. + /// + /// An empty MAP is not an empty slam. That branch used to exist and it is what made the defect + /// reachable: AisHandler produced an empty OSDMap both for a body it could not parse and for no + /// body at all, so a truncated PUT — a dropped connection is enough — was read as "replace every link + /// with none" and emptied the wearer's Current Outfit. The viewer sends a bare LLSD array and never + /// {} (spec A-Q3, llappearancemgr.cpp:2209-2245, :1795-1833), so {} can only be a + /// client we do not know or a body that arrived damaged, and under replacement semantics the safe reading of + /// both is "refuse". An empty array stays an intentional empty slam: that is how the viewer takes off + /// the last garment. + /// + /// All-or-nothing. One bad entry rejects the whole body rather than being skipped. Skipping was + /// the old behaviour and it is the same outfit loss by a quieter route — a slam replaces, so dropping an entry + /// the viewer meant to keep deletes the link it was asking to preserve. A link must carry a non-zero + /// linked_id and a type of AT_LINK or AT_LINK_FOLDER, the only two either builder emits; + /// anything else would be stored as a link row no fetch route knows how to present. + /// + public static IReadOnlyList ParseBody(OSD body) + { + var array = body as OSDArray; + if (array is null && body is OSDMap map) + { + if (map["links"] is OSDArray fromLinks) array = fromLinks; + else if (map["contents"] is OSDArray fromContents) array = fromContents; + } + if (array is null) return null; + + var links = new List(array.Count); + foreach (var entry in array) + { + if (entry is not OSDMap m) return null; + + var type = m.ContainsKey("type") ? m["type"].AsInteger() : (int)OpenMetaverse.AssetType.Link; + if (type != (int)OpenMetaverse.AssetType.Link && type != (int)OpenMetaverse.AssetType.LinkFolder) + return null; + + var linkedId = m["linked_id"].AsUUID(); + if (linkedId.IsZero()) return null; + + links.Add(new SlamLink( + m["name"].AsString() ?? "", + m["desc"].AsString() ?? "", + linkedId, + type)); + } + return links; + } + + /// + /// Replace 's links with . + /// + /// The guarantee, in plain terms. This is not atomic and does not claim to be. The new + /// links are created first and the old ones removed second, so the folder never passes through a + /// state with fewer links than it started with — the failure that would strip an avatar's Current Outfit + /// cannot occur. Concretely: + /// + /// If a creation fails, every link created so far is deleted again and the folder is left exactly as + /// it was. The response is an error. + /// If that rollback itself fails, the folder keeps the links that could not be removed: it holds + /// more than it started with, never fewer, and the outcome says so. The avatar shows duplicates until + /// the next slam, which is recoverable; a stripped outfit would not be. + /// If a removal fails after every creation succeeded, the folder holds the old links as well as the + /// new. The response is an error and the client's next slam corrects it. Nothing is lost. + /// + /// The window that remains: between the last creation and the last removal the folder holds both + /// sets, so a bake or a fetch racing the slam sees duplicates. Closing that needs a transactional or batch + /// write on IInventoryService, which does not exist (Ledger A-R2). + /// + public static SlamOutcome Run(IAisInventoryBackend backend, UUID agentId, UUID folderId, + IReadOnlyList existingLinks, IReadOnlyList wanted) + { + var created = new List(wanted.Count); + + // resolve the targets once, so a link carries its target's inventory type + var targetIds = new List(); + foreach (var link in wanted) + if (!link.LinkedId.IsZero() && !targetIds.Contains(link.LinkedId)) targetIds.Add(link.LinkedId); + var targets = new Dictionary(); + if (targetIds.Count > 0) + foreach (var item in backend.GetItems(agentId, targetIds) ?? Array.Empty()) + if (item is not null) targets[item.ID] = item; + + // 1. create everything the body asked for + foreach (var link in wanted) + { + var row = new InventoryItemBase(UUID.Random(), agentId) + { + Folder = folderId, + Name = link.Name, + Description = link.Desc, + AssetID = link.LinkedId, + AssetType = link.AssetType, + InvType = targets.TryGetValue(link.LinkedId, out var target) ? target.InvType : 0, + CreatorId = agentId.ToString(), + CreationDate = (int)Util.UnixTimeSinceEpoch(), + BasePermissions = (uint)OpenSim.Framework.PermissionMask.All, + CurrentPermissions = (uint)OpenSim.Framework.PermissionMask.All, + EveryOnePermissions = 0, + NextPermissions = (uint)OpenSim.Framework.PermissionMask.All, + GroupPermissions = 0, + Flags = 0, + }; + + if (backend.AddItem(row)) { created.Add(row); continue; } + + // 2. a creation failed: undo the ones already made and leave the folder as it was + var leftover = Rollback(backend, agentId, created); + return new SlamOutcome(Array.Empty(), Array.Empty(), + $"could not create the link to {link.LinkedId}; the folder was left unchanged", leftover.Count > 0, leftover); + } + + // 3. every creation succeeded: now remove what was there before + var oldIds = new List(existingLinks.Count); + foreach (var link in existingLinks) oldIds.Add(link.ID); + if (oldIds.Count > 0 && !backend.DeleteItems(agentId, oldIds)) + return new SlamOutcome(created, Array.Empty(), + "the new links were created but the previous ones could not be removed; the folder holds both sets", + false, Array.Empty()); + + return new SlamOutcome(created, oldIds, null, false, Array.Empty()); + } + + /// Deletes the links a failed slam had already created. Returns the ones it could not remove. + private static IReadOnlyList Rollback(IAisInventoryBackend backend, UUID agentId, List created) + { + if (created.Count == 0) return Array.Empty(); + var ids = new List(created.Count); + foreach (var row in created) ids.Add(row.ID); + return backend.DeleteItems(agentId, ids) ? Array.Empty() : ids; + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisWornAssets.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisWornAssets.cs new file mode 100644 index 00000000000..01eac3591f7 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisWornAssets.cs @@ -0,0 +1,53 @@ +using OpenMetaverse; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// +/// S9. Editing a worn wearable changes the item's asset and nothing else the region watches: the worn SET is +/// unchanged (the viewer keeps the item id - llagentwearables.cpp:319 setItemID(old_item_id)), so no +/// AgentIsNowWearing follows, and while the viewer does schedule an UpdateAvatarAppearance POST it is +/// deferred behind pending uploads (llappearancemgr.cpp:3849) and can arrive minutes later carrying a +/// cof_version the COF has already moved past - which is what was observed on 2026-09-05, one POST at +/// 20:57:40 refused as stale for four edits between 20:30 and 20:53. +/// +/// +/// So the region learns of the change at exactly one reliable moment: the AIS UpdateItem that carries the +/// new asset. This is the rule applied there. It is a pure function of the appearance and the item so it can be +/// tested without a scene; the module wires it to QueueAppearanceSave. +/// +/// +public static class AisWornAssets +{ + /// + /// Point a worn wearable at its new asset. Returns true when the item is worn and the asset actually + /// differs - the only case that is worth a save, and therefore the only case that can cost a bake. + /// + /// + /// An item that is not worn changes nothing here: an edit to something in a drawer must not queue an + /// appearance save. An item that is worn but already carries this asset changes nothing either, so a repeated + /// or replayed PATCH is free. + /// + /// + public static bool ApplyTo(AvatarAppearance appearance, UUID itemId, UUID newAssetId) + { + if (appearance is null || itemId.IsZero() || newAssetId.IsZero()) return false; + + AvatarWearable[] worn = appearance.Wearables; + if (worn is null) return false; + + for (var slot = 0; slot < worn.Length; slot++) + { + AvatarWearable w = worn[slot]; + if (w is null) continue; + for (var j = 0; j < w.Count; j++) + { + if (w[j].ItemID != itemId) continue; + if (w[j].AssetID == newAssetId) return false; // already current + w.Add(itemId, newAssetId); // Add updates in place for a known item + return true; + } + } + return false; + } +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/IAisInventoryBackend.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/IAisInventoryBackend.cs new file mode 100644 index 00000000000..d64c2322b67 --- /dev/null +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/IAisInventoryBackend.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using OpenMetaverse; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS; + +/// +/// Everything the AIS v3 routes need from inventory, and nothing else. The handler is written against this +/// interface only (Ledger P-2): no Scene, no ScenePresence. Phase 1 implements it over the region's +/// IInventoryService; Phase 2 hosts the same handler on Robust over the service directly. +/// +/// A0 defines the surface; every member is implemented in A1+. Links are ordinary +/// rows with AssetType.Link / AssetType.LinkFolder; the handler splits them out of item lists into +/// the _embedded.links collection (spec §1c), and resolves their targets with +/// (tree state T5: the service has no link-aware fetch). +/// +public interface IAisInventoryBackend +{ + /// The agent's folder of a system type (spec §1b: "current" = FolderType.CurrentOutfit); null if absent. + InventoryFolderBase GetFolderForType(UUID agentId, FolderType type); + + /// A folder with its current Version freshly read (tree state T4); null if absent or not the agent's. + InventoryFolderBase GetFolder(UUID agentId, UUID folderId); + + /// A folder's direct children: sub-folders and items (links included in Items), with the folder's version. + InventoryCollection GetFolderContent(UUID agentId, UUID folderId); + + /// A folder's direct sub-folders only (GET /category/{id}/categories). Empty when the folder is absent. + IReadOnlyList GetSubFolders(UUID agentId, UUID folderId); + + /// + /// Every folder the agent owns, parents included — the inventory skeleton + /// (IInventoryService.GetInventorySkeleton). Used only to find folders whose parent no longer exists + /// (GET /orphans); there is no cheaper orphan query in the service. + /// + IReadOnlyList GetInventorySkeleton(UUID agentId); + + /// Items by id, e.g. link targets; absent ids are simply missing from the result. + IReadOnlyList GetItems(UUID agentId, IReadOnlyList itemIds); + + /// One item (or link) by id; null if absent or not the agent's. + InventoryItemBase GetItem(UUID agentId, UUID itemId); + + /// Create a folder under its ParentID. The data layer bumps the parent's version (S0a V6). + bool AddFolder(InventoryFolderBase folder); + + /// Create an item or link under its Folder. Bumps the parent's version (S0a V6). + bool AddItem(InventoryItemBase item); + + /// Update an item's mutable fields (name, description, flags, asset, permissions). + bool UpdateItem(InventoryItemBase item); + + /// + /// Resolve a hash_id — an asset transaction id — to the asset that transaction uploaded, apply it to + /// and store the item (A16). + /// + /// + /// This is the one thing the AIS routes cannot do through IInventoryService alone. A wearable save + /// uploads its asset over the xfer protocol under a transaction id and then PATCHes the item, and the viewer + /// sends the transaction id rather than the asset id: LLViewerInventoryItem::updateServer + /// (llviewerinventory.cpp:435-454) erases asset_id and shadow_id from the body and puts + /// hash_id in their place. Only the region's asset-transaction module knows which asset that + /// transaction produced, so the region backend hands the pair to it, exactly as the legacy UDP path does + /// (Scene.Inventory.cs:579-582). + /// + /// + /// + /// A19: the answer is a three-way , not a bool. NotResolvable is what + /// a backend that resolves no transactions at all returns — the library, or a region with no transaction + /// module or no connected client for the agent — and the item's asset is simply left alone. Applied + /// means the transaction was handed over; the module stores the item itself once the xfer completes, so the + /// caller must re-read the item rather than trust the copy it passed in. Refused means the region + /// validated the uploaded asset and said no, which is a failed save. + /// + /// + AisAssetTransaction ApplyAssetTransaction(UUID agentId, UUID transactionId, InventoryItemBase item); + + /// + /// A worn wearable's asset just changed (S9). The region points the presence's wearable at the new asset and + /// queues an appearance save; the save resolves every worn item afresh, persists the result and raises the + /// S5 change trigger, and the bake's own input hash then decides whether anything is recomputed - so an edit + /// that changed nothing visible costs one hash check per channel and no compositing. + /// + /// + /// This exists because it is the only reliable signal. Editing a wearable leaves the worn set unchanged, so + /// no AgentIsNowWearing follows, and the viewer's UpdateAvatarAppearance POST is deferred behind + /// pending uploads and can arrive stale. Backends with no presence to update - the library, Phase 2 on + /// Robust - do nothing. + /// + /// + void OnItemAssetChanged(UUID agentId, UUID itemId, UUID newAssetId); + + /// Update a folder's mutable fields (name, type, parent on move). + bool UpdateFolder(InventoryFolderBase folder); + + /// Delete items (and links) by id. Bumps each parent's version (S0a V6). + bool DeleteItems(UUID agentId, IReadOnlyList itemIds); + + /// + /// Delete folders by id, recursively. is the inventory service's Trash + /// restriction: with it true a folder outside Trash or Lost And Found is silently skipped and the call still + /// succeeds. AIS passes **false** — the LL viewer deletes any non-protected folder wherever it sits — which is + /// why IInventoryService gained the three-argument overload in A2b (Ledger A-Q9). + /// + bool DeleteFolders(UUID agentId, IReadOnlyList folderIds, bool onlyIfTrash); + + /// Delete a folder's contents but keep the folder (AIS PurgeDescendents). + bool PurgeFolder(InventoryFolderBase folder); +} + +/// +/// A19. What became of a hash_id. The three states have to be distinct because two of them are fine and +/// one is a failed save, and the bool this replaced could not tell them apart - which is how a refused wearable +/// update came to be answered 200. +/// +public enum AisAssetTransaction +{ + /// The asset was applied to the item, or the xfer is still in flight and will apply it when it lands. + Applied, + + /// + /// Nothing to apply and nothing refused: this backend resolves no transactions at all (the library, or a + /// region with no asset-transaction module), or the agent has no client here. The rest of the PATCH stands + /// and the cap still answers 200 - an unknown transaction id is not an error, the uploader opens a + /// pending xfer for it (AgentAssetsTransactions.cs:68-90). + /// + NotResolvable, + + /// + /// The region validated the uploaded asset and refused it, so the asset was not stored and the item + /// still points where it did. This is a failed save and the cap must say so. + /// + Refused, +} diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/PluginRegistration.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/PluginRegistration.cs index 2648e5b7eee..ea8856fd1db 100644 --- a/Source/OpenSim.Region.ClientStack.LindenCaps/PluginRegistration.cs +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/PluginRegistration.cs @@ -34,6 +34,7 @@ public class PluginRegistration : IPluginRegistryProvider public void RegisterPlugins(PluginRegistry registry) { registry.Register("/OpenSim/RegionModules", new PluginDescriptor("AgentPreferencesModule", typeof(AgentPreferencesModule), "AgentPreferencesModule", "0.9")); + registry.Register("/OpenSim/RegionModules", new PluginDescriptor("AISv3Module", typeof(OpenSim.Region.ClientStack.LindenCaps.AIS.AISv3Module), "AISv3Module", "0.9")); registry.Register("/OpenSim/RegionModules", new PluginDescriptor("AvatarPickerSearchModule", typeof(AvatarPickerSearchModule), "AvatarPickerSearchModule", "0.9")); registry.Register("/OpenSim/RegionModules", new PluginDescriptor("BunchOfCapsModule", typeof(BunchOfCapsModule), "BunchOfCapsModule", "0.9")); registry.Register("/OpenSim/RegionModules", new PluginDescriptor("EventQueueGetModule", typeof(EventQueueGetModule), "EventQueueGetModule", "0.9")); diff --git a/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AgentAssetsTransactions.cs b/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AgentAssetsTransactions.cs index 3d611ac1a49..8bc13c49788 100644 --- a/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AgentAssetsTransactions.cs +++ b/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AgentAssetsTransactions.cs @@ -185,7 +185,8 @@ public void RequestUpdateTaskInventoryItem(IClientAPI remoteClient, uploader.RequestUpdateTaskInventoryItem(remoteClient, item); } - public void RequestUpdateInventoryItem(IClientAPI remoteClient, + /// A19: false when the update was refused; see . + public bool RequestUpdateInventoryItem(IClientAPI remoteClient, UUID transactionID, InventoryItemBase item) { AssetXferUploader uploader = RequestXferUploader(transactionID); @@ -201,6 +202,6 @@ public void RequestUpdateInventoryItem(IClientAPI remoteClient, uploader.SetOldData(oldAsset.Data); } - uploader.RequestUpdateInventoryItem(remoteClient, item); + return uploader.RequestUpdateInventoryItem(remoteClient, item); } } diff --git a/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AssetTransactionModule.cs b/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AssetTransactionModule.cs index 4abb097e6c8..cc9f28eb492 100644 --- a/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AssetTransactionModule.cs +++ b/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AssetTransactionModule.cs @@ -181,7 +181,8 @@ public bool HandleItemCreationFromTransaction(IClientAPI remoteClient, /// /// /// - public void HandleItemUpdateFromTransaction(IClientAPI remoteClient, + /// A19: false when the update was refused; see . + public bool HandleItemUpdateFromTransaction(IClientAPI remoteClient, UUID transactionID, InventoryItemBase item) { // m_log.LogDebug( @@ -190,7 +191,7 @@ public void HandleItemUpdateFromTransaction(IClientAPI remoteClient, AgentAssetTransactions transactions = GetUserTransactions(remoteClient.AgentId); - transactions.RequestUpdateInventoryItem(remoteClient, transactionID, item); + return transactions.RequestUpdateInventoryItem(remoteClient, transactionID, item); } /// diff --git a/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AssetXferUploader.cs b/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AssetXferUploader.cs index 668f399042c..5adf133ad08 100644 --- a/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AssetXferUploader.cs +++ b/Source/OpenSim.Region.CoreModules/Agent/AssetTransaction/AssetXferUploader.cs @@ -333,7 +333,12 @@ public void RequestCreateInventoryItem(IClientAPI remoteClient, } } - public void RequestUpdateInventoryItem(IClientAPI remoteClient, InventoryItemBase item) + /// + /// A19: returns false when the update was refused - said no, so the asset + /// was not stored and the item was not repointed. When the xfer has not completed yet there is nothing to + /// validate and this returns true; the refusal, if one comes, arrives on the alert-message path as before. + /// + public bool RequestUpdateInventoryItem(IClientAPI remoteClient, InventoryItemBase item) { // We must lock to avoid a race with a separate thread uploading the asset. lock (this) @@ -355,7 +360,7 @@ public void RequestUpdateInventoryItem(IClientAPI remoteClient, InventoryItemBas if (m_uploadState == UploadState.Complete) { - CompleteItemUpdate(item); + return CompleteItemUpdate(item); } else { @@ -378,6 +383,9 @@ public void RequestUpdateInventoryItem(IClientAPI remoteClient, InventoryItemBas m_updateItemData = item; } } + + // Still uploading: nothing has been validated, so this is not a refusal. + return true; } public void RequestUpdateTaskInventoryItem(IClientAPI remoteClient, TaskInventoryItem taskItem) @@ -507,6 +515,34 @@ private bool CompleteCreateItem(uint callbackID) return true; } + /// + /// A19: whether a texture is one the grid's library holds with full rights, and may therefore be referenced by + /// anyone's wearable. The owner is read off ILibraryService.LibraryRootFolder.Owner rather than + /// hardcoded, so a grid supplying its own library owner still works - the same rule + /// AISv3Module.LibraryOwnerOf uses. Returns false when the region has no library service, which leaves + /// the previous behaviour exactly as it was. + /// + private bool IsLibraryTexture(UUID textureId, uint requiredPerms) + => IsLibraryTexture( + textureId, + m_Scene?.LibraryService?.LibraryRootFolder?.Owner ?? UUID.Zero, + requiredPerms, + (owner, asset) => m_Scene.InventoryService.GetAssetPermissions(owner, asset)); + + /// + /// The rule itself, with the two lookups injected so it is a plain unit test: no Scene, no inventory + /// service. A texture is acceptable when the LIBRARY owner holds it with the same full rights the resident + /// would have needed. Anything else - another resident's texture, a texture nobody holds - is unchanged. + /// + public static bool IsLibraryTexture(UUID textureId, UUID libraryOwner, uint requiredPerms, Func permissionsOf) + { + if (libraryOwner.IsZero() || permissionsOf is null) + return false; + + uint perms = (uint)permissionsOf(libraryOwner, textureId); + return (perms & requiredPerms) == requiredPerms; + } + private uint ValidateAssets() { uint retPerms = 0x7fffffff; @@ -567,6 +603,19 @@ private uint ValidateAssets() if ((perms & texturesfullPermMask) != texturesfullPermMask) { + // A19: a LIBRARY texture is not the resident's and never will be, and asking + // GetAssetPermissions for it under the resident's id returns nothing - so the check + // above rejects every wearable built on one. SL allows library textures in + // wearables, the library is readable by everyone by construction, and this grid + // ships one; a resident who picks a library texture in the appearance editor got a + // 200 and a silently unsaved item. Ask again as the library's owner: a texture that + // resolves there with the same full rights is one anybody may reference. + if (IsLibraryTexture(tx, texturesfullPermMask)) + { + textures--; + continue; + } + m_log.LogError("[ASSET UPLOADER]: REJECTED update with texture {0} from {1} because they do not own the texture", tx, ourClient.AgentId); return 0; } diff --git a/Source/OpenSim.Region.CoreModules/Framework/Library/LocalInventoryService.cs b/Source/OpenSim.Region.CoreModules/Framework/Library/LocalInventoryService.cs index d5568a3afb2..c6674c6d261 100644 --- a/Source/OpenSim.Region.CoreModules/Framework/Library/LocalInventoryService.cs +++ b/Source/OpenSim.Region.CoreModules/Framework/Library/LocalInventoryService.cs @@ -201,6 +201,7 @@ public bool AddItem(InventoryItemBase item) /// true if the item was successfully deleted //bool DeleteItem(InventoryItemBase item); public bool DeleteFolders(UUID userID, List folderIDs) { return false; } + public bool DeleteFolders(UUID userID, List folderIDs, bool onlyIfTrash) { return false; } /// /// Purge an inventory folder of all its items and subfolders. diff --git a/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/HGInventoryBroker.cs b/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/HGInventoryBroker.cs index 327afedd180..f82fcef9108 100644 --- a/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/HGInventoryBroker.cs +++ b/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/HGInventoryBroker.cs @@ -438,6 +438,23 @@ public bool DeleteFolders(UUID ownerID, List folderIDs) return connector.DeleteFolders(ownerID, folderIDs); } + public bool DeleteFolders(UUID ownerID, List folderIDs, bool onlyIfTrash) + { + if (folderIDs == null) + return false; + if (folderIDs.Count == 0) + return false; + + string invURL = GetInventoryServiceURL(ownerID); + + if (invURL is null) // not there, forward to local inventory connector to resolve + return m_LocalGridInventoryService.DeleteFolders(ownerID, folderIDs, onlyIfTrash); + + IInventoryService connector = GetConnector(invURL); + + return connector.DeleteFolders(ownerID, folderIDs, onlyIfTrash); + } + public bool MoveFolder(InventoryFolderBase folder) { if (folder == null) diff --git a/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/LocalInventoryServiceConnector.cs b/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/LocalInventoryServiceConnector.cs index c880cecac64..affe1d86d42 100644 --- a/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/LocalInventoryServiceConnector.cs +++ b/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/LocalInventoryServiceConnector.cs @@ -235,6 +235,11 @@ public bool DeleteFolders(UUID ownerID, List folderIDs) return m_InventoryService.DeleteFolders(ownerID, folderIDs); } + public bool DeleteFolders(UUID ownerID, List folderIDs, bool onlyIfTrash) + { + return m_InventoryService.DeleteFolders(ownerID, folderIDs, onlyIfTrash); + } + /// /// Purge an inventory folder of all its items and subfolders. /// diff --git a/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/RemoteXInventoryServiceConnector.cs b/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/RemoteXInventoryServiceConnector.cs index 0612bd2bc28..85a5acbe030 100644 --- a/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/RemoteXInventoryServiceConnector.cs +++ b/Source/OpenSim.Region.CoreModules/ServiceConnectorsOut/Inventory/RemoteXInventoryServiceConnector.cs @@ -243,6 +243,16 @@ public bool DeleteFolders(UUID ownerID, List folderIDs) return m_RemoteConnector.DeleteFolders(ownerID, folderIDs); } + public bool DeleteFolders(UUID ownerID, List folderIDs, bool onlyIfTrash) + { + if (folderIDs == null) + return false; + if (folderIDs.Count == 0) + return false; + + return m_RemoteConnector.DeleteFolders(ownerID, folderIDs, onlyIfTrash); + } + public bool PurgeFolder(InventoryFolderBase folder) { diff --git a/Source/OpenSim.Region.Framework/Interfaces/IAgentAssetTransactions.cs b/Source/OpenSim.Region.Framework/Interfaces/IAgentAssetTransactions.cs index 47fe4fa61be..6f8e3d89f58 100644 --- a/Source/OpenSim.Region.Framework/Interfaces/IAgentAssetTransactions.cs +++ b/Source/OpenSim.Region.Framework/Interfaces/IAgentAssetTransactions.cs @@ -33,7 +33,17 @@ namespace OpenSim.Region.Framework.Interfaces; public interface IAgentAssetTransactions { - void HandleItemUpdateFromTransaction(IClientAPI remoteClient, UUID transactionID, + /// + /// Apply the asset an xfer transaction uploaded to an inventory item. + /// + /// + /// A19: false when the update was refused - the referenced assets did not validate, so the asset was not + /// stored and the item still points at what it pointed at before. It returns true when the update was applied + /// and also when the xfer is still in flight, because at that point nothing has been validated and the region + /// cannot honestly say the save failed; the caller learns of a later refusal the same way the legacy route + /// does, through the alert and the bulk inventory update the uploader sends. + /// + bool HandleItemUpdateFromTransaction(IClientAPI remoteClient, UUID transactionID, InventoryItemBase item); bool HandleItemCreationFromTransaction(IClientAPI remoteClient, UUID transactionID, UUID folderID, diff --git a/Source/OpenSim.Server.Handlers/Inventory/XInventoryInConnector.cs b/Source/OpenSim.Server.Handlers/Inventory/XInventoryInConnector.cs index a236fe09eeb..ee64da0d34c 100644 --- a/Source/OpenSim.Server.Handlers/Inventory/XInventoryInConnector.cs +++ b/Source/OpenSim.Server.Handlers/Inventory/XInventoryInConnector.cs @@ -459,7 +459,15 @@ byte[] HandleDeleteFolders(Dictionary request) uuids.Add(u); } - if (m_InventoryService.DeleteFolders(principal, uuids)) + // Absent means true, so an older simulator that does not send the field keeps the trash-only behaviour. + bool onlyIfTrash = true; + if (request.TryGetValue("ONLYIFTRASH", out object flag)) + { + string s = flag?.ToString(); + onlyIfTrash = !(s == "0" || string.Equals(s, "false", StringComparison.OrdinalIgnoreCase)); + } + + if (m_InventoryService.DeleteFolders(principal, uuids, onlyIfTrash)) return SuccessResult(); else return diff --git a/Source/OpenSim.Services.AISv3/Controllers/WeatherForecastController.cs b/Source/OpenSim.Services.AISv3/Controllers/WeatherForecastController.cs deleted file mode 100644 index 6c8685bb31f..00000000000 --- a/Source/OpenSim.Services.AISv3/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace OpenSim.Services.AISv3.Controllers; - -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} diff --git a/Source/OpenSim.Services.AISv3/OpenSim.Services.AISv3.csproj b/Source/OpenSim.Services.AISv3/OpenSim.Services.AISv3.csproj deleted file mode 100644 index 56d0f5a53f8..00000000000 --- a/Source/OpenSim.Services.AISv3/OpenSim.Services.AISv3.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - enable - enable - - - - - - - - diff --git a/Source/OpenSim.Services.AISv3/OpenSim.Services.AISv3.http b/Source/OpenSim.Services.AISv3/OpenSim.Services.AISv3.http deleted file mode 100644 index 92ffc34a0e4..00000000000 --- a/Source/OpenSim.Services.AISv3/OpenSim.Services.AISv3.http +++ /dev/null @@ -1,6 +0,0 @@ -@OpenSim.Services.AISv3_HostAddress = http://localhost:5126 - -GET {{OpenSim.Services.AISv3_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/Source/OpenSim.Services.AISv3/Program.cs b/Source/OpenSim.Services.AISv3/Program.cs deleted file mode 100644 index 48863a6d6cd..00000000000 --- a/Source/OpenSim.Services.AISv3/Program.cs +++ /dev/null @@ -1,25 +0,0 @@ -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. - -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseHttpsRedirection(); - -app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); diff --git a/Source/OpenSim.Services.AISv3/WeatherForecast.cs b/Source/OpenSim.Services.AISv3/WeatherForecast.cs deleted file mode 100644 index 9ab4ba00628..00000000000 --- a/Source/OpenSim.Services.AISv3/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace OpenSim.Services.AISv3; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} diff --git a/Source/OpenSim.Services.AISv3/appsettings.Development.json b/Source/OpenSim.Services.AISv3/appsettings.Development.json deleted file mode 100644 index 0c208ae9181..00000000000 --- a/Source/OpenSim.Services.AISv3/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/Source/OpenSim.Services.AISv3/appsettings.json b/Source/OpenSim.Services.AISv3/appsettings.json deleted file mode 100644 index 10f68b8c8b4..00000000000 --- a/Source/OpenSim.Services.AISv3/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/Source/OpenSim.Services.Connectors/Inventory/XInventoryServicesConnector.cs b/Source/OpenSim.Services.Connectors/Inventory/XInventoryServicesConnector.cs index f054b1997b0..c683f0ec4eb 100644 --- a/Source/OpenSim.Services.Connectors/Inventory/XInventoryServicesConnector.cs +++ b/Source/OpenSim.Services.Connectors/Inventory/XInventoryServicesConnector.cs @@ -369,18 +369,32 @@ public bool MoveFolder(InventoryFolderBase folder) } public bool DeleteFolders(UUID principalID, List folderIDs) + { + return DeleteFolders(principalID, folderIDs, true); + } + + /// + /// ONLYIFTRASH is sent only when it is false, so an older Robust that does not know the field keeps its + /// current trash-only behaviour and a newer one honours the flag (XInventoryInConnector defaults it to true + /// when absent). + /// + public bool DeleteFolders(UUID principalID, List folderIDs, bool onlyIfTrash) { List slist = []; foreach (UUID f in folderIDs) slist.Add(f.ToString()); - Dictionary ret = MakeRequest( - new Dictionary { - { "METHOD", "DELETEFOLDERS"}, - { "PRINCIPAL", principalID.ToString() }, - { "FOLDERS", slist } - }); + Dictionary request = new() + { + { "METHOD", "DELETEFOLDERS"}, + { "PRINCIPAL", principalID.ToString() }, + { "FOLDERS", slist } + }; + if (!onlyIfTrash) + request["ONLYIFTRASH"] = "0"; + + Dictionary ret = MakeRequest(request); return CheckReturn(ret); } diff --git a/Source/OpenSim.Services.HypergridService/HGInventoryService.cs b/Source/OpenSim.Services.HypergridService/HGInventoryService.cs index ff7577ed346..28efa312f54 100644 --- a/Source/OpenSim.Services.HypergridService/HGInventoryService.cs +++ b/Source/OpenSim.Services.HypergridService/HGInventoryService.cs @@ -227,6 +227,12 @@ public override bool DeleteFolders(UUID principalID, List folderIDs) return false; } + public override bool DeleteFolders(UUID principalID, List folderIDs, bool onlyIfTrash) + { + // NOGO, whatever the flag says + return false; + } + public override bool PurgeFolder(InventoryFolderBase folder) { // NOGO diff --git a/Source/OpenSim.Services.HypergridService/HGSuitcaseInventoryService.cs b/Source/OpenSim.Services.HypergridService/HGSuitcaseInventoryService.cs index acc56b4fe3f..2bf1a1a7d8c 100644 --- a/Source/OpenSim.Services.HypergridService/HGSuitcaseInventoryService.cs +++ b/Source/OpenSim.Services.HypergridService/HGSuitcaseInventoryService.cs @@ -332,6 +332,12 @@ public override bool DeleteFolders(UUID principalID, List folderIDs) return false; } + public override bool DeleteFolders(UUID principalID, List folderIDs, bool onlyIfTrash) + { + // NOGO, whatever the flag says + return false; + } + public override bool PurgeFolder(InventoryFolderBase folder) { // NOGO diff --git a/Source/OpenSim.Services.Interfaces/IInventoryService.cs b/Source/OpenSim.Services.Interfaces/IInventoryService.cs index af61e0d98ba..6294747036c 100644 --- a/Source/OpenSim.Services.Interfaces/IInventoryService.cs +++ b/Source/OpenSim.Services.Interfaces/IInventoryService.cs @@ -120,6 +120,20 @@ public interface IInventoryService //bool DeleteItem(InventoryItemBase item); bool DeleteFolders(UUID userID, List folderIDs); + /// + /// Delete folders, optionally without the Trash restriction. + /// + /// + /// The two-argument overload above is = true: it silently skips any folder + /// whose parent is not Trash or Lost And Found, and still returns true. That is the right default for the + /// legacy UDP paths, but AIS v3 DELETE /category/{id} must delete any non-protected folder wherever it + /// sits, because that is what the LL viewer asks for. This overload exposes the behaviour + /// XInventoryService has always implemented. Add-only: the two-argument signature and every existing + /// caller are unchanged. + /// + /// When true, only folders under Trash or Lost And Found are deleted. + bool DeleteFolders(UUID userID, List folderIDs, bool onlyIfTrash); + /// /// Purge an inventory folder of all its items and subfolders. /// diff --git a/Source/OpenSim.Services.InventoryService/XInventoryService.cs b/Source/OpenSim.Services.InventoryService/XInventoryService.cs index 708e4c39832..0e93dba1508 100644 --- a/Source/OpenSim.Services.InventoryService/XInventoryService.cs +++ b/Source/OpenSim.Services.InventoryService/XInventoryService.cs @@ -95,7 +95,48 @@ public XInventoryService(IConfigSource config, string configName) : base(config) throw new Exception("Could not find a storage interface in the given module"); } + /// + /// AIS-COF-1. Serialised per principal, because two overlapping calls for the same agent both read + /// "missing" and both create. is right that nothing inside it can close + /// that window; a lock outside it can. + /// + /// This closes the race for a single Robust instance only. Legion Grid runs one, so it is closed + /// there. A multi-instance or multi-simulator deployment that calls this concurrently from two processes is + /// still exposed, and for those the safety net is , which reports + /// the damage rather than preventing it. Only a unique constraint would prevent it, and the suitcase makes + /// (agentID, type) unavailable - see the remarks on that method. + /// + /// Striped rather than per-UUID: a fixed array of locks cannot leak and needs no cleanup, where a + /// dictionary of semaphores has to be reference-counted or it grows for the lifetime of the process. Two + /// principals sharing a stripe serialise against each other for the duration of one inventory creation, + /// which costs nothing that matters and can never be wrong. + /// public virtual bool CreateUserInventory(UUID principalID) + { + lock (CreateLockFor(principalID)) + return CreateUserInventoryLocked(principalID); + } + + /// The number of lock stripes. A power of two, and far more than the concurrent logins this serves. + private const int CreateLockStripes = 64; + + /// + /// Process-wide, and static deliberately: a region connector and the local service can both hold an + /// , and a per-instance lock would not serialise between them. + /// + private static readonly object[] s_createLocks = CreateStripes(); + + private static object[] CreateStripes() + { + var locks = new object[CreateLockStripes]; + for (int i = 0; i < locks.Length; i++) locks[i] = new object(); + return locks; + } + + private static object CreateLockFor(UUID principalID) + => s_createLocks[(principalID.GetHashCode() & int.MaxValue) % CreateLockStripes]; + + private bool CreateUserInventoryLocked(UUID principalID) { // This is braindeaad. We can't ever communicate that we fixed // an existing inventory. Well, just return root folder status, @@ -113,67 +154,145 @@ public virtual bool CreateUserInventory(UUID principalID) XInventoryFolder[] sysFolders = GetSystemFolders(principalID, rootFolder.ID); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Animation)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Animation, "Animations"); + WarnOnDuplicateSystemFolders(principalID, sysFolders); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.BodyPart)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.BodyPart, "Body Parts"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Animation, "Animations"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.CallingCard)) + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.BodyPart, "Body Parts"); + + XInventoryFolder callingCards = EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.CallingCard, "Calling Cards"); + if (callingCards is not null) { - XInventoryFolder folder = CreateFolder(principalID, rootFolder.ID, (int)FolderType.CallingCard, "Calling Cards"); - folder = CreateFolder(principalID, folder.folderID, (int)FolderType.CallingCard, "Friends"); + XInventoryFolder folder = CreateFolder(principalID, callingCards.folderID, (int)FolderType.CallingCard, "Friends"); CreateFolder(principalID, folder.folderID, (int)FolderType.CallingCard, "All"); } - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Clothing)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Clothing, "Clothing"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Clothing, "Clothing"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.CurrentOutfit)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.CurrentOutfit, "Current Outfit"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.CurrentOutfit, "Current Outfit"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Favorites)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Favorites, "Favorites"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Favorites, "Favorites"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Gesture)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Gesture, "Gestures"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Gesture, "Gestures"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Landmark)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Landmark, "Landmarks"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Landmark, "Landmarks"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.LostAndFound)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.LostAndFound, "Lost And Found"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.LostAndFound, "Lost And Found"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Notecard)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Notecard, "Notecards"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Notecard, "Notecards"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Object)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Object, "Objects"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Object, "Objects"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Snapshot)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Snapshot, "Photo Album"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Snapshot, "Photo Album"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.LSLText)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.LSLText, "Scripts"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.LSLText, "Scripts"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Sound)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Sound, "Sounds"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Sound, "Sounds"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Texture)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Texture, "Textures"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Texture, "Textures"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Trash)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Trash, "Trash"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Trash, "Trash"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Settings)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Settings, "Settings"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Settings, "Settings"); - if (!Array.Exists(sysFolders, f => f.type == (int)FolderType.Material)) - CreateFolder(principalID, rootFolder.ID, (int)FolderType.Material, "Materials"); + EnsureSystemFolder(principalID, rootFolder.ID, sysFolders, FolderType.Material, "Materials"); return result; } + /// + /// Create the agent's system folder of under , but only if + /// they do not already have one there. Returns the folder it created, or null when one already existed — + /// so a caller can tell whether it is responsible for populating it. + /// + /// is the snapshot read once at the top of + /// . By the time a later type is reached that snapshot is many database round + /// trips old, and this method is entered concurrently for the same principal: Direct Delivery calls + /// CreateUserInventory on every delivery, and a region can call it at any time through + /// XInventoryInConnector. Two overlapping calls that both read "missing" both create, and there is no + /// unique key on (agentID, type) to catch the loser — which is how seven Legion Grid accounts came to + /// hold two Current Outfit folders each, one of them never written to (A7, + /// Docs/feature/ais-v3/A7-DUPLICATE-COF.md). + /// + /// Re-reading immediately before the insert narrows that window from the whole method to a single + /// query. It does not close it. Nothing here can: only a unique constraint on (agentID, type) + /// makes a duplicate impossible, and that is a migration plus a dedupe of the existing rows, in that order — + /// ledger A-R8. The extra read costs nothing in the normal case, because it is only reached when the snapshot + /// already says the folder is missing. + /// + private XInventoryFolder EnsureSystemFolder(UUID principalID, UUID rootID, XInventoryFolder[] sysFolders, FolderType type, string name) + { + if (Array.Exists(sysFolders, f => f.type == (int)type)) + return null; + + XInventoryFolder[] fresh = m_Database.GetFolders( + [ "agentID", "parentFolderID", "type" ], + [ principalID.ToString(), rootID.ToString(), ((int)type).ToString() ]); + + if (fresh.Length > 0) + { + m_log.LogDebug( + "[XINVENTORY]: not creating a second {Type} folder for {Principal}: {Count} appeared since this call started", + type, principalID, fresh.Length); + return null; + } + + return CreateFolder(principalID, rootID, (int)type, name); + } + + /// + /// AIS-COF-1. One WARN per duplicated type, for the agent whose inventory is being created or checked. It is + /// free: is the snapshot has already read, so + /// this adds no query. + /// + /// Only folders directly under the agent's inventory root are counted, and that is the whole point + /// of the check. Three things legitimately repeat a system type and none of them is a fault: + /// + /// The HG suitcase. HGSuitcaseInventoryService.CreateSystemFolders builds a complete + /// second set of system folders under My Suitcase (type 100) - Current Outfit included. On Legion + /// Grid that accounted for seven accounts that looked like they had two Current Outfit folders each and did + /// not. sysFolders is parented to the root, so the suitcase subtree is already excluded. + /// The calling-card chain, Calling Cards -> Friends -> All, three folders + /// deep all typed CallingCard, created by itself. Only the first is + /// under the root, so again already excluded. + /// Saved outfits (FolderType.Outfit, 47), of which a resident may have any number, and + /// user folders (type -1). Both are excluded explicitly below - 47 by name, -1 because + /// keeps only type >= 0. + /// + /// + /// A warning here is a data fault and wants the dedupe in Docs/feature/ais-v3/A7-DUPLICATE-COF.md. It is + /// not self-healing: nothing in this class removes a folder. + /// + private void WarnOnDuplicateSystemFolders(UUID principalID, XInventoryFolder[] sysFolders) + { + if (sysFolders is null || sysFolders.Length < 2) + return; + + var byType = new Dictionary>(); + foreach (XInventoryFolder f in sysFolders) + { + if (f.type == (int)FolderType.Outfit) // a resident may save any number of outfits + continue; + if (!byType.TryGetValue(f.type, out List group)) + byType[f.type] = group = new List(1); + group.Add(f); + } + + foreach (KeyValuePair> kv in byType) + { + if (kv.Value.Count < 2) + continue; + + m_log.LogWarning( + "[XINVENTORY]: agent {Principal} has {Count} folders of type {Type} directly under the inventory " + + "root ({Folders}); exactly one is expected. This is a data fault, not a fault of this login - see " + + "Docs/feature/ais-v3/A7-DUPLICATE-COF.md for the dedupe. Folders of the same type inside My " + + "Suitcase are expected and are not counted here.", + principalID, kv.Value.Count, (FolderType)kv.Key, + string.Join(", ", kv.Value.ConvertAll(f => $"{f.folderID} v{f.version}"))); + } + } + protected XInventoryFolder CreateFolder(UUID principalID, UUID parentID, int type, string name) { var newFolder = new XInventoryFolder @@ -199,7 +318,15 @@ protected virtual XInventoryFolder[] GetSystemFolders(UUID principalID, UUID roo [ "agentID", "parentFolderID" ], [ principalID.ToString(), rootID.ToString() ]); - XInventoryFolder[] sysFolders = Array.FindAll(allFolders, f => f.type > 0); + // AIS-COF-1: >= 0, not > 0. FolderType.Texture IS zero, so the old filter dropped the "Textures" folder + // from every snapshot this returns - and EnsureSystemFolder reads that snapshot to decide whether the + // folder already exists. The answer was therefore always "missing" for Textures, on every call, and each + // call created another one. That is not a race; it is deterministic, and the data shows it exactly: + // type 0 was the ONLY type with root-level duplicates on Legion Grid, and the account Direct Delivery + // calls CreateUserInventory on for every delivery had NINE "Textures" folders, all version 1 and empty, + // beside the one real one. Nothing else duplicated. Only the A-R8 re-read added to EnsureSystemFolder + // stopped it growing further, by catching the miss one query later. + XInventoryFolder[] sysFolders = Array.FindAll(allFolders, f => f.type >= 0); //m_log.LogDebug( // "[XINVENTORY SERVICE]: Found {0} system folders for {1}", sysFolders.Length, principalID); diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCapRegistrationTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCapRegistrationTests.cs new file mode 100644 index 00000000000..e8e0229a981 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCapRegistrationTests.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Xml; +using Nwc.XmlRpc; +using NUnit.Framework; +using OpenMetaverse; +using OpenSim.Framework; +using Caps = OpenSim.Framework.Capabilities.Caps; +using OpenSim.Framework.Servers.HttpServer; +using OpenSim.Region.ClientStack.LindenCaps.AIS; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// The wiring, not the handler. These are the tests that would have caught the A6 live failure: AIS advertised its +/// caps correctly and then every request 404'd inside the HTTP server, because a capability whose URLs carry +/// sub-paths must be registered as a **variable-path** handler and AIS was not +/// (Docs/feature/ais-v3/A6-LIVE-FAILURE.md). +/// +/// The existing 114 tests all passed while this was broken: they call +/// AisHandler.Handle(request, response) directly, so they exercise routing, envelopes and every operation, +/// and never observe how the handler is bound to a URL. The bug lived entirely between RegisterCaps and the +/// listener. +/// +[TestFixture] +public class AisCapRegistrationTests +{ + private static readonly UUID Agent = new("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + + /// Records what was registered on the listener, and nothing else. + private sealed class RecordingHttpServer : IHttpServer + { + public readonly List<(ISimpleStreamHandler Handler, bool VarPath)> SimpleStreamHandlers = new(); + + public void AddSimpleStreamHandler(ISimpleStreamHandler handler, bool varPath = false) + => SimpleStreamHandlers.Add((handler, varPath)); + + public uint SSLPort => 0; + public string SSLCommonName => ""; + public uint Port => 9000; + public bool UseSSL => false; + public IPAddress ListenIPAddress { get; set; } = IPAddress.Loopback; + public int DebugLevel { get; set; } + + public bool AddHTTPHandler(string methodName, GenericHTTPMethod handler) => true; + public bool AddPollServiceHTTPHandler(string uripath, PollServiceEventArgs args) => true; + public bool AddPollServiceHTTPHandler(PollServiceEventArgs args) => true; + public bool AddPollServiceHTTPHandlerVarPath(PollServiceEventArgs args) => true; + public void RemovePollServiceHTTPHandler(string url, string path) { } + public void RemovePollServiceHTTPHandler(string path) { } + public bool AddLLSDHandler(string path, LLSDMethod handler) => true; + public void AddGlobalMethodHandler(string key, SimpleStreamMethod sh) { } + public void AddStreamHandler(IRequestHandler handler) { } + public bool AddXmlRPCHandler(string method, XmlRpcMethod handler) => true; + public bool AddXmlRPCHandler(string method, XmlRpcMethod handler, bool keepAlive) => true; + public void HandleXmlRpcRequests(OSHttpRequest request, OSHttpResponse response) { } + public void HandleXmlRpcRequests(OSHttpRequest request, OSHttpResponse response, Dictionary rpcHandlers) { } + public bool AddJsonRPCHandler(string method, JsonRPCMethod handler) => true; + public void AddWebSocketHandler(string servicepath, BaseHttpServer.WebSocketRequestDelegate handler) { } + public void RemoveWebSocketHandler(string servicepath) { } + public XmlRpcMethod GetXmlRPCHandler(string method) => null; + public bool SetDefaultLLSDHandler(DefaultLLSDMethod handler) => true; + public void RemoveHTTPHandler(string httpMethod, string path) { } + public bool RemoveLLSDHandler(string path, LLSDMethod handler) => true; + public void RemoveStreamHandler(string httpMethod, string path) { } + public void RemoveSimpleStreamHandler(string path) { } + public void RemoveXmlRPCHandler(string method) { } + public void RemoveJsonRPCHandler(string method) { } + public string GetHTTP404() => ""; + public void AddIndexPHPMethodHandler(string key, SimpleStreamMethod sh) { } + public void RemoveIndexPHPMethodHandler(string key) { } + public SimpleStreamMethod TryGetIndexPHPMethodHandler(string key) => null; + public void Start() { } + public void Stop() { } + } + + /// + /// Registers both AIS caps through the real exactly as AISv3Module.RegisterCaps does, + /// and returns the listener that recorded it plus the caps object. + /// + private static (RecordingHttpServer Server, Caps Caps, AisHandler Inventory, AisHandler Library) Register() + { + var server = new RecordingHttpServer(); + var caps = new Caps(server, "localhost", 9000, "/CAPS/" + UUID.Random(), Agent, "Ebony"); + + var backend = new FakeAisBackend(Agent); + var inventory = new AisHandler("/" + UUID.Random(), Agent, backend); + caps.RegisterSimpleHandler(AISv3Module.CapName, inventory, varPath: AISv3Module.VarPath); + + var library = new AisHandler("/" + UUID.Random(), Agent, backend, AisMode.Library, backend, Agent); + caps.RegisterSimpleHandler(AISv3Module.LibraryCapName, library, varPath: AISv3Module.VarPath); + + return (server, caps, inventory, library); + } + + [Test] + public void both_caps_are_registered_on_the_listener_as_variable_path_handlers() + { + var (server, _, inventory, library) = Register(); + + Assert.That(server.SimpleStreamHandlers.Count, Is.EqualTo(2), "both caps must reach the listener"); + foreach (var (handler, varPath) in server.SimpleStreamHandlers) + { + Assert.That(varPath, Is.True, + $"{handler.Path} was registered as an exact-path handler. BaseHttpServer keeps exact and " + + "variable-path handlers in separate dictionaries and only matches the latter by prefix, so every " + + "AIS sub-path request would 404 before the handler is entered (A6 live failure)."); + } + Assert.That(server.SimpleStreamHandlers.ConvertAll(h => h.Handler.Path), + Is.EquivalentTo(new[] { inventory.CapPath, library.CapPath })); + } + + [Test] + public void both_caps_appear_in_the_seed_response_with_their_urls() + { + var (_, caps, inventory, library) = Register(); + + // SeedCapRequest emits a URL for a requested name only when a handler is registered under it + // (CapsHandlers.GetCapsDetailsLLSDxml). ContainsCap is the same lookup over the same two dictionaries. + Assert.That(caps.CapsHandlers.ContainsCap(AISv3Module.CapName), Is.True, "InventoryAPIv3 must be advertised"); + Assert.That(caps.CapsHandlers.ContainsCap(AISv3Module.LibraryCapName), Is.True, "LibraryAPIv3 must be advertised"); + Assert.That(caps.CapsHandlers.ContainsCap("FetchInventory2"), Is.False, + "a requested name with no handler registered gets no URL, which is why an absent cap is harmless"); + + // and the URL a viewer would be given is the cap path the handler was registered under + Assert.That(inventory.CapPath, Does.StartWith("/")); + Assert.That(library.CapPath, Does.StartWith("/")); + Assert.That(inventory.CapPath, Is.Not.EqualTo(library.CapPath), "each cap gets its own path"); + } + /// + /// Why variable-path registration is required, expressed as the listener's own rule rather than as a repeat of + /// the assertion above. BaseHttpServer.TryGetSimpleStreamHandler tries an exact dictionary lookup on the + /// request's URI path and otherwise looks up only the segment before the second slash, in the variable-path + /// dictionary. Every AIS URL has segments after the cap path, so the exact lookup can never match. + /// + [TestCase("/item/22222222-2222-4222-8222-222222222222")] + [TestCase("/category/11111111-1111-4111-8111-111111111111/children")] + [TestCase("/category/current/links")] + [TestCase("/orphans")] + public void every_ais_url_is_a_sub_path_that_only_the_variable_path_rule_can_match(string suffix) + { + var capPath = "/" + UUID.Random(); + var uriPath = capPath + suffix; + + Assert.That(uriPath, Is.Not.EqualTo(capPath), + "if an AIS URL were ever equal to its cap path, exact registration would have worked"); + + // the exact-match branch: m_simpleStreamHandlers.TryGetValue(uripath) + Assert.That(uriPath.Equals(capPath, StringComparison.Ordinal), Is.False, "exact match cannot hit"); + + // the variable-path branch: m_simpleStreamVarPath.TryGetValue(uripath[..uripath.IndexOf('/', 2)]) + var indx = uriPath.IndexOf('/', 2); + Assert.That(indx, Is.GreaterThan(0), "there must be a second slash for the var-path rule to fire"); + Assert.That(indx, Is.Not.EqualTo(uriPath.Length - 1), "and it must not be the last character"); + Assert.That(uriPath[..indx], Is.EqualTo(capPath), + "the key the listener looks up is exactly the path the cap was registered under"); + } + + /// The router still resolves the operation once the request reaches us, for each of those URLs. + [Test] + public void the_sub_path_urls_resolve_to_real_operations() + { + var capPath = "/" + UUID.Random(); + foreach (var (verb, suffix, expected) in new (string, string, AisOperation)[] + { + ("GET", "/item/22222222-2222-4222-8222-222222222222", AisOperation.FetchItem), + ("GET", "/category/11111111-1111-4111-8111-111111111111/children?depth=50", AisOperation.FetchCategoryChildren), + ("GET", "/category/current/links", AisOperation.FetchCOF), + ("GET", "/orphans", AisOperation.FetchOrphans), + ("PUT", "/category/11111111-1111-4111-8111-111111111111/links", AisOperation.SlamFolder), + }) + { + var route = AisRouter.Parse(verb, capPath + suffix, capPath); + Assert.That(route.Operation, Is.EqualTo(expected), $"{verb} {suffix}"); + } + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisConcurrencyHttpTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisConcurrencyHttpTests.cs new file mode 100644 index 00000000000..c94e4458861 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisConcurrencyHttpTests.cs @@ -0,0 +1,413 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// AIS-SEC-3. Two mutations arriving for the same folder at the same time. +/// +/// The defect. SlamFolder snapshots a folder's links, creates the wanted set, then deletes the +/// snapshot. Create-before-delete is the right failure bias (A3: the folder never holds fewer links than it +/// started with), but nothing serialises two mutations on one folder. Two slams that both snapshot the old links, +/// both create their own set, and both delete only what they saw leave the folder holding the union of two +/// outfits. The viewer can produce this — an outfit change while a previous one is still in flight, or two +/// sessions — and one agent can hold caps from more than one region in this process, so it is not even a +/// cross-process problem. +/// +/// Determinism, not sleeps. Every interleaving here is pinned with +/// and a : one thread parks at a named +/// backend call while the other runs. A race test built on Thread.Sleep reports machine load rather than +/// behaviour. The gate fires before the store is touched and holds no lock, so the parked thread blocks nothing +/// it should not — which matters most for , a test that +/// would otherwise pass for the wrong reason. +/// +/// What "correct" means here. Not "both requests win" — that is impossible for two replacements of +/// the same folder. The requirement is serialisability: the end state must be one that some serial order +/// of the two requests could have produced, and no response may describe a state that never existed. +/// +[TestFixture] +public class AisConcurrencyHttpTests +{ + private const string Cap = "/CAP/5ec3ec30-0000-4000-8000-000000000000"; + private static readonly UUID Alice = new("a5ec3000-0000-4000-8000-000000000001"); + private static readonly UUID Bob = new("b5ec3000-0000-4000-8000-000000000001"); + + private static readonly UUID Root = new("00000000-0000-4000-8000-00000000c001"); + private static readonly UUID Clothing = new("00000000-0000-4000-8000-00000000c002"); + private static readonly UUID Cof = new("00000000-0000-4000-8000-00000000c003"); + private static readonly UUID Other = new("00000000-0000-4000-8000-00000000c004"); + + private static readonly UUID TargetX = new("00000000-0000-4000-8000-0000000000e1"); + private static readonly UUID TargetY = new("00000000-0000-4000-8000-0000000000e2"); + private static readonly UUID TargetZ = new("00000000-0000-4000-8000-0000000000e3"); + private static readonly UUID OldLink = new("00000000-0000-4000-8000-0000000000d1"); + + /// Which logical request the current thread is serving, so the gate can park exactly one of them. + private static readonly ThreadLocal Role = new(); + + private sealed class ConcRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public ConcRequest(string verb, string url, OSD body) + { + HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; + InputStream = body is null ? new MemoryStream() : new MemoryStream(OSDParser.SerializeLLSDXmlBytes(body)); + } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => InputStream.Length > 0; + public long ContentLength => InputStream.Length; + public long ContentLength64 => InputStream.Length; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + /// Root, Clothing, a COF holding one link, a second ordinary folder, and three link targets. + private static FakeAisBackend World(UUID owner) + { + var b = new FakeAisBackend(owner); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddFolder(Cof, Root, "Current Outfit", 11, (short)FolderType.CurrentOutfit); + b.AddFolder(Other, Root, "Another Outfit", 2, (short)FolderType.Outfit); + b.CurrentOutfitId = Cof; + b.AddItem(TargetX, Clothing, "target X"); + b.AddItem(TargetY, Clothing, "target Y"); + b.AddItem(TargetZ, Clothing, "target Z"); + b.AddLink(OldLink, Cof, "the old link", TargetX); + return b; + } + + private static OSDArray Slam(params UUID[] targets) + { + var a = new OSDArray(); + foreach (var t in targets) + a.Add(new OSDMap { ["name"] = "link to " + t, ["desc"] = "", ["linked_id"] = t, ["type"] = (int)AssetType.Link }); + return a; + } + + private static (int Status, OSDMap Body) Send(FakeAisBackend b, UUID agent, string verb, string path, OSD body) + { + var handler = new AisHandler(Cap, agent, b); + var response = new TestOSHttpResponse(); + handler.Handle(new ConcRequest(verb, Cap + path, body), response); + var parsed = OSDParser.DeserializeLLSDXml(response.RawBuffer); + return (response.StatusCode, parsed as OSDMap ?? new OSDMap()); + } + + /// The link targets currently in a folder — the outfit, independent of link row ids. + private static HashSet Outfit(FakeAisBackend b, UUID folder) + => b.Items.Values.Where(i => i.Folder == folder && i.AssetType == (int)AssetType.Link) + .Select(i => i.AssetID).ToHashSet(); + + private static List Ids(OSDMap body, string key) + => body[key] is OSDArray a ? a.Select(o => o.AsUUID()).ToList() : new List(); + + /// + /// Runs on a thread that parks at the first backend call whose label starts with + /// , then runs , then releases the first. The grace wait + /// lets the second request either finish (unserialised) or block on the lock (serialised) — both outcomes are + /// deterministic from here, and only the serialised one pays the grace. + /// + private static ((int Status, OSDMap Body) First, (int Status, OSDMap Body) Second) Interleave( + FakeAisBackend b, string parkAt, Func<(int, OSDMap)> first, Func<(int, OSDMap)> second) + { + using var parked = new ManualResetEventSlim(false); + using var release = new ManualResetEventSlim(false); + using var secondDone = new ManualResetEventSlim(false); + var hasParked = 0; + + b.BeforeCall = label => + { + if (Role.Value != "first") return; + if (!label.StartsWith(parkAt, StringComparison.Ordinal)) return; + if (Interlocked.CompareExchange(ref hasParked, 1, 0) != 0) return; // one-shot + parked.Set(); + release.Wait(TimeSpan.FromSeconds(30)); + }; + + (int, OSDMap) firstResult = default, secondResult = default; + Exception firstError = null, secondError = null; + + var t1 = new Thread(() => { Role.Value = "first"; try { firstResult = first(); } catch (Exception ex) { firstError = ex; } }); + var t2 = new Thread(() => { Role.Value = "second"; try { secondResult = second(); } catch (Exception ex) { secondError = ex; } finally { secondDone.Set(); } }); + + t1.Start(); + try + { + Assert.That(parked.Wait(TimeSpan.FromSeconds(10)), Is.True, $"the first request never reached {parkAt}"); + t2.Start(); + secondDone.Wait(TimeSpan.FromMilliseconds(750)); // finishes at once when nothing serialises + } + finally + { + release.Set(); // a parked thread holds the folder lock; leaking it would poison later tests + } + Assert.That(t1.Join(TimeSpan.FromSeconds(30)), Is.True, "the first request did not finish"); + Assert.That(t2.Join(TimeSpan.FromSeconds(30)), Is.True, "the second request did not finish"); + b.BeforeCall = null; + if (firstError is not null) throw firstError; + if (secondError is not null) throw secondError; + return (firstResult, secondResult); + } + + // ------------------------------------------------------------------ (a) slam vs slam + + /// + /// Both slams snapshot the old links before either creates — the interleaving that produces the union, and + /// the one the defect actually needs. (Parking the second slam's snapshot after the first slam's + /// creates does not reproduce it: the second would then see the first's links and replace them, which + /// is a legal serial outcome.) + /// + /// Correct behaviour is exactly one of the two outfits, whichever the lock ordered second. Never both. + /// + [Test] + public void two_slams_on_one_folder_never_leave_the_union() + { + var b = World(Alice); + // park slam A immediately before its first create, i.e. after it has snapshotted the old links + var (a, second) = Interleave(b, "AddItem(", + () => Send(b, Alice, "PUT", $"/category/{Cof}/links", Slam(TargetX, TargetY)), + () => Send(b, Alice, "PUT", $"/category/{Cof}/links", Slam(TargetZ))); + + var final = Outfit(b, Cof); + var setA = new HashSet { TargetX, TargetY }; + var setB = new HashSet { TargetZ }; + + Assert.Multiple(() => + { + Assert.That(final, Is.Not.EqualTo(new HashSet { TargetX, TargetY, TargetZ }), + "the folder holds the UNION of both outfits - this is the AIS-SEC-3 defect"); + Assert.That(final.SetEquals(setA) || final.SetEquals(setB), Is.True, + $"final outfit must be exactly one of the two requested sets, was [{string.Join(",", final)}]"); + Assert.That(a.Status is 200 or 503, Is.True, $"slam A answered {a.Status}"); + Assert.That(second.Status is 200 or 503, Is.True, $"slam B answered {second.Status}"); + }); + } + + // ------------------------------------------------------------------ (b) slam vs purge + + /// + /// A slam racing DELETE /category/{id}/children on the same folder. + /// + /// PurgeFolderGate is false on purpose, and it is the faithful setting. + /// IInventoryService.PurgeFolder is the one-argument, onlyIfTrash = true form, whose gate is + /// "is this folder Trash or Lost and Found" — and a Current Outfit is neither, so the real service refuses it + /// and AisPurge composes the purge from DeleteItems plus DeleteFolders instead + /// (AisPurge.Run, XInventoryService.cs:503-528). That composed path is the one that races. + /// + /// The purge decides which children to delete, the slam then replaces every one of them, and the purge's + /// closing re-read finds the slam's brand-new links still there — so it reports 500 "only partly purged" + /// and names links it never saw and was never asked to remove. Serialised, a purge of this folder always + /// succeeds, whichever order it runs in. + /// + [Test] + public void a_purge_racing_a_slam_does_not_blame_the_slams_new_links() + { + var b = World(Alice); + b.PurgeFolderGate = _ => false; // a COF is not Trash: the real service refuses, as above + + // park the purge once it has chosen what to delete, then let the slam replace the folder underneath it + var (purge, slam) = Interleave(b, "DeleteItems[", + () => Send(b, Alice, "DELETE", $"/category/{Cof}/children", null), + () => Send(b, Alice, "PUT", $"/category/{Cof}/links", Slam(TargetX, TargetY))); + + var final = Outfit(b, Cof); + Assert.Multiple(() => + { + Assert.That(purge.Status, Is.Not.EqualTo(500), + "the purge blamed children a concurrent slam created - this is the AIS-SEC-3 defect"); + Assert.That(purge.Status is 200 or 503, Is.True, $"purge answered {purge.Status}"); + Assert.That(final.Count == 0 || final.SetEquals(new HashSet { TargetX, TargetY }), Is.True, + $"final state must be empty or exactly the slam's set, was [{string.Join(",", final)}]"); + // whatever the purge claims to have removed, it may not name a link the slam created + var created = Ids(slam.Body, "_created_items").ToHashSet(); + var removed = Ids(purge.Body, "_removed_items"); + Assert.That(removed.Where(created.Contains), Is.Empty, + "the purge reported removing a link the concurrent slam had just created"); + }); + } + + // ------------------------------------------------------------------ (c) slam vs create + + /// + /// A slam racing POST /category/{COF} creating a link into the same folder. The created link must not + /// be reported as created and then silently vanish, and the end state must be a legal serial outcome: the + /// slam's set alone (create first, slam replaced it) or the slam's set plus the new link (slam first). + /// + [Test] + public void a_create_racing_a_slam_is_either_kept_or_never_claimed() + { + var b = World(Alice); + var body = new OSDMap + { + ["links"] = new OSDArray { new OSDMap + { ["name"] = "created link", ["linked_id"] = TargetZ, ["type"] = (int)AssetType.Link } }, + }; + + // park the create immediately before it writes, then let the slam replace the folder underneath it + var (create, slam) = Interleave(b, "AddItem(", + () => Send(b, Alice, "POST", $"/category/{Cof}", body), + () => Send(b, Alice, "PUT", $"/category/{Cof}/links", Slam(TargetX, TargetY))); + + var final = Outfit(b, Cof); + var createdIds = Ids(create.Body, "_created_items"); + Assert.Multiple(() => + { + Assert.That(create.Status is 200 or 503, Is.True, $"create answered {create.Status}"); + Assert.That(slam.Status is 200 or 503, Is.True, $"slam answered {slam.Status}"); + + var slamSet = new HashSet { TargetX, TargetY }; + var slamPlusCreate = new HashSet { TargetX, TargetY, TargetZ }; + Assert.That(final.SetEquals(slamSet) || final.SetEquals(slamPlusCreate), Is.True, + $"final state is not a legal serial outcome, was [{string.Join(",", final)}]"); + + // if the create answered 200 naming a created link, that link must still exist unless the slam, + // ordered after it, legitimately replaced the folder - which it can only have done by seeing it + if (create.Status == 200 && createdIds.Count > 0) + { + var stillThere = createdIds.All(id => b.Items.ContainsKey(id)); + var slamSawIt = Ids(slam.Body, "_removed_items").Intersect(createdIds).Any(); + Assert.That(stillThere || slamSawIt, Is.True, + "the create reported a link as created, and it is gone without the slam ever having seen it"); + } + }); + } + + // ------------------------------------------------------------------ (d) and (e): no over-locking + + /// + /// Two slams on DIFFERENT folders of the same agent must not wait on each other. The first is parked before + /// its creates; the second must complete anyway. These pass today by accident and must stay passing — a lock + /// that serialised the whole agent, or the whole handler, would break outfit changes and saved-outfit edits + /// happening together. + /// + [Test] + public void different_folders_do_not_block_each_other() + { + var b = World(Alice); + using var parked = new ManualResetEventSlim(false); + using var release = new ManualResetEventSlim(false); + using var secondDone = new ManualResetEventSlim(false); + var hasParked = 0; + + b.BeforeCall = label => + { + if (Role.Value != "first" || !label.StartsWith("AddItem(", StringComparison.Ordinal)) return; + if (Interlocked.CompareExchange(ref hasParked, 1, 0) != 0) return; + parked.Set(); + release.Wait(TimeSpan.FromSeconds(30)); + }; + + (int Status, OSDMap Body) cofResult = default, otherResult = default; + var t1 = new Thread(() => { Role.Value = "first"; cofResult = Send(b, Alice, "PUT", $"/category/{Cof}/links", Slam(TargetX)); }); + var t2 = new Thread(() => { Role.Value = "second"; otherResult = Send(b, Alice, "PUT", $"/category/{Other}/links", Slam(TargetZ)); secondDone.Set(); }); + + // The stripe array means two unrelated keys CAN share a lock; that is only ever slower, never wrong, + // but it would make this test fail for a reason that has nothing to do with the property under test. + // Assert it up front so a future UUID change says so instead of timing out mysteriously. + Assert.That(AisFolderLocks.StripeOf(Alice, Cof), Is.Not.EqualTo(AisFolderLocks.StripeOf(Alice, Other)), + "these two folder keys share a lock stripe; pick different test UUIDs"); + + t1.Start(); + try + { + Assert.That(parked.Wait(TimeSpan.FromSeconds(10)), Is.True, "the first slam never reached its creates"); + t2.Start(); + Assert.That(secondDone.Wait(TimeSpan.FromSeconds(5)), Is.True, + "a slam on a DIFFERENT folder waited on the parked one - the lock is too coarse"); + } + finally + { + // Always release: a parked thread holds the folder lock, and leaking it would break every later + // test in this process rather than just this one. + release.Set(); + t1.Join(TimeSpan.FromSeconds(30)); + t2.Join(TimeSpan.FromSeconds(30)); + b.BeforeCall = null; + } + + Assert.Multiple(() => + { + Assert.That(otherResult.Status, Is.EqualTo(200)); + Assert.That(cofResult.Status, Is.EqualTo(200)); + Assert.That(Outfit(b, Other), Is.EquivalentTo(new[] { TargetZ })); + Assert.That(Outfit(b, Cof), Is.EquivalentTo(new[] { TargetX })); + }); + } + + /// Two different agents slamming their own Current Outfit must not block each other either. + [Test] + public void different_agents_do_not_block_each_other() + { + var alice = World(Alice); + var bob = World(Bob); + using var parked = new ManualResetEventSlim(false); + using var release = new ManualResetEventSlim(false); + using var secondDone = new ManualResetEventSlim(false); + var hasParked = 0; + + alice.BeforeCall = label => + { + if (Role.Value != "first" || !label.StartsWith("AddItem(", StringComparison.Ordinal)) return; + if (Interlocked.CompareExchange(ref hasParked, 1, 0) != 0) return; + parked.Set(); + release.Wait(TimeSpan.FromSeconds(30)); + }; + + (int Status, OSDMap Body) aliceResult = default, bobResult = default; + var t1 = new Thread(() => { Role.Value = "first"; aliceResult = Send(alice, Alice, "PUT", $"/category/{Cof}/links", Slam(TargetX)); }); + var t2 = new Thread(() => { Role.Value = "second"; bobResult = Send(bob, Bob, "PUT", $"/category/{Cof}/links", Slam(TargetZ)); secondDone.Set(); }); + + Assert.That(AisFolderLocks.StripeOf(Alice, Cof), Is.Not.EqualTo(AisFolderLocks.StripeOf(Bob, Cof)), + "Alice's and Bob's COF keys share a lock stripe; pick different test agent UUIDs"); + + t1.Start(); + try + { + Assert.That(parked.Wait(TimeSpan.FromSeconds(10)), Is.True, "Alice's slam never reached its creates"); + t2.Start(); + Assert.That(secondDone.Wait(TimeSpan.FromSeconds(5)), Is.True, + "Bob waited on Alice - the lock key is missing the agent, so two residents whose COF ids collide would serialise"); + } + finally + { + release.Set(); + t1.Join(TimeSpan.FromSeconds(30)); + t2.Join(TimeSpan.FromSeconds(30)); + alice.BeforeCall = null; + } + + Assert.Multiple(() => + { + Assert.That(aliceResult.Status, Is.EqualTo(200)); + Assert.That(bobResult.Status, Is.EqualTo(200)); + Assert.That(Outfit(alice, Cof), Is.EquivalentTo(new[] { TargetX })); + Assert.That(Outfit(bob, Cof), Is.EquivalentTo(new[] { TargetZ })); + }); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCreatePartialHttpTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCreatePartialHttpTests.cs new file mode 100644 index 00000000000..f52bc4f8a75 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCreatePartialHttpTests.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// AIS-SEC-4. POST /category/{parent} that fails partway must say what it already created. +/// +/// The defect. CreateInventory adds categories one at a time, then links one at a time. A +/// failure after earlier writes succeeded answered 500 with a body carrying only the error keys - so the +/// objects already in the database were invisible to the client, which could neither adopt them nor clean them up, +/// and a retry created them a second time. AisCopy already got this right and is the precedent this follows: +/// "additive, so a partial copy leaves what it made and risks nothing that existed before", and its failure +/// response names the counts it created before failing. +/// +/// The status stays 500, with the delta keys populated. The spec settles this; it is not a coin toss. +/// AIS-V3-SPEC.md §1f says of the viewer, for every response: "always, success or failure - +/// onUpdateReceived(result, type, body) (llaisapi.cpp:946): the body is parsed as an update +/// even on error", and "the completion callback fires at least once, with a null id unless the body carries the +/// ids of §1c". §1c in turn says CREATEINVENTORY "fires once per _created_categories / +/// _created_items entry". So created ids placed in a 500 body are not decoration the viewer discards - it +/// applies them as an update and fires the per-id callbacks. That is exactly the reconciliation the defect denied. +/// +/// +/// Why not 207. 207 is a 2xx, so llaisapi would take the response as success and never emit the +/// "any other failure" warning the spec's table records for 4xx/5xx (:942-943) - the failure would vanish +/// from the client's own log. 500 with a populated body gives both halves: the client learns what exists, and the +/// failure is still a failure to everyone watching. The conservative choice and the correct one coincide here. +/// +/// One constraint the spec does impose, and the shape below respects it: an error body "must be a map +/// and must not carry item_id/category_id + parent_id pairs it does not mean". The success +/// path already emits no top-level ids - only _created_categories, _created_items, +/// _embedded and _updated_category_versions - so reusing it verbatim is safe. +/// +[TestFixture] +public class AisCreatePartialHttpTests +{ + private const string Cap = "/CAP/5ec40000-0000-4000-8000-000000000000"; + private static readonly UUID Agent = new("a5ec4000-0000-4000-8000-000000000001"); + + private static readonly UUID Root = new("00000000-0000-4000-8000-00000000d001"); + private static readonly UUID Clothing = new("00000000-0000-4000-8000-00000000d002"); + private static readonly UUID TargetA = new("00000000-0000-4000-8000-00000000d0a1"); + private static readonly UUID TargetB = new("00000000-0000-4000-8000-00000000d0a2"); + private static readonly UUID TargetC = new("00000000-0000-4000-8000-00000000d0a3"); + + private sealed class CpRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public CpRequest(string verb, string url, OSD body) + { + HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; + InputStream = body is null ? new MemoryStream() : new MemoryStream(OSDParser.SerializeLLSDXmlBytes(body)); + } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => InputStream.Length > 0; + public long ContentLength => InputStream.Length; + public long ContentLength64 => InputStream.Length; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + private static FakeAisBackend World() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddItem(TargetA, Clothing, "target A"); + b.AddItem(TargetB, Clothing, "target B"); + b.AddItem(TargetC, Clothing, "target C"); + return b; + } + + private static (int Status, OSDMap Body) Post(FakeAisBackend b, OSDMap body) + { + var handler = new AisHandler(Cap, Agent, b); + var response = new TestOSHttpResponse(); + handler.Handle(new CpRequest("POST", Cap + $"/category/{Clothing}", body), response); + var parsed = OSDParser.DeserializeLLSDXml(response.RawBuffer); + Assert.That(parsed, Is.InstanceOf(), "an error body must still be an LLSD map (spec 1f)"); + return (response.StatusCode, (OSDMap)parsed); + } + + private static OSDMap Category(string name) => new() { ["name"] = name, ["type_default"] = (int)FolderType.Outfit }; + private static OSDMap Link(UUID target) => new() + { ["name"] = "link to " + target, ["desc"] = "", ["linked_id"] = target, ["type"] = (int)AssetType.Link }; + + private static List Ids(OSDMap body, string key) + => body[key] is OSDArray a ? a.Select(o => o.AsUUID()).ToList() : new List(); + + // ------------------------------------------------------------------ (a) a category fails partway + + /// + /// Three categories, the second refused. The first is already in the database, so the response must name it: + /// without that the client cannot adopt it and its retry makes a duplicate. + /// + [Test] + public void a_category_create_that_fails_on_the_second_reports_the_first() + { + var b = World(); + var seen = 0; + b.AddFolderGate = _ => ++seen != 2; // the second AddFolder is refused + + var (status, body) = Post(b, new OSDMap + { + ["categories"] = new OSDArray { Category("first"), Category("second"), Category("third") }, + }); + + var reported = Ids(body, "_created_categories"); + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(500), "the failure is still a failure"); + Assert.That(reported, Has.Count.EqualTo(1), + "the category created before the failure must be reported - this is the AIS-SEC-4 defect"); + Assert.That(b.Folders.ContainsKey(reported.FirstOrDefault()), Is.True, + "and the id reported must be the one that really exists"); + Assert.That(b.Folders.Values.Count(f => f.Name == "first"), Is.EqualTo(1)); + Assert.That(b.Folders.Values.Any(f => f.Name == "second" || f.Name == "third"), Is.False, + "nothing after the failure may have been written"); + Assert.That(body.ContainsKey("message"), Is.True, "and the error keys are still there for the log"); + }); + } + + // ------------------------------------------------------------------ (b) a link fails after categories + + /// + /// Two categories then three links, the second link refused. Everything before it - both categories and the + /// first link - must be reported, because all of it is in the database. + /// + [Test] + public void a_link_create_that_fails_on_the_second_reports_the_categories_and_the_first_link() + { + var b = World(); + var seen = 0; + b.AddItemGate = _ => ++seen != 2; // the second AddItem is refused + + var (status, body) = Post(b, new OSDMap + { + ["categories"] = new OSDArray { Category("outfit one"), Category("outfit two") }, + ["links"] = new OSDArray { Link(TargetA), Link(TargetB), Link(TargetC) }, + }); + + var categories = Ids(body, "_created_categories"); + var items = Ids(body, "_created_items"); + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(500)); + Assert.That(categories, Has.Count.EqualTo(2), "both categories were created and must be reported"); + Assert.That(items, Has.Count.EqualTo(1), "so was the first link"); + Assert.That(categories.All(id => b.Folders.ContainsKey(id)), Is.True); + Assert.That(items.All(id => b.Items.ContainsKey(id)), Is.True); + // the one link that did land points at the first target, and nothing later exists + Assert.That(b.Items[items[0]].AssetID, Is.EqualTo(TargetA)); + Assert.That(b.Items.Values.Any(i => i.AssetID == TargetB || i.AssetID == TargetC), Is.False); + Assert.That(body.ContainsKey("_updated_category_versions"), Is.True, + "the parent's version moved, so the viewer needs it or it will not re-read the folder"); + }); + } + + // ------------------------------------------------------------------ (c) the first write fails + + /// + /// Nothing was created, so nothing is reported. This is the case whose behaviour must NOT change: the status is + /// what it was, and the delta keys are absent rather than present-and-empty (absent and empty are the same to + /// the viewer, and absent is what the success path emits when a collection is empty). + /// + [Test] + public void a_create_that_fails_on_the_very_first_write_reports_nothing_created() + { + var b = World(); + b.AddFolderGate = _ => false; // every AddFolder refused + + var (status, body) = Post(b, new OSDMap + { + ["categories"] = new OSDArray { Category("first"), Category("second") }, + }); + + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(500), "unchanged from before AIS-SEC-4"); + Assert.That(body.ContainsKey("_created_categories"), Is.False); + Assert.That(body.ContainsKey("_created_items"), Is.False); + Assert.That(body.ContainsKey("_embedded"), Is.False); + Assert.That(b.Folders.Values.Any(f => f.Name == "first" || f.Name == "second"), Is.False); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(500)); + }); + } + + // ------------------------------------------------------------------ (d) the happy path is pinned + + /// + /// Full success, pinned against the behaviour that shipped before this session so AIS-SEC-4 cannot quietly + /// change it: status 200, the same four delta/content keys, and no error keys. + /// + [Test] + public void a_fully_successful_create_is_unchanged() + { + var b = World(); + var (status, body) = Post(b, new OSDMap + { + ["categories"] = new OSDArray { Category("outfit one"), Category("outfit two") }, + ["links"] = new OSDArray { Link(TargetA), Link(TargetB) }, + }); + + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(200)); + Assert.That(Ids(body, "_created_categories"), Has.Count.EqualTo(2)); + Assert.That(Ids(body, "_created_items"), Has.Count.EqualTo(2)); + Assert.That(body.ContainsKey("_embedded"), Is.True); + Assert.That(body.ContainsKey("_updated_category_versions"), Is.True); + + // no error keys on a success, and no top-level ids the spec forbids in any body (1f) + Assert.That(body.ContainsKey("error_code"), Is.False); + Assert.That(body.ContainsKey("message"), Is.False); + Assert.That(body.ContainsKey("category_id"), Is.False); + Assert.That(body.ContainsKey("item_id"), Is.False); + + var embedded = (OSDMap)body["_embedded"]; + Assert.That(((OSDMap)embedded["categories"]).Count, Is.EqualTo(2)); + Assert.That(((OSDMap)embedded["links"]).Count, Is.EqualTo(2)); + Assert.That(b.Folders.Values.Count(f => f.Name.StartsWith("outfit ")), Is.EqualTo(2)); + }); + } + + /// + /// The partial-failure body must carry no top-level category_id / item_id either. The spec is + /// explicit that an error body "must not carry item_id/category_id + parent_id pairs it + /// does not mean" (§1f), and the viewer parses an error body as an update, so a stray pair would be applied. + /// + [Test] + public void a_partial_failure_body_carries_no_top_level_ids() + { + var b = World(); + var seen = 0; + b.AddFolderGate = _ => ++seen != 2; + + var (_, body) = Post(b, new OSDMap + { + ["categories"] = new OSDArray { Category("first"), Category("second") }, + }); + + Assert.Multiple(() => + { + Assert.That(body.ContainsKey("category_id"), Is.False); + Assert.That(body.ContainsKey("item_id"), Is.False); + Assert.That(body.ContainsKey("parent_id"), Is.False); + }); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCrossUserHttpTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCrossUserHttpTests.cs new file mode 100644 index 00000000000..726ddbaece3 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCrossUserHttpTests.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// AIS-SEC-1. Alice's cap, handed Bob's UUIDs, over the real +/// sitting on - +/// a double that reproduces XInventoryService's habit of looking rows up by UUID and disregarding the +/// principal it was handed. +/// +/// Why not . Every other HTTP fixture in this project runs on that fake, +/// which enforces owner scoping itself (if (agentId != Owner) return null; in each read). So the whole +/// suite was green while the shipped backend did no scoping at all: it was a pass-through to +/// IInventoryService, and a valid AIS cap plus another resident's item or folder UUID could read, rename, +/// delete, purge, slam and create across the boundary. These tests are built so that they cannot be satisfied by +/// anything except a check inside the backend. +/// +/// The status chosen for a foreign object is 404, uniformly, for reads and mutations alike - not 403. +/// Three reasons, in order of weight: +/// +/// It is what the scoping means. The backend's contract is that a foreign object is not visible to +/// this cap, so the handler's pre-existing not-found paths are the correct ones and no status mapping had to be +/// invented for the security case. +/// 403 would be an oracle: it distinguishes "this UUID belongs to someone else" from "this UUID does not +/// exist", which hands an attacker a membership test over the whole inventory keyspace. 404 says the same thing +/// to both. +/// The viewer treats any non-2xx the same way here - it logs the status and still runs +/// onUpdateReceived without reverting the local edit (llaisapi.cpp:851-951) - so nothing client +/// side turns on the choice. What matters is that the error body carries no +/// _updated_category_versions, which is true of every WriteError path. +/// +/// The one exception is POST /category/{parent} whose body names a different +/// parent_id: that is a malformed request rather than a missing object, and it answers 400. The URL +/// is the authority (AISAPI::CreateInventory builds {inv}/category/{parentId}, +/// llaisapi.cpp:115), and the viewer's own body repeats the same id +/// (LLInventoryCategory::asAISCreateCatLLSD, llinventory.cpp:1256-1276), so a mismatch is never +/// something a viewer sends. +/// +/// Every cross-user case asserts both halves: the status, and that Bob's rows are byte-for-byte where +/// they were. A 404 that had already written is not a fix. +/// +[TestFixture] +public class AisCrossUserHttpTests +{ + private const string Cap = "/CAP/5ec0de00-0000-4000-8000-000000000000"; + + /// One resident's inventory shape: root, COF, Clothing, a saved Outfit, a wearable and a COF link. + private sealed record Person( + UUID Agent, UUID Root, UUID Cof, UUID Clothing, UUID Outfit, + UUID Wearable, UUID OutfitItem, UUID CofLink); + + private static readonly Person Alice = new( + new("a0000000-0000-4000-8000-000000000001"), new("a0000000-0000-4000-8000-000000000002"), + new("a0000000-0000-4000-8000-000000000003"), new("a0000000-0000-4000-8000-000000000004"), + new("a0000000-0000-4000-8000-000000000005"), new("a0000000-0000-4000-8000-000000000006"), + new("a0000000-0000-4000-8000-000000000007"), new("a0000000-0000-4000-8000-000000000008")); + + private static readonly Person Bob = new( + new("b0000000-0000-4000-8000-000000000001"), new("b0000000-0000-4000-8000-000000000002"), + new("b0000000-0000-4000-8000-000000000003"), new("b0000000-0000-4000-8000-000000000004"), + new("b0000000-0000-4000-8000-000000000005"), new("b0000000-0000-4000-8000-000000000006"), + new("b0000000-0000-4000-8000-000000000007"), new("b0000000-0000-4000-8000-000000000008")); + + private sealed class CrossUserRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public CrossUserRequest(string verb, string url, OSD body) + { + HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; + InputStream = body is null ? new MemoryStream() : new MemoryStream(OSDParser.SerializeLLSDXmlBytes(body)); + } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => InputStream.Length > 0; + public long ContentLength => InputStream.Length; + public long ContentLength64 => InputStream.Length; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + /// Alice and Bob, same shape, one store, nothing scoped by the service underneath. + private static PrincipalIgnoringInventoryService World() + { + var svc = new PrincipalIgnoringInventoryService(); + foreach (var p in new[] { Alice, Bob }) + { + svc.Seed(p.Root, p.Agent, UUID.Zero, "My Inventory", (short)FolderType.Root, 3); + svc.Seed(p.Cof, p.Agent, p.Root, "Current Outfit", (short)FolderType.CurrentOutfit, 11); + svc.Seed(p.Clothing, p.Agent, p.Root, "Clothing", (short)FolderType.Clothing, 7); + // an ordinary saved outfit: FolderType.Outfit is the one system type the delete route does NOT + // protect, so a cross-user DELETE really would go through if nothing else stopped it + svc.Seed(p.Outfit, p.Agent, p.Clothing, "Beach Outfit", (short)FolderType.Outfit, 2); + svc.SeedItem(p.Wearable, p.Agent, p.Clothing, "a shirt"); + svc.SeedItem(p.OutfitItem, p.Agent, p.Outfit, "a note in the outfit"); + svc.SeedLink(p.CofLink, p.Agent, p.Cof, "link to the shirt", p.Wearable); + } + return svc; + } + + /// + /// The real region backend, owner-bound. Before AIS-SEC-1 the constructor took no owner at all and this call + /// is the whole of the defect: nothing downstream of it knew whose cap this was. + /// + private static IAisInventoryBackend Backend(PrincipalIgnoringInventoryService service, UUID owner) + => new AISv3Module.InventoryServiceBackend(service, owner); + + private static (int Status, OSDMap Body) AsAlice(PrincipalIgnoringInventoryService svc, string verb, string path, OSD body = null) + { + var handler = new AisHandler(Cap, Alice.Agent, Backend(svc, Alice.Agent)); + var response = new TestOSHttpResponse(); + handler.Handle(new CrossUserRequest(verb, Cap + path, body), response); + var parsed = OSDParser.DeserializeLLSDXml(response.RawBuffer); + Assert.That(parsed, Is.InstanceOf()); + return (response.StatusCode, (OSDMap)parsed); + } + + private static IReadOnlyList ChildFolders(PrincipalIgnoringInventoryService svc, UUID parent) + => svc.Folders.Values.Where(f => f.ParentID == parent).Select(f => f.ID).OrderBy(i => i).ToList(); + + private static IReadOnlyList ChildItems(PrincipalIgnoringInventoryService svc, UUID parent) + => svc.Items.Values.Where(i => i.Folder == parent).Select(i => i.ID).OrderBy(i => i).ToList(); + + // ------------------------------------------------------------------ reads + + [Test] + public void GET_item_of_another_resident_is_404() + { + var svc = World(); + var (status, body) = AsAlice(svc, "GET", $"/item/{Bob.Wearable}"); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(body.ContainsKey("item_id"), Is.False, "no part of Bob's item may travel"); + Assert.That(svc.Items[Bob.Wearable].Name, Is.EqualTo("a shirt")); + } + + [Test] + public void GET_category_children_of_another_resident_is_404() + { + var svc = World(); + var (status, body) = AsAlice(svc, "GET", $"/category/{Bob.Outfit}/children"); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(body.ContainsKey("_embedded"), Is.False, "Bob's children may not be enumerated"); + } + + [Test] + public void GET_category_links_of_another_resident_is_404() + { + var svc = World(); + var (status, body) = AsAlice(svc, "GET", $"/category/{Bob.Cof}/links"); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(body.ContainsKey("_embedded"), Is.False, "Bob's outfit may not be enumerated"); + } + + // ------------------------------------------------------------------ updates + + [Test] + public void PATCH_item_of_another_resident_is_refused_and_changes_nothing() + { + var svc = World(); + var (status, _) = AsAlice(svc, "PATCH", $"/item/{Bob.Wearable}", new OSDMap { ["name"] = "stolen" }); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(svc.Items[Bob.Wearable].Name, Is.EqualTo("a shirt")); + } + + [Test] + public void PATCH_category_of_another_resident_is_refused_and_changes_nothing() + { + var svc = World(); + var (status, _) = AsAlice(svc, "PATCH", $"/category/{Bob.Outfit}", new OSDMap { ["name"] = "stolen" }); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(svc.Folders[Bob.Outfit].Name, Is.EqualTo("Beach Outfit")); + } + + // ------------------------------------------------------------------ deletes + + [Test] + public void DELETE_item_of_another_resident_is_refused_and_the_row_survives() + { + var svc = World(); + var (status, _) = AsAlice(svc, "DELETE", $"/item/{Bob.Wearable}"); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(svc.Items.ContainsKey(Bob.Wearable), Is.True); + } + + [Test] + public void DELETE_category_of_another_resident_is_refused_and_the_row_survives() + { + var svc = World(); + var (status, _) = AsAlice(svc, "DELETE", $"/category/{Bob.Outfit}"); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(svc.Folders.ContainsKey(Bob.Outfit), Is.True); + Assert.That(svc.Items.ContainsKey(Bob.OutfitItem), Is.True, "nor may its contents be purged as a side effect"); + } + + [Test] + public void DELETE_category_children_of_another_resident_is_refused_and_the_children_survive() + { + var svc = World(); + var (status, _) = AsAlice(svc, "DELETE", $"/category/{Bob.Outfit}/children"); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(ChildItems(svc, Bob.Outfit), Is.EqualTo(new[] { Bob.OutfitItem })); + } + + // ------------------------------------------------------------------ slam and create + + [Test] + public void PUT_links_into_another_residents_COF_is_refused_and_their_outfit_is_unchanged() + { + var svc = World(); + var before = ChildItems(svc, Bob.Cof); + + var slam = new OSDArray + { + new OSDMap + { + ["name"] = "link to Alice's shirt", + ["desc"] = "", + ["linked_id"] = Alice.Wearable, + ["type"] = (int)AssetType.Link, + }, + }; + var (status, _) = AsAlice(svc, "PUT", $"/category/{Bob.Cof}/links", slam); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(ChildItems(svc, Bob.Cof), Is.EqualTo(before), "Bob's COF links must be exactly what they were"); + Assert.That(svc.Items[Bob.CofLink].AssetID, Is.EqualTo(Bob.Wearable)); + } + + [Test] + public void POST_category_under_another_resident_is_refused_and_creates_nothing() + { + var svc = World(); + var before = ChildFolders(svc, Bob.Outfit); + + var (status, _) = AsAlice(svc, "POST", $"/category/{Bob.Outfit}", + new OSDMap { ["categories"] = new OSDArray { new OSDMap { ["name"] = "planted" } } }); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(ChildFolders(svc, Bob.Outfit), Is.EqualTo(before)); + } + + /// + /// The same create, but addressed to Alice's own folder with a body parent_id pointing at Bob's. The URL + /// is the authority; the mismatch is a malformed request and answers 400 before anything is written. The + /// backend's own parent check would refuse the write in any case - this makes the refusal say why. + /// + [Test] + public void POST_category_whose_body_parent_points_at_another_resident_is_400_and_creates_nothing() + { + var svc = World(); + var beforeBob = ChildFolders(svc, Bob.Outfit); + var beforeAlice = ChildFolders(svc, Alice.Outfit); + + var (status, body) = AsAlice(svc, "POST", $"/category/{Alice.Outfit}", new OSDMap + { + ["categories"] = new OSDArray + { + new OSDMap + { + ["category_id"] = UUID.Zero, + ["parent_id"] = Bob.Outfit, + ["type_default"] = (int)FolderType.Outfit, + ["name"] = "planted", + }, + }, + }); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.BadRequest)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(400)); + Assert.That(ChildFolders(svc, Bob.Outfit), Is.EqualTo(beforeBob), "nothing may be created under Bob"); + Assert.That(ChildFolders(svc, Alice.Outfit), Is.EqualTo(beforeAlice), "and nothing under Alice either"); + } + + // ------------------------------------------------------------------ the positive control + + /// + /// The same eleven routes against Alice's own objects. Without this the fixture could be satisfied by a + /// backend that refuses everything, which is not a fix but an outage. + /// + [Test] + public void every_route_still_works_against_the_callers_own_objects() + { + Assert.Multiple(() => + { + var svc = World(); + Assert.That(AsAlice(svc, "GET", $"/item/{Alice.Wearable}").Status, Is.EqualTo(200), "GET /item"); + Assert.That(AsAlice(svc, "GET", $"/category/{Alice.Outfit}/children").Status, Is.EqualTo(200), "GET /children"); + Assert.That(AsAlice(svc, "GET", $"/category/{Alice.Cof}/links").Status, Is.EqualTo(200), "GET /links"); + + svc = World(); + Assert.That(AsAlice(svc, "PATCH", $"/item/{Alice.Wearable}", new OSDMap { ["name"] = "renamed" }).Status, + Is.EqualTo(200), "PATCH /item"); + Assert.That(svc.Items[Alice.Wearable].Name, Is.EqualTo("renamed")); + + svc = World(); + Assert.That(AsAlice(svc, "PATCH", $"/category/{Alice.Outfit}", new OSDMap { ["name"] = "renamed" }).Status, + Is.EqualTo(200), "PATCH /category"); + Assert.That(svc.Folders[Alice.Outfit].Name, Is.EqualTo("renamed")); + + svc = World(); + Assert.That(AsAlice(svc, "DELETE", $"/item/{Alice.Wearable}").Status, Is.EqualTo(200), "DELETE /item"); + Assert.That(svc.Items.ContainsKey(Alice.Wearable), Is.False); + + svc = World(); + Assert.That(AsAlice(svc, "DELETE", $"/category/{Alice.Outfit}").Status, Is.EqualTo(200), "DELETE /category"); + Assert.That(svc.Folders.ContainsKey(Alice.Outfit), Is.False); + + svc = World(); + Assert.That(AsAlice(svc, "DELETE", $"/category/{Alice.Outfit}/children").Status, Is.EqualTo(200), "DELETE /children"); + Assert.That(ChildItems(svc, Alice.Outfit), Is.Empty); + + svc = World(); + var slam = new OSDArray + { + new OSDMap + { + ["name"] = "link to the shirt", + ["desc"] = "", + ["linked_id"] = Alice.Wearable, + ["type"] = (int)AssetType.Link, + }, + }; + Assert.That(AsAlice(svc, "PUT", $"/category/{Alice.Cof}/links", slam).Status, Is.EqualTo(200), "PUT /links"); + Assert.That(svc.Items.ContainsKey(Alice.CofLink), Is.False, "the old link went"); + Assert.That(ChildItems(svc, Alice.Cof), Has.Count.EqualTo(1), "and exactly one new one is there"); + + svc = World(); + var (createStatus, createBody) = AsAlice(svc, "POST", $"/category/{Alice.Outfit}", + new OSDMap { ["categories"] = new OSDArray { new OSDMap { ["name"] = "Sub Outfit" } } }); + Assert.That(createStatus, Is.EqualTo(200), "POST /category"); + var created = ((OSDArray)createBody["_created_categories"]).Single().AsUUID(); + Assert.That(svc.Folders[created].ParentID, Is.EqualTo(Alice.Outfit)); + + // and the create whose body repeats the URL parent, which is what the viewer actually sends + svc = World(); + var (echoStatus, _) = AsAlice(svc, "POST", $"/category/{Alice.Outfit}", new OSDMap + { + ["categories"] = new OSDArray + { + new OSDMap + { + ["category_id"] = UUID.Zero, + ["parent_id"] = Alice.Outfit, + ["type_default"] = (int)FolderType.Outfit, + ["name"] = "Sub Outfit", + }, + }, + }); + Assert.That(echoStatus, Is.EqualTo(200), "POST /category with the body repeating the URL parent"); + Assert.That(ChildFolders(svc, Alice.Outfit), Has.Count.EqualTo(1)); + }); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisDuplicateSystemFolderTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisDuplicateSystemFolderTests.cs new file mode 100644 index 00000000000..55127e4713b --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisDuplicateSystemFolderTests.cs @@ -0,0 +1,179 @@ +using System; +using System.Linq; +using NUnit.Framework; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// The A7 live failure: an agent with two Current Outfit folders, where the inventory service resolves +/// "current" to the wrong one and a take-off is written into a folder no viewer reads +/// (Docs/feature/ais-v3/A7-DUPLICATE-COF.md). +/// +/// The existing suite could not catch this because every fixture gives its fake agent exactly **one** folder +/// per type. `FakeAisBackend.GetFolderForType` then answers correctly no matter what rule it uses, so the tests +/// agree with the service on an input where the service cannot be wrong. The bug only exists on the input nobody +/// constructed: two folders of the same type. It is a resolution bug, not a route bug, and the 121 route tests +/// resolve nothing. +/// +[TestFixture] +public class AisDuplicateSystemFolderTests +{ + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + + // The live ids, so the test reads as the incident it encodes. + private static readonly UUID RealCof = new("71c3c184-410b-4dae-b20a-855741cf1faf"); // version 457, the one the viewer uses + private static readonly UUID DupeCof = new("52c327c4-cb7d-4365-a7f0-62a6f7545265"); // version 1, the one we returned + + /// + /// Truly Bazar's inventory as it actually is: a root, two type-46 folders, and a backend whose + /// GetFolderForType returns the version-1 duplicate — which is what the unordered + /// folders[0] query did on the day. + /// + private static FakeAisBackend TwoCurrentOutfits(int realVersion = 457, int dupeVersion = 1) + { + var backend = new FakeAisBackend(Agent); + var root = UUID.Random(); + backend.AddFolder(root, UUID.Zero, "My Inventory", 1, (short)FolderType.Root); + backend.AddFolder(DupeCof, root, "Current Outfit", dupeVersion, (short)FolderType.CurrentOutfit); + backend.AddFolder(RealCof, root, "Current Outfit", realVersion, (short)FolderType.CurrentOutfit); + + // Reproduce the failure exactly: the service hands back the duplicate. + backend.CurrentOutfitId = DupeCof; + return backend; + } + + [Test] + public void current_resolves_to_the_folder_the_viewer_uses_not_the_one_the_service_returns() + { + var backend = TwoCurrentOutfits(); + + var cof = AisInventory.GetCurrentOutfit(backend, Agent); + + Assert.That(cof, Is.Not.Null); + Assert.That(cof.ID, Is.EqualTo(RealCof), + "resolved to the version-1 duplicate. A slam against it is written to a folder no viewer reads, so the " + + "outfit change silently does not stick (A7 live failure, checklist step 10)."); + Assert.That(cof.Version, Is.EqualTo(457)); + } + + [Test] + public void the_backends_own_answer_is_the_wrong_one_so_the_fixture_really_does_reproduce_the_bug() + { + var backend = TwoCurrentOutfits(); + + // Guards the test itself: if the fake ever stopped returning the duplicate, the test above would pass + // for the wrong reason. + Assert.That(backend.GetFolderForType(Agent, FolderType.CurrentOutfit).ID, Is.EqualTo(DupeCof)); + } + + [Test] + public void a_warning_names_every_candidate_its_version_and_the_one_chosen() + { + var backend = TwoCurrentOutfits(); + + using var log = new CapturedLog(); + AisInventory.GetCurrentOutfit(backend, Agent); + + var warning = log.Warnings.SingleOrDefault(w => w.Contains("folders of type")); + Assert.That(warning, Is.Not.Null, "an operator must see this without running a DB query"); + Assert.That(warning, Does.Contain(Agent.ToString()), "names the agent"); + Assert.That(warning, Does.Contain(RealCof.ToString()), "names the kept folder"); + Assert.That(warning, Does.Contain(DupeCof.ToString()), "names the duplicate"); + Assert.That(warning, Does.Contain("v457").And.Contain("v1"), "names both versions"); + Assert.That(warning, Does.Contain("CurrentOutfit"), "names the type"); + } + + [Test] + public void one_folder_of_a_type_emits_no_warning() + { + var backend = new FakeAisBackend(Agent); + var root = UUID.Random(); + backend.AddFolder(root, UUID.Zero, "My Inventory", 1, (short)FolderType.Root); + backend.AddFolder(RealCof, root, "Current Outfit", 457, (short)FolderType.CurrentOutfit); + + using var log = new CapturedLog(); + var cof = AisInventory.GetCurrentOutfit(backend, Agent); + + Assert.That(cof.ID, Is.EqualTo(RealCof)); + Assert.That(log.Warnings.Any(w => w.Contains("folders of type")), Is.False, + "the normal case must stay quiet or the warning is noise"); + } + + /// + /// Two folders with no usage history cannot be told apart by version, so the only requirement is that the + /// answer is stable — the same one on every call and on every region, rather than whatever the database + /// happened to return first. + /// + [Test] + public void a_version_tie_is_broken_deterministically_by_id() + { + var lower = new UUID("11111111-1111-4111-8111-111111111111"); + var higher = new UUID("22222222-2222-4222-8222-222222222222"); + + foreach (var insertHigherFirst in new[] { true, false }) + { + var backend = new FakeAisBackend(Agent); + var root = UUID.Random(); + backend.AddFolder(root, UUID.Zero, "My Inventory", 1, (short)FolderType.Root); + if (insertHigherFirst) + { + backend.AddFolder(higher, root, "Current Outfit", 1, (short)FolderType.CurrentOutfit); + backend.AddFolder(lower, root, "Current Outfit", 1, (short)FolderType.CurrentOutfit); + } + else + { + backend.AddFolder(lower, root, "Current Outfit", 1, (short)FolderType.CurrentOutfit); + backend.AddFolder(higher, root, "Current Outfit", 1, (short)FolderType.CurrentOutfit); + } + backend.CurrentOutfitId = higher; + + Assert.That(AisInventory.GetCurrentOutfit(backend, Agent).ID, Is.EqualTo(lower), + $"tie-break must not depend on enumeration order (higher inserted first: {insertHigherFirst})"); + } + } + + /// The same coin flip applies to every system type, so the resolution is general, not COF-specific. + [TestCase(FolderType.Trash)] + [TestCase(FolderType.Clothing)] + [TestCase(FolderType.Object)] + public void duplicates_of_any_system_type_resolve_by_the_same_rule(FolderType type) + { + var backend = new FakeAisBackend(Agent); + var root = UUID.Random(); + var stale = UUID.Random(); + var live = UUID.Random(); + backend.AddFolder(root, UUID.Zero, "My Inventory", 1, (short)FolderType.Root); + backend.AddFolder(stale, root, type.ToString(), 1, (short)type); + backend.AddFolder(live, root, type.ToString(), 92, (short)type); + + var folder = AisInventory.GetSystemFolder(backend, Agent, type); + + Assert.That(folder.ID, Is.EqualTo(live), $"{type} must resolve by the same rule as CurrentOutfit"); + } + + /// + /// With no skeleton to work from, the backend's own answer stands — the fix must not turn a resolvable + /// folder into a null for a backend that has no skeleton (the library backend is one). + /// + [Test] + public void an_empty_skeleton_falls_back_to_the_backend() + { + var backend = new FakeAisBackend(Agent); + var cof = AisInventory.GetCurrentOutfit(backend, Agent); + + Assert.That(cof, Is.Null, "no folders at all means no Current Outfit, not a crash"); + } + + [Test] + public void a_type_absent_from_the_skeleton_falls_back_to_the_backend() + { + var backend = new FakeAisBackend(Agent); + var root = UUID.Random(); + backend.AddFolder(root, UUID.Zero, "My Inventory", 1, (short)FolderType.Root); + + Assert.That(AisInventory.GetCurrentOutfit(backend, Agent), Is.Null); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisErrorHygieneTraversalTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisErrorHygieneTraversalTests.cs new file mode 100644 index 00000000000..f20945afced --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisErrorHygieneTraversalTests.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// AIS-SEC-5. The last two items from the external audit: what an unexpected exception tells the client, and +/// what a cyclic folder graph costs. +/// +/// Fix A — error hygiene. AisHandler.Dispatch caught unexpected exceptions and put +/// ex.Message straight into the response, without logging the exception server-side. So a connector or +/// database fault reached an untrusted client as text — connection strings, host names, SQL fragments, whatever +/// the exception happened to carry — while the one person who needs the stack, an operator reading the region +/// log, got nothing at all. Both halves of that are wrong, and they are the same line. +/// +/// Fix B — traversal. caps depth at 50, which bounds the damage but +/// does not stop a cycle from being re-walked: A→B→A→B… for fifty levels, and every level is a backend +/// round trip, which on this grid is a call to Robust. A visited set stops it at two. +/// +/// Why a visited set is safe in Expand, which is not obvious. Without() clones the +/// expanded map minus the current folder and hands the same clone to every sibling, so it implements +/// ancestor-path exclusion: a folder excluded down one branch is still available to a sibling branch. A +/// single shared visited set is stronger — global once-only. Those two differ exactly when a folder is +/// reachable by two distinct paths, i.e. a diamond. InventoryFolderBase.ParentID is a single scalar and +/// GetFolderContent selects children by ParentID == folderId, so every folder is the child of +/// exactly one parent: the graph is a forest plus possible cycles, and a diamond cannot occur. That is why the +/// substitution is equivalence and not an approximation — and +/// records the condition, so if the data model ever gains multi-parenting the assumption fails loudly here. +/// +[TestFixture] +public class AisErrorHygieneTraversalTests +{ + private const string Cap = "/CAP/5ec50000-0000-4000-8000-000000000000"; + private static readonly UUID Agent = new("a5ec5000-0000-4000-8000-000000000001"); + + private static readonly UUID Root = new("00000000-0000-4000-8000-00000000f001"); + private static readonly UUID Clothing = new("00000000-0000-4000-8000-00000000f002"); + private static readonly UUID Item = new("00000000-0000-4000-8000-00000000f003"); + + // the cycle: each is the other's parent + private static readonly UUID CycA = new("00000000-0000-4000-8000-00000000fa01"); + private static readonly UUID CycB = new("00000000-0000-4000-8000-00000000fa02"); + + /// Text a real connector fault might carry. If this reaches the client, the fix is not working. + private const string Secret = + "Server=10.44.0.9;Database=legiongrid;Uid=root;Pwd=hunter2 -- at MySql.Data.MySqlClient.NativeDriver.Open()"; + + private sealed class HygRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public HygRequest(string verb, string url, OSD body) + { + HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; + InputStream = body is null ? new MemoryStream() : new MemoryStream(OSDParser.SerializeLLSDXmlBytes(body)); + } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => InputStream.Length > 0; + public long ContentLength => InputStream.Length; + public long ContentLength64 => InputStream.Length; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + private static FakeAisBackend World() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddItem(Item, Clothing, "a shirt"); + return b; + } + + /// A↔B: each is the other's parent, so each is the other's only child. + private static FakeAisBackend CyclicWorld() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(CycA, CycB, "A", 1); + b.AddFolder(CycB, CycA, "B", 1); + return b; + } + + private static (int Status, OSDMap Body) Send(FakeAisBackend b, string verb, string path, OSD body = null) + { + var handler = new AisHandler(Cap, Agent, b); + var response = new TestOSHttpResponse(); + handler.Handle(new HygRequest(verb, Cap + path, body), response); + var parsed = OSDParser.DeserializeLLSDXml(response.RawBuffer); + return (response.StatusCode, parsed as OSDMap ?? new OSDMap()); + } + + private static string Flatten(OSDMap body) => OSDParser.SerializeLLSDXmlString(body); + + // ================================================================== Fix A + + [Test] + public void an_unexpected_backend_exception_is_logged_and_not_echoed_to_the_client() + { + using var log = new CapturedLog(); + var b = World(); + b.ThrowOn = label => label.StartsWith("GetItem(", StringComparison.Ordinal) + ? new InvalidOperationException(Secret) + : null; + + var (status, body) = Send(b, "PATCH", $"/item/{Item}", new OSDMap { ["name"] = "x" }); + string wire = Flatten(body); + + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(500), "an unexpected fault is still a 500"); + + // the whole point: nothing of the exception may travel + Assert.That(wire, Does.Not.Contain("hunter2"), "a credential reached the client"); + Assert.That(wire, Does.Not.Contain("10.44.0.9"), "a host address reached the client"); + Assert.That(wire, Does.Not.Contain("MySql"), "an internal type name reached the client"); + Assert.That(wire, Does.Not.Contain(Secret)); + Assert.That(wire, Does.Not.Contain("InvalidOperationException")); + Assert.That(wire, Does.Not.Contain(" at "), "no stack frame text"); + + // and the operator must get what the client did not + var errors = log.Entries(LogLevel.Error); + Assert.That(errors, Is.Not.Empty, "the fault was not logged at Error"); + Assert.That(errors.Any(e => e.Exception is not null), Is.True, + "the exception object itself must reach the logger, not just a message mentioning a fault"); + Assert.That(errors.Any(e => e.Exception is not null && e.Exception.Message.Contains("hunter2")), Is.True, + "and it must be the real exception, so the stack is in the log"); + var text = string.Join(" | ", errors.Select(e => e.Message)); + Assert.That(text, Does.Contain("UpdateItem"), "the log names the operation"); + Assert.That(text, Does.Contain(Agent.ToString()), "and the agent"); + }); + } + + /// + /// Control: an ordinary refusal is untouched. These messages are written by the handler for the client and + /// must keep travelling - the fix is about exception text, not about all error text. + /// + [Test] + public void an_ordinary_error_response_is_unchanged() + { + using var log = new CapturedLog(); + var b = World(); + + var (status, body) = Send(b, "GET", $"/item/{UUID.Random()}"); + + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(404)); + Assert.That(body["message"].AsString(), Does.StartWith("no item "), + "the handler's own 404 text is deliberate and still sent"); + Assert.That(log.Entries(LogLevel.Error), Is.Empty, "a 404 is not a server fault and is not logged as one"); + }); + } + + // ================================================================== Fix B + + [Test] + public void walk_terminates_on_a_cycle_visiting_each_folder_once() + { + var b = CyclicWorld(); + + var walked = AisInventory.Walk(b, Agent, CycA, AisInventory.MaxDepth); + var ids = walked.Select(c => c.Folder.ID).ToList(); + + Assert.Multiple(() => + { + Assert.That(ids, Is.EqualTo(new[] { CycA, CycB }), + "a cycle must be walked once, not re-walked to the depth cap"); + Assert.That(ids.Distinct().Count(), Is.EqualTo(ids.Count), "no folder visited twice"); + + // the cost, which is the real defect: every level is a backend round trip + int contentCalls = b.Calls.Count(c => c.StartsWith("GetFolderContent(", StringComparison.Ordinal)); + Assert.That(contentCalls, Is.LessThanOrEqualTo(3), + $"a cycle cost {contentCalls} GetFolderContent round trips; before AIS-SEC-5 it cost one per level to the cap of {AisInventory.MaxDepth}"); + }); + } + + [Test] + public void the_children_route_on_a_cyclic_graph_answers_the_same_shape_as_before() + { + var b = CyclicWorld(); + + var (status, body) = Send(b, "GET", $"/category/{CycA}/children?depth={AisInventory.MaxDepth}"); + + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(200)); + Assert.That(body["category_id"].AsUUID(), Is.EqualTo(CycA)); + + // A expands and contains B; B expands and contains A as a BARE category - no _embedded - because + // the traversal refuses to re-enter a folder it has already expanded. That is exactly what the + // Without() clone produced, and pinning it is what makes the substitution a non-change. + var aEmbedded = (OSDMap)body["_embedded"]; + var aCats = (OSDMap)aEmbedded["categories"]; + Assert.That(aCats.ContainsKey(CycB.ToString()), Is.True); + + var bNode = (OSDMap)aCats[CycB.ToString()]; + Assert.That(bNode.ContainsKey("_embedded"), Is.True, "B is expanded"); + var bCats = (OSDMap)((OSDMap)bNode["_embedded"])["categories"]; + Assert.That(bCats.ContainsKey(CycA.ToString()), Is.True, "and lists A"); + + var aAgain = (OSDMap)bCats[CycA.ToString()]; + Assert.That(aAgain.ContainsKey("_embedded"), Is.False, + "A appears under B as a bare category - the recursion stops rather than looping"); + }); + } + + /// + /// Control: a deep acyclic tree is unaffected, in both the walk and the response nesting. A visited set and + /// ancestor-path exclusion agree on a forest, and this is the test that would notice if they did not. + /// + [Test] + public void a_deep_acyclic_tree_is_unchanged() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + var chain = new List { Root }; + for (var i = 0; i < 6; i++) + { + var id = new UUID($"00000000-0000-4000-8000-0000000000{(0xb0 + i):x2}"); + b.AddFolder(id, chain[^1], $"level {i}", 1); + chain.Add(id); + } + // a sibling at level 0, so the tree branches rather than being a bare chain + var sibling = new UUID("00000000-0000-4000-8000-0000000000c9"); + b.AddFolder(sibling, Root, "sibling", 1); + + var walked = AisInventory.Walk(b, Agent, Root, AisInventory.MaxDepth).Select(c => c.Folder.ID).ToList(); + + Assert.Multiple(() => + { + Assert.That(walked.Distinct().Count(), Is.EqualTo(walked.Count), "no repeats in a forest"); + Assert.That(walked, Has.Count.EqualTo(8), "root, six levels and the sibling"); + Assert.That(walked[0], Is.EqualTo(Root)); + Assert.That(walked, Does.Contain(sibling)); + + // and the nesting still reaches the bottom of the chain + var (status, body) = Send(b, "GET", $"/category/{Root}/children?depth={AisInventory.MaxDepth}"); + Assert.That(status, Is.EqualTo(200)); + OSDMap node = body; + for (var i = 1; i < chain.Count; i++) + { + var cats = (OSDMap)((OSDMap)node["_embedded"])["categories"]; + Assert.That(cats.ContainsKey(chain[i].ToString()), Is.True, $"level {i} is present"); + node = (OSDMap)cats[chain[i].ToString()]; + } + }); + } + + /// + /// The condition under which ancestor-path exclusion and a visited set would stop agreeing: a folder with two + /// parents. This asserts the data model forbids it, so the equivalence the fix relies on is not folklore. If + /// InventoryFolderBase ever gains multi-parenting, this fails and points at AIS-SEC-5. + /// + [Test] + public void a_diamond_would_be_the_one_case_where_they_differ() + { + var folder = new InventoryFolderBase(UUID.Random(), "f", Agent, -1, UUID.Random(), 1); + var first = folder.ParentID; + folder.ParentID = UUID.Random(); + + Assert.That(folder.ParentID, Is.Not.EqualTo(first), + "ParentID is a single scalar: setting it replaces the parent rather than adding one, so a folder " + + "cannot be the child of two folders and no diamond can reach the traversal"); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisFetchRoutesHttpTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisFetchRoutesHttpTests.cs new file mode 100644 index 00000000000..19f243521b1 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisFetchRoutesHttpTests.cs @@ -0,0 +1,474 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// Every A1 fetch route driven over HTTP against the handler with an in-memory backend, asserting the envelope +/// the viewer parses (AIS-V3-SPEC.md §1c/§1d) and the shapes the A0 fixtures pin. These are the cases that catch +/// real divergence — links kept out of items, the depth shapes, a subset naming a child that is not there, +/// an empty COF, an unknown id — not just the happy paths. +/// +[TestFixture] +public class AisFetchRoutesHttpTests +{ + private const string Cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + private static readonly UUID Agent = new("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + + private static readonly UUID Root = new("00000000-0000-4000-8000-000000000001"); + private static readonly UUID Clothing = new("11111111-1111-4111-8111-111111111111"); + private static readonly UUID Outfits = new("66666666-6666-4666-8666-666666666666"); + private static readonly UUID Party = new("77777777-7777-4777-8777-777777777771"); + private static readonly UUID Cof = new("cccccccc-cccc-4ccc-8ccc-cccccccccccc"); + private static readonly UUID Shirt = new("22222222-2222-4222-8222-222222222222"); + private static readonly UUID Pants = new("22222222-2222-4222-8222-222222222223"); + private static readonly UUID LinkToShirt = new("88888888-8888-4888-8888-888888888888"); + private static readonly UUID LinkToPants = new("88888888-8888-4888-8888-888888888889"); + + /// Minimal IOSHttpRequest: only what the handler reads (verb, raw url, body). + private sealed class FetchTestRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public FetchTestRequest(string verb, string url) { HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } = new MemoryStream(); + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => false; + public long ContentLength => 0; + public long ContentLength64 => 0; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + private static FakeAisBackend Inventory() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddFolder(Outfits, Clothing, "Outfits", 2); + b.AddFolder(Party, Outfits, "Party", 5); + b.AddFolder(Cof, Root, "Current Outfit", 11, (short)FolderType.CurrentOutfit); + b.CurrentOutfitId = Cof; + b.AddItem(Shirt, Clothing, "Blue Shirt"); + b.AddItem(Pants, Clothing, "Grey Pants"); + b.AddLink(LinkToShirt, Cof, "Blue Shirt", Shirt); + b.AddLink(LinkToPants, Cof, "Grey Pants", Pants); + return b; + } + + /// Drives one request end to end through SimpleStreamHandler.Handle and returns status + parsed body. + private static (int Status, OSDMap Body) Get(FakeAisBackend backend, string path, AisMode mode = AisMode.Inventory) + { + var handler = new AisHandler(Cap, Agent, backend, mode); + var request = new FetchTestRequest("GET", Cap + path); + var response = new TestOSHttpResponse(); + handler.Handle(request, response); + Assert.That(response.ContentType, Is.EqualTo("application/llsd+xml"), $"GET {path}"); + var body = OSDParser.DeserializeLLSDXml(response.RawBuffer); + Assert.That(body, Is.InstanceOf(), "the viewer forces 500 on a non-map body (llaisapi.cpp:882-885)"); + return (response.StatusCode, (OSDMap)body); + } + + private static OSDMap Embedded(OSDMap category) => (OSDMap)category["_embedded"]; + private static OSDMap Coll(OSDMap category, string name) => (OSDMap)Embedded(category)[name]; + + // ------------------------------------------------------------------ GET /item/{id} + + [Test] + public void item_route_returns_an_item_and_a_link_route_returns_a_link() + { + var b = Inventory(); + + var (status, item) = Get(b, $"/item/{Shirt}"); + Assert.That(status, Is.EqualTo(200)); + Assert.That(item["item_id"].AsUUID(), Is.EqualTo(Shirt)); + Assert.That(item["parent_id"].AsUUID(), Is.EqualTo(Clothing)); + Assert.That(item.ContainsKey("linked_id"), Is.False, "a real item must not carry linked_id: that would select parseLink (§1c)"); + Assert.That(item["permissions"], Is.InstanceOf()); + Assert.That(item["sale_info"], Is.InstanceOf()); + + var (linkStatus, link) = Get(b, $"/item/{LinkToShirt}"); + Assert.That(linkStatus, Is.EqualTo(200)); + Assert.That(link["linked_id"].AsUUID(), Is.EqualTo(Shirt), "a link carries the target id (§1d)"); + Assert.That(link["item_id"].AsUUID(), Is.EqualTo(LinkToShirt)); + Assert.That(link.ContainsKey("permissions"), Is.False, "the viewer overwrites a link's permissions with defaults (llaisapi.cpp:1278-1283)"); + } + + [Test] + public void an_unknown_item_id_is_404_with_the_error_body_shape() + { + var (status, body) = Get(Inventory(), $"/item/{UUID.Random()}"); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + AssertErrorBody(body, 404); + } + + // ------------------------------------------------------------------ GET /category/{id}/children + + [Test] + public void children_at_depth_0_expands_the_folder_only() + { + var (status, cat) = Get(Inventory(), $"/category/{Clothing}/children?depth=0"); + Assert.That(status, Is.EqualTo(200)); + Assert.That(cat["category_id"].AsUUID(), Is.EqualTo(Clothing)); + Assert.That(cat["parent_id"].AsUUID(), Is.EqualTo(Root)); + Assert.That(cat["version"].AsInteger(), Is.EqualTo(7), "the folder's version, read fresh (T4)"); + + Assert.That(Coll(cat, "items").Count, Is.EqualTo(2)); + Assert.That(Coll(cat, "links").Count, Is.EqualTo(0)); + var categories = Coll(cat, "categories"); + Assert.That(categories.Count, Is.EqualTo(1)); + var child = (OSDMap)categories[Outfits.ToString()]; + Assert.That(child.ContainsKey("_embedded"), Is.False, "at depth 0 a child category is a stub, not expanded"); + } + + [Test] + public void children_at_depth_1_and_2_expand_one_and_two_generations() + { + var b = Inventory(); + + var (_, d1) = Get(b, $"/category/{Clothing}/children?depth=1"); + var outfits1 = (OSDMap)Coll(d1, "categories")[Outfits.ToString()]; + Assert.That(outfits1.ContainsKey("_embedded"), Is.True, "depth 1 expands the child"); + var party1 = (OSDMap)Coll(outfits1, "categories")[Party.ToString()]; + Assert.That(party1.ContainsKey("_embedded"), Is.False, "depth 1 stops at the grandchild"); + + var (_, d2) = Get(b, $"/category/{Clothing}/children?depth=2"); + var outfits2 = (OSDMap)Coll(d2, "categories")[Outfits.ToString()]; + var party2 = (OSDMap)Coll(outfits2, "categories")[Party.ToString()]; + Assert.That(party2.ContainsKey("_embedded"), Is.True, "depth 2 expands the grandchild"); + Assert.That(Coll(party2, "categories").Count, Is.EqualTo(0)); + } + + [Test] + public void an_expanded_category_always_carries_all_three_collections() + { + // spec §1c: the viewer knows a folder's descendent count only from all three, and versions it only then + var (_, cat) = Get(Inventory(), $"/category/{Clothing}/children?depth=2"); + void AssertAllThree(OSDMap c) + { + if (!c.ContainsKey("_embedded")) return; + var e = Embedded(c); + Assert.That(e.ContainsKey("categories"), Is.True, $"{c["name"].AsString()}: categories"); + Assert.That(e.ContainsKey("items"), Is.True, $"{c["name"].AsString()}: items"); + Assert.That(e.ContainsKey("links"), Is.True, $"{c["name"].AsString()}: links"); + foreach (var child in ((OSDMap)e["categories"]).Values) AssertAllThree((OSDMap)child); + } + AssertAllThree(cat); + } + + [Test] + public void an_unknown_category_id_is_404_with_the_error_body_shape() + { + var (status, body) = Get(Inventory(), $"/category/{UUID.Random()}/children?depth=1"); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + AssertErrorBody(body, 404); + } + + // ------------------------------------------------------------------ subset + + [Test] + public void a_subset_returns_only_the_named_children_and_skips_one_that_does_not_exist() + { + var absent = UUID.Random(); + var (status, cat) = Get(Inventory(), $"/category/{Clothing}/children?depth=1&children={Shirt},{absent},{Outfits}"); + Assert.That(status, Is.EqualTo(200), "a named child that is gone does not fail the request"); + + Assert.That(Coll(cat, "items").Keys, Is.EquivalentTo(new[] { Shirt.ToString() }), "Pants was not asked for"); + Assert.That(Coll(cat, "categories").Keys, Is.EquivalentTo(new[] { Outfits.ToString() })); + Assert.That(Coll(cat, "items").ContainsKey(absent.ToString()), Is.False); + Assert.That(Coll(cat, "categories").ContainsKey(absent.ToString()), Is.False); + } + + // ------------------------------------------------------------------ categories + + [Test] + public void the_categories_route_returns_sub_folders_only_and_no_sibling_collections() + { + var (status, cat) = Get(Inventory(), $"/category/{Clothing}/categories?depth=1"); + Assert.That(status, Is.EqualTo(200)); + var e = Embedded(cat); + Assert.That(((OSDMap)e["categories"]).Keys, Is.EquivalentTo(new[] { Outfits.ToString() })); + Assert.That(e.ContainsKey("items"), Is.False, + "a partial view must not carry empty siblings: the viewer would read a descendent count of 1 and version a folder it has not seen (§1c)"); + Assert.That(e.ContainsKey("links"), Is.False); + } + + // ------------------------------------------------------------------ links + + [Test] + public void links_are_their_own_collection_and_items_carries_the_link_targets() + { + // the COF's links point at items that live in Clothing, not in the COF + var (status, cat) = Get(Inventory(), $"/category/{Cof}/links"); + Assert.That(status, Is.EqualTo(200)); + Assert.That(cat["category_id"].AsUUID(), Is.EqualTo(Cof)); + + var links = Coll(cat, "links"); + Assert.That(links.Keys, Is.EquivalentTo(new[] { LinkToShirt.ToString(), LinkToPants.ToString() }), + "keyed by the link's own id, not the target's"); + Assert.That(((OSDMap)links[LinkToShirt.ToString()])["linked_id"].AsUUID(), Is.EqualTo(Shirt)); + + var items = Coll(cat, "items"); + Assert.That(items.Keys, Is.EquivalentTo(new[] { Shirt.ToString(), Pants.ToString() }), + "items carries the link TARGETS — the real items the links resolve to (FetchInvDescHandler.cs:429), never the links"); + foreach (var value in items.Values) + Assert.That(((OSDMap)value).ContainsKey("linked_id"), Is.False, "a link must never appear in the items collection (risk A-R4)"); + } + + [Test] + public void the_cof_alias_resolves_to_the_current_outfit_folder() + { + var b = Inventory(); + var (status, cat) = Get(b, "/category/current/links"); + Assert.That(status, Is.EqualTo(200)); + Assert.That(cat["category_id"].AsUUID(), Is.EqualTo(Cof), "'current' resolved by folder type (T2)"); + Assert.That(b.Calls, Does.Contain("GetInventorySkeleton"), "resolved deterministically over the skeleton (A7)"); + Assert.That(Coll(cat, "links").Count, Is.EqualTo(2)); + } + + [Test] + public void an_empty_cof_returns_an_empty_links_collection_not_an_error() + { + var b = Inventory(); + b.Items.Remove(LinkToShirt); + b.Items.Remove(LinkToPants); + + var (status, cat) = Get(b, "/category/current/links"); + Assert.That(status, Is.EqualTo(200)); + Assert.That(cat["category_id"].AsUUID(), Is.EqualTo(Cof)); + Assert.That(Coll(cat, "links").Count, Is.EqualTo(0), "an empty outfit is an empty links map, so the viewer can count 0 descendents"); + Assert.That(Coll(cat, "items").Count, Is.EqualTo(0)); + } + + [Test] + public void an_agent_with_no_cof_at_all_is_404() + { + var b = Inventory(); + b.CurrentOutfitId = UUID.Zero; + b.Folders.Remove(Cof); + var (status, body) = Get(b, "/category/current/links"); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + AssertErrorBody(body, 404); + } + + // ------------------------------------------------------------------ orphans + + [Test] + public void the_orphans_route_lists_folders_whose_parent_is_gone() + { + var b = Inventory(); + var (status, body) = Get(b, "/orphans"); + Assert.That(status, Is.EqualTo(200)); + Assert.That(body.ContainsKey("category_id"), Is.False, "no top-level object, so the viewer parses _embedded straight (§1c)"); + Assert.That(((OSDMap)Embedded(body)["categories"]).Count, Is.EqualTo(0)); + + b.Folders.Remove(Outfits); + var (_, withOrphan) = Get(b, "/orphans"); + Assert.That(((OSDMap)Embedded(withOrphan)["categories"]).Keys, Is.EquivalentTo(new[] { Party.ToString() })); + } + + // ------------------------------------------------------------------ tid, library, config + + [Test] + public void tid_is_echoed_when_the_request_carries_one() + { + var tid = UUID.Random(); + var (_, cat) = Get(Inventory(), $"/category/{Clothing}/children?depth=0&tid={tid}"); + Assert.That(cat["tid"].AsUUID(), Is.EqualTo(tid)); + + var (_, noTid) = Get(Inventory(), $"/category/{Clothing}/children?depth=0"); + Assert.That(noTid.ContainsKey("tid"), Is.False, "nothing is invented when the request had no tid"); + } + + [Test] + public void the_library_cap_serves_the_same_reads_and_refuses_every_mutation_with_405() + { + var b = Inventory(); + var (status, cat) = Get(b, $"/category/{Clothing}/children?depth=0", AisMode.Library); + Assert.That(status, Is.EqualTo(200), "reads are identical on the library cap"); + Assert.That(cat["category_id"].AsUUID(), Is.EqualTo(Clothing)); + + foreach (var (verb, path) in new (string, string)[] + { + ("POST", $"/category/{Clothing}"), ("PUT", $"/category/{Clothing}/links"), ("DELETE", $"/category/{Clothing}"), + ("DELETE", $"/item/{Shirt}"), ("PATCH", $"/category/{Clothing}"), ("PATCH", $"/item/{Shirt}"), + ("DELETE", $"/category/{Clothing}/children"), + }) + { + var handler = new AisHandler(Cap, Agent, b, AisMode.Library); + var response = new TestOSHttpResponse(); + handler.Handle(new FetchTestRequest(verb, Cap + path), response); + Assert.That(response.StatusCode, Is.EqualTo((int)HttpStatusCode.MethodNotAllowed), $"{verb} {path} on the library"); + var body = (OSDMap)OSDParser.DeserializeLLSDXml(response.RawBuffer); + AssertErrorBody(body, 405); + } + } + + /// + /// The advertisement gate (A5). Both cap names are pinned, the default is off, and the per-region override + /// resolves independently for each region - a grid-wide flip is unacceptable under risk A-R1, because + /// turning AIS on hands the LL viewer's whole inventory path to it with no fallback. + /// + [Test] + public void the_flag_defaults_off_and_resolves_per_region() + { + Assert.That(AISv3Module.CapName, Is.EqualTo("InventoryAPIv3")); + Assert.That(AISv3Module.LibraryCapName, Is.EqualTo("LibraryAPIv3")); + + var module = new AISv3Module(); + module.Initialise(new Nini.Config.IniConfigSource()); + Assert.That(module.Enabled, Is.False, "[AIS] Enabled defaults to false (A-D4, risk A-R1)"); + Assert.DoesNotThrow(() => module.RegionLoaded(null), "a null scene is ignored, not dereferenced"); + + var source = new Nini.Config.IniConfigSource(); + source.AddConfig("AIS").Set("Enabled", "true"); + var enabled = new AISv3Module(); + enabled.Initialise(source); + Assert.That(enabled.Enabled, Is.True); + } + + /// + /// One region can be turned on without touching the others, using the per-region idiom this tree already has + /// (a [<Region Name>] section, as AutoBackupModule.cs:400-406 reads it). Resolution is static and takes a + /// plain config source, so it is provable without building a Scene. + /// + [Test] + public void one_region_can_be_enabled_without_affecting_the_others() + { + var sceneConfig = new Nini.Config.IniConfigSource(); + sceneConfig.AddConfig("AIS").Set("Enabled", "false"); // grid default: off + sceneConfig.AddConfig("Ebony").Set("AIS_Enabled", "true"); + sceneConfig.AddConfig("Transylvania").Set("SomethingElse", "1"); + + Assert.That(AISv3Module.ResolveEnabled(false, sceneConfig, "Ebony"), Is.True, + "the named region opts in"); + Assert.That(AISv3Module.ResolveEnabled(false, sceneConfig, "Transylvania"), Is.False, + "a region with its own section but no AIS_Enabled key stays on the grid default"); + Assert.That(AISv3Module.ResolveEnabled(false, sceneConfig, "Elm"), Is.False, + "a region with no section at all stays on the grid default"); + + // and the override works downwards too: a grid that is on can exempt one region + var optOut = new Nini.Config.IniConfigSource(); + optOut.AddConfig("Elm").Set("AIS_Enabled", "false"); + Assert.That(AISv3Module.ResolveEnabled(true, optOut, "Elm"), Is.False); + Assert.That(AISv3Module.ResolveEnabled(true, optOut, "Ebony"), Is.True); + + Assert.That(AISv3Module.ResolveEnabled(false, null, "Ebony"), Is.False, + "no scene config at all falls back to the grid default"); + } + // ------------------------------------------------------------------ shared assertions + + /// Spec §1f: an error body is a flat map that the viewer's update parser finds nothing to apply in. + private static void AssertErrorBody(OSDMap body, int code) + { + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(code)); + Assert.That(body.ContainsKey("message"), Is.True); + Assert.That(body.ContainsKey("parent_id"), Is.False); + Assert.That(body.ContainsKey("item_id"), Is.False); + Assert.That(body.ContainsKey("category_id"), Is.False); + Assert.That(body.ContainsKey("_embedded"), Is.False); + } + + // ------------------------------------------------------------------ conformance with the A0 fixtures + + private static OSDMap LoadFixture(string name) + { + var path = Path.Combine(AppContext.BaseDirectory, "AIS", "Fixtures", name); + return (OSDMap)OSDParser.DeserializeLLSDXml(File.ReadAllBytes(path)); + } + + /// + /// The live envelopes carry exactly the keys the golden fixtures pin — no extra keys, none missing. The + /// fixtures were corrected in A1 to the field set A-Q1 resolved: type, inv_type and + /// sale_type as integers (LLInventoryItem::fromLLSD accepts either, llinventory.cpp:1108-1135, + /// and integers are what this tree already sends, LLSDInventoryItem.cs:33-68), and no last_owner_id + /// in permissions for the same reason. + /// + [Test] + public void live_envelopes_match_the_golden_fixture_key_sets() + { + var b = Inventory(); + + var (_, item) = Get(b, $"/item/{Shirt}"); + var itemFixture = LoadFixture("item-fetch.xml"); + Assert.That(item.Keys, Is.EquivalentTo(itemFixture.Keys), "item envelope"); + Assert.That(((OSDMap)item["permissions"]).Keys, Is.EquivalentTo(((OSDMap)itemFixture["permissions"]).Keys), "permissions"); + Assert.That(((OSDMap)item["sale_info"]).Keys, Is.EquivalentTo(((OSDMap)itemFixture["sale_info"]).Keys), "sale_info"); + foreach (var key in itemFixture.Keys) + Assert.That(item[key].Type, Is.EqualTo(itemFixture[key].Type), $"item.{key} LLSD type"); + + var (_, cat) = Get(b, $"/category/{Clothing}/children?depth=1"); + var catFixture = LoadFixture("category-fetch.xml"); + Assert.That(cat.Keys, Is.EquivalentTo(catFixture.Keys), "category envelope"); + Assert.That(Embedded(cat).Keys, Is.EquivalentTo(Embedded(catFixture).Keys), "_embedded collections"); + + var (_, link) = Get(b, $"/item/{LinkToShirt}"); + var linkFixture = (OSDMap)((OSDMap)Coll(catFixture, "links")).Values.First(); + Assert.That(link.Keys, Is.EquivalentTo(linkFixture.Keys), "link envelope"); + } + + /// + /// The depth contract of spec 1c-bis, pinned: N licenses exactly N generations below the requested folder, + /// an expanded category carries all three collections and a version, and an unexpanded one is a stub. The + /// viewer versions a category only while its decremented depth is still >= 0 and its descendent count is + /// known (llaisapi.cpp:1380-1407), so our deepest expanded generation lands on exactly 0 - the last value + /// that still counts. A requested depth above the viewer own ceiling of 50 is clamped. + /// + [Test] + public void the_depth_contract_matches_the_viewers_parse() + { + var b = Inventory(); + + // the two depths the viewer actually sends (llinventorymodelbackgroundfetch.cpp:937, :994 with + // llaisapi.cpp:463-474): 0 for a plain fetch, 50 for a recursive one + var (_, d0) = Get(b, $"/category/{Clothing}/children?depth=0"); + Assert.That(((OSDMap)Coll(d0, "categories")[Outfits.ToString()]).ContainsKey("_embedded"), Is.False, + "depth 0 versions the requested folder only; every child must be a stub so the viewer re-queues it"); + + var (_, d50) = Get(b, $"/category/{Clothing}/children?depth=50"); + var outfits = (OSDMap)Coll(d50, "categories")[Outfits.ToString()]; + var party = (OSDMap)Coll(outfits, "categories")[Party.ToString()]; + Assert.That(party.ContainsKey("_embedded"), Is.True, "a recursive fetch expands the whole tree"); + + // every expanded category carries all three collections AND a version, or the viewer cannot count its + // descendents and will never version it (llaisapi.cpp:1466-1482, risk A-R3) + void AssertExpandedAreCountable(OSDMap c) + { + if (!c.ContainsKey("_embedded")) return; + Assert.That(c.ContainsKey("version"), Is.True, c["name"].AsString()); + var e = Embedded(c); + foreach (var name in new[] { "categories", "items", "links" }) + Assert.That(e.ContainsKey(name), Is.True, $"{c["name"].AsString()}: {name}"); + foreach (var child in ((OSDMap)e["categories"]).Values) AssertExpandedAreCountable((OSDMap)child); + } + AssertExpandedAreCountable(d50); + + // a depth beyond the viewer ceiling is clamped, not honoured literally + var (status, clamped) = Get(b, $"/category/{Clothing}/children?depth=100000"); + Assert.That(status, Is.EqualTo(200)); + Assert.That(((OSDMap)Coll((OSDMap)Coll(clamped, "categories")[Outfits.ToString()], "categories")[Party.ToString()]) + .ContainsKey("_embedded"), Is.True, "clamping to 50 still covers any real inventory"); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisHandlerHttpTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisHandlerHttpTests.cs new file mode 100644 index 00000000000..6153025acc0 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisHandlerHttpTests.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// HTTP-level behaviour of the A0 handler (501 on every route with an LLSD error map, spec §1f) and the golden +/// envelope fixtures under AIS/Fixtures (spec §1c): every key present with the right LLSD type. +/// +[TestFixture] +public class AisHandlerHttpTests +{ + private static readonly UUID Agent = new("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + private static readonly UUID Cat = new("11111111-1111-4111-8111-111111111111"); + + /// A backend that must never be reached in A0. + private sealed class ExplodingBackend : IAisInventoryBackend + { + private static Exception Boom() => new InvalidOperationException("A0 handler must not call the backend"); + public InventoryFolderBase GetFolderForType(UUID agentId, FolderType type) => throw Boom(); + public InventoryFolderBase GetFolder(UUID agentId, UUID folderId) => throw Boom(); + public InventoryCollection GetFolderContent(UUID agentId, UUID folderId) => throw Boom(); + public IReadOnlyList GetItems(UUID agentId, IReadOnlyList itemIds) => throw Boom(); + public IReadOnlyList GetSubFolders(UUID agentId, UUID folderId) => throw Boom(); + public IReadOnlyList GetInventorySkeleton(UUID agentId) => throw Boom(); + public InventoryItemBase GetItem(UUID agentId, UUID itemId) => throw Boom(); + public bool AddFolder(InventoryFolderBase folder) => throw Boom(); + public bool AddItem(InventoryItemBase item) => throw Boom(); + public bool UpdateItem(InventoryItemBase item) => throw Boom(); + public AisAssetTransaction ApplyAssetTransaction(UUID agentId, UUID transactionId, InventoryItemBase item) => throw Boom(); + public void OnItemAssetChanged(UUID agentId, UUID itemId, UUID newAssetId) => throw Boom(); + public bool UpdateFolder(InventoryFolderBase folder) => throw Boom(); + public bool DeleteItems(UUID agentId, IReadOnlyList itemIds) => throw Boom(); + public bool DeleteFolders(UUID agentId, IReadOnlyList folderIds, bool onlyIfTrash) => throw Boom(); + public bool PurgeFolder(InventoryFolderBase folder) => throw Boom(); + } + + /// + /// The shared TestOSHttpRequest mock throws on HttpMethod, so the end-to-end test uses this stub: only the + /// members the handler reads are implemented (verb, raw url, path, body); everything else throws. + /// + private sealed class AisTestRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public AisTestRequest(string verb, string url) { HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } = new MemoryStream(); + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => false; + public long ContentLength => 0; + public long ContentLength64 => 0; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public System.Net.IPEndPoint RemoteIPEndPoint => new(System.Net.IPAddress.Loopback, 1); + public System.Net.IPEndPoint LocalIPEndPoint => new(System.Net.IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + private static (AisTestRequest Request, TestOSHttpResponse Response) Http(string verb, string url) + => (new AisTestRequest(verb, url), new TestOSHttpResponse()); + + /// + /// After A4 every operation in the spec is implemented, so the only 501 left is COPY arriving on the + /// **inventory** cap: it is a LibraryAPIv3 operation by design (spec 1a row 5 sends it to {lib}), and the + /// inventory handler has no library source or destination to serve it with. + /// + [Test] + public void copy_on_the_inventory_cap_is_the_only_501_left() + { + var cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + var handler = new AisHandler(cap, Agent, new ExplodingBackend()); + var path = $"/category/{Cat}?tid={UUID.Random()},depth=0"; + var route = AisRouter.Parse("COPY", cap + path, cap); + Assert.That(route.Operation, Is.EqualTo(AisOperation.CopyCategory)); + + var (req, resp) = Http("COPY", cap + path); + handler.Dispatch(route, req, resp); + + Assert.That(resp.StatusCode, Is.EqualTo((int)HttpStatusCode.NotImplemented)); + Assert.That(resp.ContentType, Is.EqualTo("application/llsd+xml")); + var map = (OSDMap)OSDParser.DeserializeLLSDXml(resp.RawBuffer); + Assert.That(map["error_code"].AsInteger(), Is.EqualTo(501)); + Assert.That(map["operation"].AsString(), Is.EqualTo(nameof(AisOperation.CopyCategory))); + // spec 1f: an error body must not look like content the viewer would apply + Assert.That(map.ContainsKey("parent_id"), Is.False); + Assert.That(map.ContainsKey("item_id"), Is.False); + Assert.That(map.ContainsKey("category_id"), Is.False); + Assert.That(map.ContainsKey("_embedded"), Is.False); + } + [Test] + public void the_handler_parses_verb_and_path_from_the_request_itself() + { + // through SimpleStreamHandler.Handle -> ProcessRequest -> AisRouter.Parse(HttpMethod, RawUrl, capPath) + var cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + var handler = new AisHandler(cap, Agent, new ExplodingBackend()); + var (req, resp) = Http("DELETE", cap + $"/category/{Cat}/children"); + handler.Handle(req, resp); + // every operation is implemented as of A4, so this backend throws rather than being left untouched; what + // the test is actually about is that the verb and the path came off the request and reached the router + var map = (OSDMap)OSDParser.DeserializeLLSDXml(resp.RawBuffer); + Assert.That(map["operation"].AsString(), Is.EqualTo(nameof(AisOperation.PurgeDescendents))); + Assert.That(map["path"].AsString(), Is.EqualTo($"/category/{Cat}/children")); + } + + [Test] + public void an_unknown_route_returns_404_with_the_same_body_shape() + { + var cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + var handler = new AisHandler(cap, Agent, new ExplodingBackend()); + var route = AisRouter.Parse("GET", cap + $"/category/{Cat}", cap); + var (req, resp) = Http("GET", cap + $"/category/{Cat}"); + handler.Dispatch(route, req, resp); + Assert.That(resp.StatusCode, Is.EqualTo((int)HttpStatusCode.NotFound)); + var map = (OSDMap)OSDParser.DeserializeLLSDXml(resp.RawBuffer); + Assert.That(map["error_code"].AsInteger(), Is.EqualTo(404)); + } + + // ------------------------------------------------------------------ golden envelope fixtures (spec §1c) + + private static string FixturesDir => Path.Combine(AppContext.BaseDirectory, "AIS", "Fixtures"); + + private static OSDMap LoadFixture(string name) + { + var path = Path.Combine(FixturesDir, name); + Assert.That(File.Exists(path), Is.True, $"fixture {path} must be copied to the output directory"); + var osd = OSDParser.DeserializeLLSDXml(File.ReadAllText(path)); + Assert.That(osd, Is.InstanceOf(), name); + return (OSDMap)osd; + } + + private static void AssertUuidArray(OSDMap map, string key) + { + Assert.That(map.ContainsKey(key), Is.True, key); + Assert.That(map[key], Is.InstanceOf(), key); + foreach (var e in (OSDArray)map[key]) Assert.That(e.Type, Is.EqualTo(OSDType.UUID), $"{key} entries are uuids"); + } + + private static void AssertEmbedded(OSDMap category, bool requireAllThree) + { + Assert.That(category.ContainsKey("_embedded"), Is.True, "_embedded"); + var embedded = (OSDMap)category["_embedded"]; + foreach (var coll in new[] { "categories", "items", "links" }) + { + if (requireAllThree) Assert.That(embedded.ContainsKey(coll), Is.True, $"_embedded.{coll} must be present even when empty (spec §1c: descendent count needs all three)"); + if (embedded.ContainsKey(coll)) Assert.That(embedded[coll], Is.InstanceOf(), $"_embedded.{coll} is a map keyed by uuid string"); + } + } + + [Test] + public void mutation_envelope_fixture_has_every_meta_key_with_the_right_type() + { + // spec §1c meta keys, llaisapi.cpp:1101-1177 + var m = LoadFixture("mutation-envelope.xml"); + AssertUuidArray(m, "_categories_removed"); + AssertUuidArray(m, "_category_items_removed"); + AssertUuidArray(m, "_removed_items"); + AssertUuidArray(m, "_broken_links_removed"); + AssertUuidArray(m, "_created_items"); + AssertUuidArray(m, "_created_categories"); + Assert.That(m["_updated_category_versions"], Is.InstanceOf()); + foreach (KeyValuePair kv in (OSDMap)m["_updated_category_versions"]) + { + Assert.That(UUID.TryParse(kv.Key, out _), Is.True, "keys are category uuids"); + Assert.That(kv.Value.Type, Is.EqualTo(OSDType.Integer), "values are integer versions"); + } + Assert.That(m["_embedded"], Is.InstanceOf()); + } + + [Test] + public void category_fetch_fixture_is_a_category_with_all_three_embedded_collections() + { + // spec §1c content keys, llaisapi.cpp:1203-1206, :1466-1482 + var c = LoadFixture("category-fetch.xml"); + Assert.That(c["category_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(c["parent_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(c["agent_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(c["version"].Type, Is.EqualTo(OSDType.Integer)); + AssertEmbedded(c, requireAllThree: true); + var embedded = (OSDMap)c["_embedded"]; + // links are a separate collection, not items + Assert.That(((OSDMap)embedded["links"]).Count, Is.EqualTo(1)); + Assert.That(((OSDMap)embedded["items"]).Count, Is.EqualTo(1)); + Assert.That(((OSDMap)embedded["categories"]).Count, Is.EqualTo(1)); + foreach (KeyValuePair kv in (OSDMap)embedded["links"]) + { + var link = (OSDMap)kv.Value; + Assert.That(link["linked_id"].Type, Is.EqualTo(OSDType.UUID), "a link carries linked_id (llaisapi.cpp:1185)"); + Assert.That(link["item_id"].AsString(), Is.EqualTo(kv.Key)); + Assert.That(link["parent_id"].AsUUID(), Is.EqualTo(c["category_id"].AsUUID())); + } + foreach (KeyValuePair kv in (OSDMap)embedded["items"]) + { + var item = (OSDMap)kv.Value; + Assert.That(item.ContainsKey("linked_id"), Is.False, "an item is not a link"); + Assert.That(item["item_id"].AsString(), Is.EqualTo(kv.Key)); + Assert.That(item["parent_id"].Type, Is.EqualTo(OSDType.UUID)); + } + foreach (KeyValuePair kv in (OSDMap)embedded["categories"]) + { + var sub = (OSDMap)kv.Value; + Assert.That(sub["category_id"].AsString(), Is.EqualTo(kv.Key)); + Assert.That(sub["version"].Type, Is.EqualTo(OSDType.Integer)); + AssertEmbedded(sub, requireAllThree: true); + } + } + + [Test] + public void cof_links_fixture_is_a_links_only_category() + { + // spec §1c: FT_CURRENT_OUTFIT may carry links alone (llaisapi.cpp:1477-1481); a link may embed its target + var c = LoadFixture("cof-links.xml"); + Assert.That(c["category_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(c["type_default"].AsInteger(), Is.EqualTo((int)FolderType.CurrentOutfit)); + var embedded = (OSDMap)c["_embedded"]; + Assert.That(embedded.ContainsKey("links"), Is.True); + Assert.That(embedded.ContainsKey("items"), Is.False); + foreach (KeyValuePair kv in (OSDMap)embedded["links"]) + { + var link = (OSDMap)kv.Value; + Assert.That(link["linked_id"].Type, Is.EqualTo(OSDType.UUID)); + var linkEmbedded = (OSDMap)link["_embedded"]; + Assert.That(linkEmbedded["item"], Is.InstanceOf(), "_embedded.item inside a link (llaisapi.cpp:1496)"); + Assert.That(((OSDMap)linkEmbedded["item"])["item_id"].AsUUID(), Is.EqualTo(link["linked_id"].AsUUID())); + } + } + + [Test] + public void item_fetch_fixture_is_an_item() + { + var i = LoadFixture("item-fetch.xml"); + Assert.That(i["item_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(i["parent_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(i.ContainsKey("linked_id"), Is.False); + Assert.That(i["permissions"], Is.InstanceOf()); + Assert.That(i["sale_info"], Is.InstanceOf()); + } + + [Test] + public void link_fetch_fixture_is_a_link_with_an_embedded_target() + { + var l = LoadFixture("link-fetch.xml"); + Assert.That(l["linked_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(l["item_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(l["parent_id"].Type, Is.EqualTo(OSDType.UUID)); + Assert.That(((OSDMap)l["_embedded"])["item"], Is.InstanceOf()); + } + + [Test] + public void error_fixture_is_a_flat_map_the_viewer_ignores() + { + // spec §1f + var e = LoadFixture("error.xml"); + Assert.That(e["error_code"].Type, Is.EqualTo(OSDType.Integer)); + Assert.That(e["error_description"].Type, Is.EqualTo(OSDType.String)); + Assert.That(e["message"].Type, Is.EqualTo(OSDType.String)); + Assert.That(e.ContainsKey("parent_id"), Is.False); + Assert.That(e.ContainsKey("_embedded"), Is.False); + var live = AisHandler.ErrorBody(HttpStatusCode.NotImplemented, "x", AisRoute.None); + foreach (var key in new[] { "error_code", "error_description", "message" }) + Assert.That(live[key].Type, Is.EqualTo(e[key].Type), $"live error body and fixture agree on {key}"); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisInventoryTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisInventoryTests.cs new file mode 100644 index 00000000000..a31a94a5ceb --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisInventoryTests.cs @@ -0,0 +1,173 @@ +using System.Linq; +using NUnit.Framework; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// The backend surface the fetch routes need (A1 Part 1), against an in-memory inventory: folder and item reads, +/// sub-folders, the COF resolve, link-target resolution and the depth walk. No Scene, no ScenePresence — the +/// composition is exactly what Phase 2 will reuse on Robust (Ledger P-2). +/// +[TestFixture] +public class AisInventoryTests +{ + private static readonly UUID Agent = new("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + private static readonly UUID Stranger = new("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); + + private static readonly UUID Root = new("00000000-0000-4000-8000-000000000001"); + private static readonly UUID Clothing = new("11111111-1111-4111-8111-111111111111"); + private static readonly UUID Outfits = new("66666666-6666-4666-8666-666666666666"); + private static readonly UUID Party = new("77777777-7777-4777-8777-777777777771"); + private static readonly UUID Cof = new("cccccccc-cccc-4ccc-8ccc-cccccccccccc"); + private static readonly UUID Shirt = new("22222222-2222-4222-8222-222222222222"); + private static readonly UUID Pants = new("22222222-2222-4222-8222-222222222223"); + private static readonly UUID LinkToShirt = new("88888888-8888-4888-8888-888888888888"); + private static readonly UUID LinkToPants = new("88888888-8888-4888-8888-888888888889"); + + /// + /// Root ├ Clothing (shirt, pants) ├ Outfits ├ Party and a COF holding links that point at items in Clothing + /// — i.e. links whose targets live in another folder. + /// + private static FakeAisBackend Inventory() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddFolder(Outfits, Clothing, "Outfits", 2); + b.AddFolder(Party, Outfits, "Party", 5); + b.AddFolder(Cof, Root, "Current Outfit", 11, (short)FolderType.CurrentOutfit); + b.CurrentOutfitId = Cof; + b.AddItem(Shirt, Clothing, "Blue Shirt"); + b.AddItem(Pants, Clothing, "Grey Pants"); + b.AddLink(LinkToShirt, Cof, "Blue Shirt", Shirt); + b.AddLink(LinkToPants, Cof, "Grey Pants", Pants); + return b; + } + + // ---------------- primitives ---------------- + + [Test] + public void get_item_and_get_folder_are_scoped_to_the_owner() + { + var b = Inventory(); + Assert.That(b.GetItem(Agent, Shirt)?.Name, Is.EqualTo("Blue Shirt")); + Assert.That(b.GetItem(Agent, UUID.Random()), Is.Null); + Assert.That(b.GetItem(Stranger, Shirt), Is.Null, "another agent must not read this inventory"); + Assert.That(b.GetFolder(Agent, Clothing)?.Version, Is.EqualTo(7), "the folder version is read fresh (T4)"); + Assert.That(b.GetFolder(Stranger, Clothing), Is.Null); + } + + [Test] + public void get_sub_folders_returns_the_direct_children_only() + { + var b = Inventory(); + var subs = b.GetSubFolders(Agent, Clothing); + Assert.That(subs.Select(f => f.ID), Is.EquivalentTo(new[] { Outfits })); + Assert.That(b.GetSubFolders(Agent, Party), Is.Empty); + } + + [Test] + public void the_cof_resolves_by_folder_type() + { + var b = Inventory(); + var cof = AisInventory.GetCurrentOutfit(b, Agent); + Assert.That(cof?.ID, Is.EqualTo(Cof)); + // A7: resolution moved from the service's own folders[0] to a deterministic scan of the skeleton, because + // an agent can own more than one folder of a type and the service picks arbitrarily between them. + Assert.That(b.Calls, Does.Contain("GetInventorySkeleton"), "resolved by folder type over the skeleton (T2)"); + + b.CurrentOutfitId = UUID.Zero; + b.Folders.Remove(Cof); + Assert.That(AisInventory.GetCurrentOutfit(b, Agent), Is.Null, "an agent with no COF resolves to null, not an exception"); + } + + // ---------------- links ---------------- + + [Test] + public void folder_contents_split_links_out_of_items() + { + var b = Inventory(); + var contents = AisInventory.GetContents(b, Agent, Cof); + Assert.That(contents, Is.Not.Null); + Assert.That(contents.Items, Is.Empty, "the COF holds only links"); + Assert.That(contents.Links.Select(l => l.ID), Is.EquivalentTo(new[] { LinkToShirt, LinkToPants })); + + var clothing = AisInventory.GetContents(b, Agent, Clothing); + Assert.That(clothing.Items.Select(i => i.ID), Is.EquivalentTo(new[] { Shirt, Pants })); + Assert.That(clothing.Links, Is.Empty); + Assert.That(AisInventory.GetContents(b, Agent, UUID.Random()), Is.Null); + } + + [Test] + public void link_targets_in_another_folder_are_resolved_in_one_batched_call() + { + var b = Inventory(); + var contents = AisInventory.GetContents(b, Agent, Cof); + b.Calls.Clear(); + + var targets = AisInventory.ResolveLinkTargets(b, Agent, contents.Links); + + Assert.That(targets.Select(t => t.ID), Is.EquivalentTo(new[] { Shirt, Pants }), + "the targets live in Clothing, not in the folder the links are in"); + Assert.That(b.Calls.Count(c => c.StartsWith("GetItems")), Is.EqualTo(1), + "one GetMultipleItems for every link, as FetchInvDescHandler.ProcessLinks does (T5)"); + Assert.That(b.Calls, Does.Contain("GetItems[2]")); + } + + [Test] + public void a_broken_link_resolves_to_nothing_and_a_link_to_a_link_is_dropped() + { + var b = Inventory(); + var broken = new UUID("99999999-9999-4999-8999-999999999991"); + b.AddLink(broken, Cof, "gone", UUID.Random()); // target does not exist + var chained = new UUID("99999999-9999-4999-8999-999999999992"); + b.AddLink(chained, Cof, "link to a link", LinkToShirt); // target is itself a link + + var contents = AisInventory.GetContents(b, Agent, Cof); + var targets = AisInventory.ResolveLinkTargets(b, Agent, contents.Links); + + Assert.That(targets.Select(t => t.ID), Is.EquivalentTo(new[] { Shirt, Pants }), + "broken links resolve to nothing; links to links are dropped as the descendents cap drops them"); + } + + // ---------------- depth ---------------- + + [Test] + public void walk_expands_exactly_the_requested_number_of_generations() + { + var b = Inventory(); + + var d0 = AisInventory.Walk(b, Agent, Clothing, 0); + Assert.That(d0.Select(c => c.Folder.ID), Is.EqualTo(new[] { Clothing }), "depth 0 expands the folder itself only"); + + var d1 = AisInventory.Walk(b, Agent, Clothing, 1); + Assert.That(d1.Select(c => c.Folder.ID), Is.EqualTo(new[] { Clothing, Outfits })); + + var d2 = AisInventory.Walk(b, Agent, Clothing, 2); + Assert.That(d2.Select(c => c.Folder.ID), Is.EqualTo(new[] { Clothing, Outfits, Party }), + "depth 2 reaches the grandchild"); + + var d5 = AisInventory.Walk(b, Agent, Clothing, 5); + Assert.That(d5.Select(c => c.Folder.ID), Is.EqualTo(new[] { Clothing, Outfits, Party }), + "a depth deeper than the tree stops at the tree"); + + Assert.That(AisInventory.Walk(b, Agent, UUID.Random(), 3), Is.Empty, "an unknown folder walks to nothing"); + } + + // ---------------- orphans ---------------- + + [Test] + public void orphans_are_the_folders_whose_parent_is_gone() + { + var b = Inventory(); + Assert.That(AisInventory.FindOrphans(b, Agent).Folders, Is.Empty, "a consistent tree has no orphans"); + + b.Folders.Remove(Outfits); // Party's parent disappears + var orphans = AisInventory.FindOrphans(b, Agent); + Assert.That(orphans.Folders.Select(f => f.ID), Is.EquivalentTo(new[] { Party })); + Assert.That(orphans.Items, Is.Empty, "orphaned items are not reported: the service has no query for them"); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisMutationHttpTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisMutationHttpTests.cs new file mode 100644 index 00000000000..97bc2555a2a --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisMutationHttpTests.cs @@ -0,0 +1,685 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// The A2 mutations driven over HTTP: PATCH and DELETE on a single item or category, asserting the delta envelope +/// the viewer actually applies (AIS-V3-SPEC.md §1d-bis) rather than just a 200. The cases that matter are the +/// ones where a plausible-looking response would be silently useless: an update that omits +/// _updated_category_versions is discarded by the viewer, a version read before the write is stale, and a +/// removal under the wrong key does nothing. +/// +[TestFixture] +public class AisMutationHttpTests +{ + private const string Cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + private static readonly UUID Agent = new("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + + private static readonly UUID Root = new("00000000-0000-4000-8000-000000000001"); + private static readonly UUID Clothing = new("11111111-1111-4111-8111-111111111111"); + private static readonly UUID Outfits = new("66666666-6666-4666-8666-666666666666"); + private static readonly UUID Party = new("77777777-7777-4777-8777-777777777771"); + private static readonly UUID Shirt = new("22222222-2222-4222-8222-222222222222"); + private static readonly UUID PartyHat = new("33333333-3333-4333-8333-333333333331"); + + private sealed class MutTestRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + /// + /// The body is , not . It used to be OSDMap, and that + /// narrowing was not harmless: the one caller with a non-map body wrote body as OSDMap to satisfy + /// it, which silently became null and sent no body at all. A slam body is a bare LLSD + /// array (llappearancemgr.cpp:2209-2245) — so the one shape the viewer really sends was + /// exactly the shape this harness could not express. + /// + public MutTestRequest(string verb, string url, OSD body = null) + { + HttpMethod = verb; + Url = new Uri("http://sim.test" + url); + RawUrl = url; + InputStream = body is null ? new MemoryStream() : new MemoryStream(OSDParser.SerializeLLSDXmlBytes(body)); + } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => InputStream.Length > 0; + public long ContentLength => InputStream.Length; + public long ContentLength64 => InputStream.Length; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + /// Root ├ Clothing (shirt) ├ Outfits ├ Party (party hat). + private static FakeAisBackend Inventory() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddFolder(Outfits, Clothing, "Outfits", 2); + b.AddFolder(Party, Outfits, "Party", 5); + b.AddItem(Shirt, Clothing, "Blue Shirt"); + b.AddItem(PartyHat, Party, "Party Hat"); + return b; + } + + private static (int Status, OSDMap Body) Send(FakeAisBackend backend, string verb, string path, OSD body = null, AisMode mode = AisMode.Inventory) + { + var handler = new AisHandler(Cap, Agent, backend, mode); + var response = new TestOSHttpResponse(); + handler.Handle(new MutTestRequest(verb, Cap + path, body), response); + Assert.That(response.ContentType, Is.EqualTo("application/llsd+xml"), $"{verb} {path}"); + var parsed = OSDParser.DeserializeLLSDXml(response.RawBuffer); + Assert.That(parsed, Is.InstanceOf(), "the viewer forces 500 on a non-map body (llaisapi.cpp:882-885)"); + return (response.StatusCode, (OSDMap)parsed); + } + + private static OSDMap Versions(OSDMap body) + { + Assert.That(body.ContainsKey("_updated_category_versions"), Is.True, + "without this key the viewer skips the folder entirely: 'Skipping version increment for non-updated category' (llaisapi.cpp:1625-1629)"); + return (OSDMap)body["_updated_category_versions"]; + } + + // ------------------------------------------------------------------ PATCH /item + + [Test] + public void patching_an_item_name_returns_the_item_as_content_with_its_parents_new_version() + { + var b = Inventory(); + var before = b.Folders[Clothing].Version; + + var (status, body) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["name"] = "Red Shirt" }); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Items[Shirt].Name, Is.EqualTo("Red Shirt"), "the write actually happened"); + + // there is no "updated" delta key: an updated item is top-level content (§1d-bis) + Assert.That(body["item_id"].AsUUID(), Is.EqualTo(Shirt)); + Assert.That(body["parent_id"].AsUUID(), Is.EqualTo(Clothing)); + Assert.That(body["name"].AsString(), Is.EqualTo("Red Shirt")); + Assert.That(body.ContainsKey("_updated_items"), Is.False, "no such key exists in the viewer"); + + var versions = Versions(body); + Assert.That(versions.ContainsKey(Clothing.ToString()), Is.True, "the parent must be listed or the update is discarded"); + Assert.That(versions[Clothing.ToString()].AsInteger(), Is.EqualTo(before + 1), "and it must be the post-write version"); + } + + [Test] + public void the_reported_version_is_read_after_the_write_not_before() + { + // a backend that bumps again underneath the handler proves the version is not cached from before the write + var b = Inventory(); + b.OnWrite = () => b.Folders[Clothing].Version += 10; + + var (_, body) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["name"] = "Green Shirt" }); + + Assert.That(Versions(body)[Clothing.ToString()].AsInteger(), Is.EqualTo(b.Folders[Clothing].Version), + "the version reported is whatever GetFolder returns after the write, read fresh (tree state T4)"); + } + + /// + /// The viewer sends the item's whole asLLSD map (A-Q3, llviewerinventory.cpp:435-454), so a + /// body is mostly keys carrying values that have not changed. The ones this tree has no column for — + /// thumbnail, favorite, created_at — and the invariants type / inv_type / + /// parent_id are ignored rather than refused, because refusing would fail every ordinary rename. + /// A16 moved asset_id, hash_id and permissions out of this set; the two here are still + /// no-ops for their own reasons — an unknown transaction has no asset yet, and owner_mask is not a + /// client-settable field. + /// + [Test] + public void a_patch_carrying_fields_this_tree_cannot_store_is_ignored_not_refused() + { + var b = Inventory(); + var body = new OSDMap + { + ["name"] = "Renamed", + ["item_id"] = Shirt, + ["parent_id"] = Clothing, + ["hash_id"] = UUID.Random(), + ["thumbnail"] = new OSDMap { ["asset_id"] = UUID.Random() }, + ["favorite"] = new OSDMap { ["toggled"] = true }, + ["type"] = 5, + ["inv_type"] = 18, + ["created_at"] = 1756900000, + ["permissions"] = new OSDMap { ["owner_mask"] = 0 }, + }; + + var (status, response) = Send(b, "PATCH", $"/item/{Shirt}", body); + + Assert.That(status, Is.EqualTo(200), "an unstorable field must not fail an ordinary rename"); + Assert.That(b.Items[Shirt].Name, Is.EqualTo("Renamed")); + Assert.That(b.Items[Shirt].CurrentPermissions, Is.EqualTo(0x7fffffffu), "owner_mask is never taken from a body"); + Assert.That(response["item_id"].AsUUID(), Is.EqualTo(Shirt)); + } + + [Test] + public void patching_sale_info_and_flags_is_applied() + { + var b = Inventory(); + var (status, _) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap + { + ["sale_info"] = new OSDMap { ["sale_price"] = 250, ["sale_type"] = 2 }, + ["flags"] = 4, + }); + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Items[Shirt].SalePrice, Is.EqualTo(250)); + Assert.That(b.Items[Shirt].SaleType, Is.EqualTo(2)); + Assert.That(b.Items[Shirt].Flags, Is.EqualTo(4u)); + } + + // ------------------------------------------------------------------ A16: the asset id + + /// + /// The A16 defect, reproduced. A wearable save uploads a new asset and then PATCHes the item; the PATCH + /// answered 200 and the item kept its old asset, so the edit survived only in the viewer's cache. Live on + /// 2026-09-05: asset 44f77403 uploaded at 16:27:24,899, PATCH of item e2c03d62 answered 200 at :24,958, and + /// the row still read 637022fb. The legacy UDP path did persist it, which is what made the bug look + /// intermittent. + /// + [Test] + public void patching_asset_id_persists_it() + { + var b = Inventory(); + var uploaded = UUID.Random(); + var before = b.Items[Shirt].AssetID; + var versionBefore = b.Folders[Clothing].Version; + + var (status, body) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["asset_id"] = uploaded }); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Items[Shirt].AssetID, Is.Not.EqualTo(before), "the item still carries the asset it had before the save"); + Assert.That(b.Items[Shirt].AssetID, Is.EqualTo(uploaded)); + Assert.That(body["asset_id"].AsUUID(), Is.EqualTo(uploaded), "and the response says so"); + Assert.That(Versions(body)[Clothing.ToString()].AsInteger(), Is.EqualTo(versionBefore + 1), + "an asset change is a change: the parent's version must move or the viewer never re-reads the item"); + } + + /// + /// The path a wearable save actually takes. LLViewerInventoryItem::updateServer erases + /// asset_id from the body and sends hash_id — the xfer transaction id — in its place + /// (llviewerinventory.cpp:445-452), so the server never sees an asset id at all and must ask the + /// asset-transaction module which asset that transaction produced. + /// + [Test] + public void patching_hash_id_resolves_the_transaction_to_the_uploaded_asset() + { + var b = Inventory(); + var transaction = UUID.Random(); + var uploaded = UUID.Random(); + b.Transactions[transaction] = uploaded; + var versionBefore = b.Folders[Clothing].Version; + + var (status, body) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["hash_id"] = transaction }); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Items[Shirt].AssetID, Is.EqualTo(uploaded)); + Assert.That(body["asset_id"].AsUUID(), Is.EqualTo(uploaded), "the response carries the resolved asset, not the transaction"); + Assert.That(Versions(body)[Clothing.ToString()].AsInteger(), Is.EqualTo(versionBefore + 1)); + } + + [Test] + public void the_map_fields_are_stored_before_the_transaction_is_handed_over() + { + // the legacy route's order: InventoryService.UpdateItem (Scene.Inventory.cs:576), then + // HandleItemUpdateFromTransaction (:579-582), which stores again once the xfer completes + var b = Inventory(); + var transaction = UUID.Random(); + b.Transactions[transaction] = UUID.Random(); + + Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["name"] = "Saved Shirt", ["hash_id"] = transaction }); + + var order = b.Calls.Where(c => c.StartsWith("UpdateItem(") || c.StartsWith("ApplyAssetTransaction(")).ToList(); + Assert.That(order.First(), Does.StartWith("UpdateItem("), "the body's own fields are stored first"); + Assert.That(order, Does.Contain($"ApplyAssetTransaction({transaction})")); + Assert.That(b.Items[Shirt].Name, Is.EqualTo("Saved Shirt")); + } + + [Test] + public void an_unresolvable_transaction_leaves_the_asset_alone_and_still_answers_200() + { + // a region with no transaction module, or the library: the item keeps what it had + var b = Inventory(); + b.ResolvesTransactions = false; + var before = b.Items[Shirt].AssetID; + + var (status, _) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["hash_id"] = UUID.Random() }); + + Assert.That(status, Is.EqualTo(200), "an unresolvable hash must not fail the rest of the patch"); + Assert.That(b.Items[Shirt].AssetID, Is.EqualTo(before)); + } + + // ------------------------------------------------------------------ A19: a refused save is not a 200 + + [Test] + public void a_refused_transaction_answers_403_and_does_not_bump_the_category_version() + { + // 2026-09-06 09:52:55: a wearable referencing a library texture was refused by the uploader + // ("REJECTED update with texture 00000000-0000-2222-3333-100000001002 ... because they do not own the + // texture") and the cap still answered "UpdateItem -> 200" with the item's asset unchanged. The viewer was + // told the save had succeeded. + var b = Inventory(); + var transaction = UUID.Random(); + b.RefusedTransactions.Add(transaction); + var assetBefore = b.Items[Shirt].AssetID; + var versionBefore = b.Folders[Clothing].Version; + + var (status, body) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["hash_id"] = transaction }); + + Assert.That(status, Is.EqualTo(403), "a refused save must not be reported as a success"); + Assert.That(b.Items[Shirt].AssetID, Is.EqualTo(assetBefore), "the item still points at its previous asset"); + Assert.That(body.ContainsKey("_updated_category_versions"), Is.False, + "a failed save must not advance the folder version the viewer holds, or its next fetch skips the folder " + + "and it never sees the true state (llaisapi.cpp:1625-1629)"); + Assert.That(b.Folders[Clothing].Version, Is.EqualTo(versionBefore)); + } + + [Test] + public void a_refused_transaction_does_not_fire_the_worn_wearable_rebake() + { + // S9's hook is keyed on the asset actually changing, so a refusal must leave it silent - it did before this + // fix and it must keep doing so, or a refused edit would cost a bake of an outfit that did not change. + var b = Inventory(); + var transaction = UUID.Random(); + b.RefusedTransactions.Add(transaction); + + Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["hash_id"] = transaction }); + + Assert.That(b.AssetChanges, Is.Empty, "nothing changed, so nothing may queue an appearance save"); + } + + [Test] + public void a_refused_transaction_still_leaves_the_body_fields_it_already_stored() + { + // The map fields are applied and stored before the transaction is handed over (the legacy route's order), + // so a rename that travelled with a refused asset is already written. The 403 is about the ASSET; it does + // not pretend the rest of the PATCH did not happen, and the viewer re-reads the item either way. + var b = Inventory(); + var transaction = UUID.Random(); + b.RefusedTransactions.Add(transaction); + + var (status, _) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap + { + ["name"] = "Renamed Shirt", + ["hash_id"] = transaction, + }); + + Assert.That(status, Is.EqualTo(403)); + Assert.That(b.Items[Shirt].Name, Is.EqualTo("Renamed Shirt")); + } + + [Test] + public void patching_permissions_applies_next_everyone_and_group_masked_by_base() + { + // set_default_permissions changes exactly these three and calls updateServer (llagentwearables.cpp:62-78) + var b = Inventory(); + b.Items[Shirt].BasePermissions = 0x0000000e; // copy | modify | transfer, nothing else + b.Items[Shirt].NextPermissions = 0; + b.Items[Shirt].EveryOnePermissions = 0; + b.Items[Shirt].GroupPermissions = 0; + + var (status, _) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap + { + ["permissions"] = new OSDMap + { + ["next_owner_mask"] = unchecked((int)0xffffffff), + ["everyone_mask"] = unchecked((int)0xffffffff), + ["group_mask"] = 0x00000008, + }, + }); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Items[Shirt].NextPermissions, Is.EqualTo(0x0000000eu), "masked by base (Scene.Inventory.cs:539)"); + Assert.That(b.Items[Shirt].EveryOnePermissions, Is.EqualTo(0x0000000eu)); + Assert.That(b.Items[Shirt].GroupPermissions, Is.EqualTo(0x00000008u)); + } + + [Test] + public void patching_permissions_never_takes_base_or_current_from_the_body() + { + var b = Inventory(); + var (status, _) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap + { + ["permissions"] = new OSDMap { ["base_mask"] = 0, ["owner_mask"] = 0, ["next_owner_mask"] = 8 }, + }); + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Items[Shirt].BasePermissions, Is.EqualTo(0x7fffffffu), "base is not a client-settable field"); + Assert.That(b.Items[Shirt].CurrentPermissions, Is.EqualTo(0x7fffffffu), "nor is current"); + } + + [Test] + public void patching_permissions_leaves_the_export_bit_as_it_stands() + { + var b = Inventory(); + b.Items[Shirt].BasePermissions = 0x7fffffff; + b.Items[Shirt].EveryOnePermissions = 0; + + Send(b, "PATCH", $"/item/{Shirt}", new OSDMap + { + ["permissions"] = new OSDMap { ["everyone_mask"] = (int)OpenSim.Framework.PermissionMask.Export }, + }); + + Assert.That(b.Items[Shirt].EveryOnePermissions & (uint)OpenSim.Framework.PermissionMask.Export, Is.EqualTo(0u), + "export is not granted through this route; the legacy path guards it with a creator check we do not reproduce here"); + } + + [Test] + public void a_patch_that_changes_nothing_writes_nothing() + { + var b = Inventory(); + var asset = b.Items[Shirt].AssetID; + var version = b.Folders[Clothing].Version; + + var (status, _) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap + { + ["name"] = "Blue Shirt", + ["asset_id"] = asset, + }); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Calls, Does.Not.Contain($"UpdateItem({Shirt})"), "an unchanged body must not bump the folder"); + Assert.That(b.Folders[Clothing].Version, Is.EqualTo(version)); + } + + [Test] + public void patching_an_unknown_item_is_404() + { + var (status, body) = Send(Inventory(), "PATCH", $"/item/{UUID.Random()}", new OSDMap { ["name"] = "x" }); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + AssertErrorBody(body, 404); + } + + // ------------------------------------------------------------------ PATCH /category + + [Test] + public void patching_a_category_lists_both_the_category_and_its_parent() + { + var b = Inventory(); + var (status, body) = Send(b, "PATCH", $"/category/{Outfits}", new OSDMap { ["name"] = "My Outfits" }); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Folders[Outfits].Name, Is.EqualTo("My Outfits")); + Assert.That(body["category_id"].AsUUID(), Is.EqualTo(Outfits)); + Assert.That(body["name"].AsString(), Is.EqualTo("My Outfits")); + + var versions = Versions(body); + Assert.That(versions.Keys, Is.EquivalentTo(new[] { Outfits.ToString(), Clothing.ToString() }), + "parseCategory creates zero-delta entries for the category AND its parent (llaisapi.cpp:1419-1428), so both must be listed"); + } + + [Test] + public void patching_an_unknown_category_is_404() + { + var (status, body) = Send(Inventory(), "PATCH", $"/category/{UUID.Random()}", new OSDMap { ["name"] = "x" }); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + AssertErrorBody(body, 404); + } + + // ------------------------------------------------------------------ DELETE /item + + [Test] + public void deleting_an_item_reports_removed_items_and_the_parents_new_version() + { + var b = Inventory(); + var before = b.Folders[Clothing].Version; + + var (status, body) = Send(b, "DELETE", $"/item/{Shirt}"); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Items.ContainsKey(Shirt), Is.False, "the item is gone"); + Assert.That(body.ContainsKey("_removed_items"), Is.True); + Assert.That(((OSDArray)body["_removed_items"]).Select(o => o.AsUUID()), Is.EquivalentTo(new[] { Shirt })); + Assert.That(body.ContainsKey("item_id"), Is.False, "a delete returns no content, only deltas"); + Assert.That(Versions(body)[Clothing.ToString()].AsInteger(), Is.EqualTo(before + 1)); + } + + [Test] + public void deleting_an_unknown_item_is_404() + { + var (status, body) = Send(Inventory(), "DELETE", $"/item/{UUID.Random()}"); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + AssertErrorBody(body, 404); + } + + // ------------------------------------------------------------------ DELETE /category + + [Test] + public void deleting_a_category_names_only_the_folder_and_lets_the_viewer_purge_its_descendents() + { + var b = Inventory(); + var before = b.Folders[Clothing].Version; + + var (status, body) = Send(b, "DELETE", $"/category/{Outfits}"); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Folders.ContainsKey(Outfits), Is.False); + Assert.That(b.Folders.ContainsKey(Party), Is.False, "descendents are gone on the server"); + Assert.That(b.Items.ContainsKey(PartyHat), Is.False); + + var removed = ((OSDArray)body["_categories_removed"]).Select(o => o.AsUUID()).ToList(); + Assert.That(removed, Is.EquivalentTo(new[] { Outfits }), + "only the folder is named: onObjectDeletedFromServer purges descendents locally (llinventorymodel.cpp:2019-2023), so they are implied"); + Assert.That(body.ContainsKey("_removed_items"), Is.False, "the descendent item is not enumerated either"); + Assert.That(Versions(body)[Clothing.ToString()].AsInteger(), Is.EqualTo(before + 1), "the deleted folder's PARENT is the one that changed"); + Assert.That(Versions(body).ContainsKey(Outfits.ToString()), Is.False, + "the deleted folder must not be listed: the viewer would dereference a category it has just removed (Ledger A-R6)"); + } + + /// + /// A2b, replacing A2's 409: the handler passes onlyIfTrash: false through the IInventoryService overload + /// added in A2b, so a folder outside Trash is deleted rather than silently skipped. The trash gate is armed + /// on the backend here, so this fails if the handler ever stops passing the flag. + /// + [Test] + public void a_folder_outside_trash_is_deleted_because_the_handler_passes_only_if_trash_false() + { + var b = Inventory(); + b.DeleteFoldersOnlyIfTrash = true; // armed: with onlyIfTrash true this folder would be skipped + var before = b.Folders[Clothing].Version; + + var (status, body) = Send(b, "DELETE", $"/category/{Outfits}"); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Folders.ContainsKey(Outfits), Is.False, "the folder is gone even though it was never in Trash"); + Assert.That(b.Calls, Does.Contain("DeleteFolders[1, onlyIfTrash=False]"), + "the AIS route must ask for the unrestricted delete (Ledger A-Q9)"); + Assert.That(((OSDArray)body["_categories_removed"]).Select(o => o.AsUUID()), Is.EquivalentTo(new[] { Outfits })); + Assert.That(Versions(body)[Clothing.ToString()].AsInteger(), Is.EqualTo(before + 1)); + } + + /// + /// Protected folders are still refused, server side. The viewer refuses to send RemoveCategory for a folder + /// whose type lookupIsProtectedType accepts (llviewerinventory.cpp:1557-1561), so this is defence in depth; + /// the exact membership of that predicate is UNVERIFIED (llfoldertype.cpp is not a permitted read), so the + /// server rule is "the root, or any system type except Outfit". + /// + [Test] + public void a_protected_folder_is_refused_with_403_and_nothing_is_deleted() + { + var b = Inventory(); + + // a system-typed folder + var (status, body) = Send(b, "DELETE", $"/category/{Clothing}"); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.Forbidden)); + AssertErrorBody(body, 403); + Assert.That(b.Folders.ContainsKey(Clothing), Is.True); + Assert.That(b.Calls.Any(c => c.StartsWith("DeleteFolders")), Is.False, "the backend is never asked"); + + // the inventory root + var (rootStatus, _) = Send(b, "DELETE", $"/category/{Root}"); + Assert.That(rootStatus, Is.EqualTo((int)HttpStatusCode.Forbidden)); + Assert.That(b.Folders.ContainsKey(Root), Is.True); + } + + /// A saved outfit is ordinary user data and must stay deletable. + [Test] + public void a_saved_outfit_folder_is_not_protected() + { + var b = Inventory(); + var outfit = new UUID("55555555-5555-4555-8555-555555555551"); + b.AddFolder(outfit, Outfits, "Beach Outfit", 1, (short)FolderType.Outfit); + + var (status, body) = Send(b, "DELETE", $"/category/{outfit}"); + + Assert.That(status, Is.EqualTo(200), "FolderType.Outfit is a user folder, not a protected system one"); + Assert.That(b.Folders.ContainsKey(outfit), Is.False); + Assert.That(((OSDArray)body["_categories_removed"]).Select(o => o.AsUUID()), Is.EquivalentTo(new[] { outfit })); + } + + /// + /// A3 Part 0: the protected set is the viewer's own table, not a guess. Every type whose PROTECTED column is + /// false in LLFolderDictionary (llfoldertype.cpp:85-127) must be deletable, and every other type - including + /// one the viewer's table has never heard of, which lookupIsProtectedType defaults to protected (:154-162) - + /// must be refused. + /// + [TestCase((short)FolderType.None, false, TestName = "protected_set: FT_NONE is deletable")] + [TestCase((short)FolderType.Outfit, false, TestName = "protected_set: FT_OUTFIT is deletable")] + [TestCase((short)FolderType.MarketplaceListings, false, TestName = "protected_set: FT_MARKETPLACE_LISTINGS is deletable")] + [TestCase((short)FolderType.MarkplaceStock, false, TestName = "protected_set: FT_MARKETPLACE_STOCK is deletable")] + [TestCase((short)30, false, TestName = "protected_set: an ensemble type is deletable")] + [TestCase((short)FolderType.Clothing, true, TestName = "protected_set: FT_CLOTHING is protected")] + [TestCase((short)FolderType.Trash, true, TestName = "protected_set: FT_TRASH is protected")] + [TestCase((short)FolderType.CurrentOutfit, true, TestName = "protected_set: FT_CURRENT_OUTFIT is protected")] + [TestCase((short)FolderType.MyOutfits, true, TestName = "protected_set: FT_MY_OUTFITS is protected")] + [TestCase((short)FolderType.Favorites, true, TestName = "protected_set: FT_FAVORITE is protected")] + [TestCase((short)FolderType.Settings, true, TestName = "protected_set: FT_SETTINGS is protected")] + [TestCase((short)FolderType.Suitcase, true, TestName = "protected_set: a type the viewer's table lacks defaults to protected")] + public void the_protected_set_is_the_viewers_table(short folderType, bool expectRefused) + { + var b = Inventory(); + var id = new UUID("44444444-4444-4444-8444-44444444444" + (folderType < 0 ? "0" : "1")); + b.AddFolder(id, Outfits, "subject", 1, folderType); + + var (status, _) = Send(b, "DELETE", $"/category/{id}"); + + if (expectRefused) + { + Assert.That(status, Is.EqualTo((int)HttpStatusCode.Forbidden), $"type {folderType} must be protected"); + Assert.That(b.Folders.ContainsKey(id), Is.True); + } + else + { + Assert.That(status, Is.EqualTo(200), $"type {folderType} must be deletable"); + Assert.That(b.Folders.ContainsKey(id), Is.False); + } + } + + /// The verification survives: a service that really does nothing is a 500, not a false 200. + [Test] + public void a_delete_the_service_did_not_perform_is_reported_as_a_failure() + { + var b = Inventory(); + b.AllowWrite = false; // DeleteFolders returns false and changes nothing + + var (status, body) = Send(b, "DELETE", $"/category/{Outfits}"); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.InternalServerError)); + AssertErrorBody(body, 500); + Assert.That(b.Folders.ContainsKey(Outfits), Is.True); + } + + [Test] + public void deleting_an_unknown_category_is_404() + { + var (status, body) = Send(Inventory(), "DELETE", $"/category/{UUID.Random()}"); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + AssertErrorBody(body, 404); + } + + // ------------------------------------------------------------------ cross-cutting + + [Test] + public void tid_is_echoed_on_a_mutation() + { + var tid = UUID.Random(); + var (_, body) = Send(Inventory(), "DELETE", $"/item/{Shirt}?tid={tid}"); + Assert.That(body["tid"].AsUUID(), Is.EqualTo(tid)); + } + + [Test] + public void every_mutation_through_the_library_cap_is_405_and_changes_nothing() + { + foreach (var (verb, path) in new (string, string)[] + { + ("PATCH", $"/item/{Shirt}"), ("PATCH", $"/category/{Outfits}"), + ("DELETE", $"/item/{Shirt}"), ("DELETE", $"/category/{Outfits}"), + }) + { + var b = Inventory(); + var (status, body) = Send(b, verb, path, new OSDMap { ["name"] = "hacked" }, AisMode.Library); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.MethodNotAllowed), $"{verb} {path}"); + AssertErrorBody(body, 405); + Assert.That(b.Items[Shirt].Name, Is.EqualTo("Blue Shirt"), "nothing was written"); + Assert.That(b.Folders[Outfits].Name, Is.EqualTo("Outfits")); + Assert.That(b.Calls.Any(c => c.StartsWith("Update") || c.StartsWith("Delete")), Is.False, + "the backend is never even asked"); + } + } + + [Test] + public void a_service_that_refuses_the_write_is_a_500_not_a_silent_success() + { + var b = Inventory(); + b.AllowWrite = false; + var (status, body) = Send(b, "PATCH", $"/item/{Shirt}", new OSDMap { ["name"] = "Nope" }); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.InternalServerError)); + AssertErrorBody(body, 500); + } + + /// + /// After A4 nothing in the spec answers 501 on the inventory cap except COPY, which belongs to the library + /// cap. Purge, slam and create all landed in A3/A4. + /// + [Test] + public void the_previously_unimplemented_mutations_now_answer_properly() + { + var b = Inventory(); + foreach (var (verb, path, expected) in new (string, string, int)[] + { + ("DELETE", $"/category/{Outfits}/children", 200), // purge (A4) + ("PUT", $"/category/{Outfits}/links", 200), // slam (A3) + ("POST", $"/category/{Outfits}", 200), // create (A3) + ("COPY", $"/category/{Outfits}", 501), // library-cap operation + }) + { + var handler = new AisHandler(Cap, Agent, b); + var response = new TestOSHttpResponse(); + // The PUT case must send the bare array it builds. `body as OSDMap` used to stand here and turned it + // into null, i.e. no body at all — see the remarks on MutTestRequest. + var body = verb == "PUT" ? (OSD)new OSDArray() : new OSDMap(); + handler.Handle(new MutTestRequest(verb, Cap + path, body), response); + Assert.That(response.StatusCode, Is.EqualTo(expected), $"{verb} {path}"); + } + } + private static void AssertErrorBody(OSDMap body, int code) + { + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(code)); + Assert.That(body.ContainsKey("message"), Is.True); + Assert.That(body.ContainsKey("parent_id"), Is.False); + Assert.That(body.ContainsKey("item_id"), Is.False); + Assert.That(body.ContainsKey("category_id"), Is.False); + Assert.That(body.ContainsKey("_embedded"), Is.False); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisPurgeCopyHttpTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisPurgeCopyHttpTests.cs new file mode 100644 index 00000000000..23dd8dfaf5a --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisPurgeCopyHttpTests.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// PurgeDescendents and CopyLibraryCategory over HTTP. A purge cannot be rolled back, so the interesting test is +/// what a partial purge reports; a copy is additive, so the interesting tests are structure and the tid quirk. +/// +[TestFixture] +public class AisPurgeCopyHttpTests +{ + private const string Cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + private static readonly UUID Agent = new("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + private static readonly UUID LibraryOwner = new("11111111-0000-0000-0000-000100bba000"); + + private static readonly UUID Root = new("00000000-0000-4000-8000-000000000001"); + private static readonly UUID Clothing = new("11111111-1111-4111-8111-111111111111"); + private static readonly UUID Trash = new("77777777-7777-4777-8777-777777777771"); + private static readonly UUID TrashSub = new("77777777-7777-4777-8777-777777777772"); + private static readonly UUID TrashItem = new("77777777-7777-4777-8777-777777777773"); + private static readonly UUID TrashSubItem = new("77777777-7777-4777-8777-777777777774"); + private static readonly UUID Empty = new("77777777-7777-4777-8777-777777777775"); + + // the library side + private static readonly UUID LibRoot = new("00000112-000f-0000-0000-000100bba000"); + private static readonly UUID LibOutfit = new("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1"); + private static readonly UUID LibSub = new("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2"); + private static readonly UUID LibShirt = new("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb3"); + private static readonly UUID LibShoes = new("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb4"); + + private sealed class PcTestRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public PcTestRequest(string verb, string url, string destination = null) + { + HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; + if (destination is not null) Headers["Destination"] = destination; + } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } = new MemoryStream(); + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => false; + public long ContentLength => 0; + public long ContentLength64 => 0; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + /// Trash holding an item and a subfolder that itself holds an item, plus an empty folder. + private static FakeAisBackend Inventory() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddFolder(Trash, Root, "Trash", 5, (short)FolderType.Trash); + b.TrashId = Trash; + b.AddFolder(TrashSub, Trash, "a discarded folder", 1); + b.AddFolder(Empty, Root, "nothing in here", 2); + b.AddItem(TrashItem, Trash, "a discarded item"); + b.AddItem(TrashSubItem, TrashSub, "nested discard"); + return b; + } + + /// The library: an outfit folder with two items and a subfolder. + private static FakeAisBackend Library() + { + var lib = new FakeAisBackend(LibraryOwner); + lib.AddFolder(LibRoot, UUID.Zero, "OpenSim Library", 1, (short)FolderType.Root); + lib.AddFolder(LibOutfit, LibRoot, "Casual Outfit", 1); + lib.AddFolder(LibSub, LibOutfit, "Accessories", 1); + var shirt = lib.AddItem(LibShirt, LibOutfit, "Library Shirt"); + shirt.BasePermissions = 0x7ffffff0; shirt.CurrentPermissions = 0x7ffffff0; + shirt.NextPermissions = 0x00008000; shirt.EveryOnePermissions = 0x00004000; + lib.AddItem(LibShoes, LibSub, "Library Shoes"); + return lib; + } + + private static (int Status, OSDMap Body) Purge(FakeAisBackend b, string path) + { + var handler = new AisHandler(Cap, Agent, b); + var response = new TestOSHttpResponse(); + handler.Handle(new PcTestRequest("DELETE", Cap + path), response); + return (response.StatusCode, (OSDMap)OSDParser.DeserializeLLSDXml(response.RawBuffer)); + } + + private static (int Status, OSDMap Body) Copy(FakeAisBackend lib, FakeAisBackend dest, string path, string destination) + { + var handler = new AisHandler(Cap, LibraryOwner, lib, AisMode.Library, dest, Agent); + var response = new TestOSHttpResponse(); + handler.Handle(new PcTestRequest("COPY", Cap + path, destination), response); + return (response.StatusCode, (OSDMap)OSDParser.DeserializeLLSDXml(response.RawBuffer)); + } + + private static List Ids(OSDMap body, string key) + => body[key] is OSDArray a ? a.Select(o => o.AsUUID()).ToList() : new List(); + + // ------------------------------------------------------------------ purge + + [Test] + public void purging_a_folder_removes_everything_inside_and_keeps_the_folder() + { + var b = Inventory(); + var before = b.Folders[Trash].Version; + + var (status, body) = Purge(b, $"/category/{Trash}/children"); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Folders.ContainsKey(Trash), Is.True, "the folder itself survives a purge"); + Assert.That(b.Folders.ContainsKey(TrashSub), Is.False, "the subfolder is gone"); + Assert.That(b.Items.ContainsKey(TrashItem), Is.False); + Assert.That(b.Items.ContainsKey(TrashSubItem), Is.False, "and so is the item nested inside it"); + + // the deltas must be enumerated: nothing in the viewer sweeps a purged folder's children for us + Assert.That(Ids(body, "_categories_removed"), Is.EquivalentTo(new[] { TrashSub }), + "the DIRECT subfolder is named; its own children are implied by its removal"); + Assert.That(Ids(body, "_removed_items"), Is.EquivalentTo(new[] { TrashItem }), + "the nested item is NOT enumerated: removing its parent folder purges it locally"); + Assert.That(((OSDMap)body["_updated_category_versions"])[Trash.ToString()].AsInteger(), + Is.EqualTo(b.Folders[Trash].Version)); + Assert.That(b.Folders[Trash].Version, Is.GreaterThan(before)); + } + + [Test] + public void purging_trash_is_the_real_caller_and_needs_no_special_case() + { + // Empty Trash: LLInventoryModel::emptyFolderType -> purge_descendents_of (llinventorymodel.cpp:4125-4131) + var b = Inventory(); + Assert.That(AisHandler.IsProtected(b.Folders[Trash]), Is.True, "Trash is a protected type"); + + var (status, _) = Purge(b, $"/category/{Trash}/children"); + + Assert.That(status, Is.EqualTo(200), "yet purging it is exactly what the operation is for"); + Assert.That(b.Items.Values.Any(i => i.Folder == Trash), Is.False); + } + + [Test] + public void purging_an_already_empty_folder_succeeds_and_reports_nothing_removed() + { + var b = Inventory(); + var (status, body) = Purge(b, $"/category/{Empty}/children"); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(body.ContainsKey("_categories_removed"), Is.False); + Assert.That(body.ContainsKey("_removed_items"), Is.False); + Assert.That(body.ContainsKey("_updated_category_versions"), Is.True, "the folder's version is still reported"); + Assert.That(b.Folders.ContainsKey(Empty), Is.True); + } + + [Test] + public void a_partial_purge_reports_the_survivors_and_does_not_claim_success() + { + // a purge is destructive by intent: there is nothing to roll back to, so the contract is to say what + // actually went and name what did not + var b = Inventory(); + b.DeleteFoldersOnlyIfTrash = false; + b.AllowWrite = true; + // the subfolder cannot be deleted; the item can + b.DeleteItemsGate = _ => true; + b.PurgeFolderGate = _ => false; // the service's own purge declines + b.DeleteFoldersGate = _ => false; // and so does the composed fallback + + var (status, body) = Purge(b, $"/category/{Trash}/children"); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.InternalServerError)); + Assert.That(body["message"].AsString(), Does.Contain(TrashSub.ToString()), + "the survivor must be named in the error"); + Assert.That(b.Folders.ContainsKey(TrashSub), Is.True, "and it really is still there"); + Assert.That(b.Items.ContainsKey(TrashItem), Is.False, "while the item that could be removed was removed"); + } + + [Test] + public void purging_an_unknown_category_is_404() + { + var (status, body) = Purge(Inventory(), $"/category/{UUID.Random()}/children"); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(404)); + } + + // ------------------------------------------------------------------ copy + + [Test] + public void copying_a_library_folder_preserves_its_structure_at_the_destination() + { + var lib = Library(); + var dest = Inventory(); + + var (status, body) = Copy(lib, dest, $"/category/{LibOutfit}?tid={UUID.Random()}", Clothing.ToString()); + + Assert.That(status, Is.EqualTo(200)); + var categories = Ids(body, "_created_categories"); + var items = Ids(body, "_created_items"); + Assert.That(categories.Count, Is.EqualTo(2), "the outfit folder and its subfolder"); + Assert.That(items.Count, Is.EqualTo(2), "the shirt and the shoes"); + + var copiedOutfit = dest.Folders.Values.Single(f => f.ParentID == Clothing && f.Name == "Casual Outfit"); + var copiedSub = dest.Folders.Values.Single(f => f.ParentID == copiedOutfit.ID); + Assert.That(copiedSub.Name, Is.EqualTo("Accessories"), "the nesting is preserved"); + Assert.That(dest.Items.Values.Single(i => i.Folder == copiedOutfit.ID).Name, Is.EqualTo("Library Shirt")); + Assert.That(dest.Items.Values.Single(i => i.Folder == copiedSub.ID).Name, Is.EqualTo("Library Shoes")); + + // everything created belongs to the agent, not to the library owner + Assert.That(dest.Items.Values.Where(i => i.Folder == copiedOutfit.ID).All(i => i.Owner == Agent), Is.True); + Assert.That(((OSDMap)body["_updated_category_versions"])[Clothing.ToString()].AsInteger(), + Is.EqualTo(dest.Folders[Clothing].Version)); + } + + [Test] + public void a_copied_library_item_keeps_the_source_permissions() + { + // Scene.Inventory.cs:1053-1064 - the library branch passes the source's own masks through, unlike the + // resident-to-resident branch at :1066-1078 which degrades them to NextPermissions + var lib = Library(); + var dest = Inventory(); + + Copy(lib, dest, $"/category/{LibOutfit}?tid={UUID.Random()}", Clothing.ToString()); + + var copiedShirt = dest.Items.Values.Single(i => i.Name == "Library Shirt"); + Assert.That(copiedShirt.BasePermissions, Is.EqualTo(0x7ffffff0u)); + Assert.That(copiedShirt.CurrentPermissions, Is.EqualTo(0x7ffffff0u), "not degraded to NextPermissions"); + Assert.That(copiedShirt.NextPermissions, Is.EqualTo(0x00008000u)); + Assert.That(copiedShirt.EveryOnePermissions, Is.EqualTo(0x00004000u)); + Assert.That(copiedShirt.AssetID, Is.EqualTo(lib.Items[LibShirt].AssetID), "the asset is shared, not duplicated"); + Assert.That(copiedShirt.CreatorId, Is.EqualTo(lib.Items[LibShirt].CreatorId), "the creator is preserved"); + } + + [Test] + public void the_depth_zero_tid_form_copies_the_folder_without_its_subfolders() + { + // llaisapi.cpp:275-278 appends ",depth=0" to the tid VALUE rather than adding a query parameter + var lib = Library(); + var dest = Inventory(); + var tid = UUID.Random(); + + var (status, body) = Copy(lib, dest, $"/category/{LibOutfit}?tid={tid},depth=0", Clothing.ToString()); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(Ids(body, "_created_categories").Count, Is.EqualTo(1), "only the outfit folder, no Accessories"); + Assert.That(Ids(body, "_created_items").Count, Is.EqualTo(1), "and only the item directly inside it"); + Assert.That(dest.Folders.Values.Any(f => f.Name == "Accessories"), Is.False); + Assert.That(body["tid"].AsUUID(), Is.EqualTo(tid), "the tid is echoed with the suffix stripped"); + } + + [Test] + public void copying_a_source_that_is_not_in_the_library_is_404() + { + var lib = Library(); + var dest = Inventory(); + + // Clothing exists in the AGENT's inventory but not in the library + var (status, body) = Copy(lib, dest, $"/category/{Clothing}?tid={UUID.Random()}", Clothing.ToString()); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.InternalServerError)); + Assert.That(body["message"].AsString(), Does.Contain("no library category")); + Assert.That(dest.Folders.Values.Any(f => f.Name == "Casual Outfit"), Is.False, "nothing was copied"); + } + + [Test] + public void a_copy_without_a_destination_header_is_400_and_an_unknown_destination_is_404() + { + var lib = Library(); + var dest = Inventory(); + + var (noHeader, _) = Copy(lib, dest, $"/category/{LibOutfit}?tid={UUID.Random()}", null); + Assert.That(noHeader, Is.EqualTo((int)HttpStatusCode.BadRequest)); + + var (unknown, _) = Copy(lib, dest, $"/category/{LibOutfit}?tid={UUID.Random()}", UUID.Random().ToString()); + Assert.That(unknown, Is.EqualTo((int)HttpStatusCode.NotFound)); + } + + /// + /// The destination is deliberately NOT protected-gated: copying a library folder into Clothing (a protected + /// system type) or into the inventory root is the ordinary case, and lookupIsProtectedType governs moving, + /// deleting and retyping a folder rather than adding children to it — the same reconciliation as slam and + /// purge. This documents the choice so it cannot change silently. + /// + [Test] + public void copying_into_a_protected_system_folder_is_allowed() + { + var lib = Library(); + var dest = Inventory(); + Assert.That(AisHandler.IsProtected(dest.Folders[Clothing]), Is.True, "Clothing is a protected type"); + + var (status, _) = Copy(lib, dest, $"/category/{LibOutfit}?tid={UUID.Random()}", Clothing.ToString()); + + Assert.That(status, Is.EqualTo(200), "yet it is the natural destination for a library copy"); + Assert.That(dest.Folders.Values.Any(f => f.ParentID == Clothing && f.Name == "Casual Outfit"), Is.True); + } + + [Test] + public void a_copy_that_fails_part_way_keeps_what_it_made_and_says_how_much() + { + var lib = Library(); + var dest = Inventory(); + var adds = 0; + dest.AddItemGate = _ => ++adds < 2; // the second item fails + + var (status, body) = Copy(lib, dest, $"/category/{LibOutfit}?tid={UUID.Random()}", Clothing.ToString()); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.InternalServerError)); + Assert.That(body["message"].AsString(), Does.Contain("were created before the failure")); + Assert.That(dest.Items.Values.Any(i => i.Name == "Library Shirt"), Is.True, + "a copy is additive: what was made stays, and nothing pre-existing was at risk"); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisResponseLoggingTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisResponseLoggingTests.cs new file mode 100644 index 00000000000..569c109b142 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisResponseLoggingTests.cs @@ -0,0 +1,223 @@ +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// A11: every mutation must say, in the log, what it answered — the status and the delta keys with their +/// contents. A10 could not separate "the viewer rejected our delta" from "our delta was fine" because nothing +/// recorded what was sent back; both hypotheses died on the same missing evidence +/// (Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md). +/// +[TestFixture] +public class AisResponseLoggingTests +{ + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + private static readonly UUID Cof = new("71c3c184-410b-4dae-b20a-855741cf1faf"); + + /// An agent with a Current Outfit folder holding one link, which is the shape step 10 exercises. + private static (FakeAisBackend Backend, AisHandler Handler, UUID LinkId) Inventory() + { + var backend = new FakeAisBackend(Agent); + var root = UUID.Random(); + backend.AddFolder(root, UUID.Zero, "My Inventory", 1, (short)FolderType.Root); + backend.AddFolder(Cof, root, "Current Outfit", 500, (short)FolderType.CurrentOutfit); + backend.CurrentOutfitId = Cof; + + var target = UUID.Random(); + backend.AddItem(target, root, "Skirt"); + var linkId = UUID.Random(); + backend.AddLink(linkId, Cof, "Skirt", target); + + return (backend, new AisHandler("/" + UUID.Random(), Agent, backend), linkId); + } + + /// Minimal IOSHttpRequest: only what the handler reads (verb, raw url, empty body). + private sealed class LogTestRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public LogTestRequest(string verb, string url) { HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public System.IO.Stream InputStream { get; set; } = new System.IO.MemoryStream(); + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => false; + public long ContentLength => 0; + public long ContentLength64 => 0; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public System.Collections.Generic.HashSet QueryFlags => throw new NotImplementedException(); + public System.Collections.Generic.Dictionary QueryAsDictionary => throw new NotImplementedException(); + public System.Net.IPEndPoint RemoteIPEndPoint => new(System.Net.IPAddress.Loopback, 1); + public System.Net.IPEndPoint LocalIPEndPoint => new(System.Net.IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + private static void Send(AisHandler handler, string verb, string path) + { + var url = handler.CapPath + path; + handler.Dispatch(AisRouter.Parse(verb, url, handler.CapPath), new LogTestRequest(verb, url), new TestOSHttpResponse()); + } + + // ------------------------------------------------------------------ the log line + + [Test] + public void a_removal_logs_its_status_and_the_delta_keys_it_actually_sent() + { + var (_, handler, linkId) = Inventory(); + + using var log = new CapturedLog(); + Send(handler, "DELETE", "/item/" + linkId); + + var line = log.Messages(LogLevel.Debug).SingleOrDefault(m => m.Contains("RemoveItem ->")); + Assert.That(line, Is.Not.Null, "a mutation must log what it answered"); + Assert.That(line, Does.Contain("200"), "the status code"); + Assert.That(line, Does.Contain(AisMutation.RemovedItems), "the removal delta key"); + Assert.That(line, Does.Contain(linkId.ToString()), "and the id that was removed"); + Assert.That(line, Does.Contain(AisMutation.UpdatedCategoryVersions), "the version key that gates it"); + Assert.That(line, Does.Contain(Cof.ToString()), "naming the folder whose version moved"); + } + + [Test] + public void a_failed_mutation_logs_its_status_and_reason() + { + var (_, handler, _) = Inventory(); + + using var log = new CapturedLog(); + Send(handler, "DELETE", "/item/" + UUID.Random()); // no such item + + var line = log.Messages(LogLevel.Debug).SingleOrDefault(m => m.Contains("RemoveItem ->")); + Assert.That(line, Is.Not.Null, "a mutation that fails is exactly the case A10 needed"); + Assert.That(line, Does.Contain("404")); + } + + [Test] + public void a_fetch_does_not_log_a_response_line() + { + var (_, handler, _) = Inventory(); + + using var log = new CapturedLog(); + Send(handler, "GET", "/category/current/links"); + + Assert.That(log.Messages(LogLevel.Debug).Any(m => m.Contains("FetchCOF ->")), Is.False, + "fetch bodies are whole inventory listings; logging them would bury the mutations"); + } + + [Test] + public void fetch_cof_logs_the_folder_current_resolved_to() + { + var (_, handler, _) = Inventory(); + + using var log = new CapturedLog(); + Send(handler, "GET", "/category/current/links"); + + var line = log.Messages(LogLevel.Debug).SingleOrDefault(m => m.Contains("resolved \"current\"")); + Assert.That(line, Is.Not.Null, + "the A7 WARN only fires when there is more than one candidate, so the ordinary case recorded nothing"); + Assert.That(line, Does.Contain(Cof.ToString())); + Assert.That(line, Does.Contain("500"), "and the version it carried"); + } + + // ------------------------------------------------------------------ the summariser + + [Test] + public void every_delta_key_of_the_contract_is_rendered() + { + var a = new UUID("11111111-1111-4111-8111-111111111111"); + var b = new UUID("22222222-2222-4222-8222-222222222222"); + var body = new OSDMap + { + ["category_id"] = Cof, + [AisMutation.CreatedCategories] = new OSDArray { OSD.FromUUID(a) }, + [AisMutation.CreatedItems] = new OSDArray { OSD.FromUUID(b) }, + [AisMutation.CategoriesRemoved] = new OSDArray { OSD.FromUUID(a) }, + [AisMutation.RemovedItems] = new OSDArray { OSD.FromUUID(b) }, + [AisMutation.CategoryItemsRemoved] = new OSDArray { OSD.FromUUID(b) }, + [AisMutation.BrokenLinksRemoved] = new OSDArray { OSD.FromUUID(a) }, + [AisMutation.UpdatedCategoryVersions] = new OSDMap { [Cof.ToString()] = 500 }, + }; + + var summary = AisMutation.SummariseDeltas(body); + + foreach (var key in AisMutation.DeltaKeys) + Assert.That(summary, Does.Contain(key), $"{key} must be rendered"); + Assert.That(summary, Does.Contain("category_id=" + Cof), "the top-level content object"); + Assert.That(summary, Does.Contain(Cof + ":500"), "a version renders as folder:version"); + Assert.That(summary, Does.Contain(a.ToString()).And.Contain(b.ToString())); + } + + /// + /// The case worth seeing at a glance: a mutation that changed something and reported nothing. That is how a + /// viewer's model goes stale, and it must not look like an ordinary line. + /// + [Test] + public void a_response_with_no_deltas_says_so() + { + Assert.That(AisMutation.SummariseDeltas(new OSDMap()), Is.EqualTo("empty body")); + Assert.That(AisMutation.SummariseDeltas(new OSDMap { ["tid"] = UUID.Random() }), Is.EqualTo("no deltas")); + Assert.That(AisMutation.SummariseDeltas(null), Is.EqualTo("empty body")); + } + + [Test] + public void absent_keys_are_omitted_rather_than_printed_empty() + { + var body = new OSDMap { [AisMutation.RemovedItems] = new OSDArray { OSD.FromUUID(Cof) } }; + + var summary = AisMutation.SummariseDeltas(body); + + Assert.That(summary, Does.Contain(AisMutation.RemovedItems)); + Assert.That(summary, Does.Not.Contain(AisMutation.CreatedItems)); + Assert.That(summary, Does.Not.Contain(AisMutation.UpdatedCategoryVersions)); + } + + // ------------------------------------------------------------------ classification + + [Test] + public void every_mutation_is_classified_as_one_and_no_fetch_is() + { + foreach (var op in new[] + { + AisOperation.CreateInventory, AisOperation.SlamFolder, AisOperation.RemoveCategory, + AisOperation.RemoveItem, AisOperation.PurgeDescendents, AisOperation.UpdateCategory, + AisOperation.UpdateItem, AisOperation.CopyCategory, + }) + Assert.That(AisOperations.IsMutation(op), Is.True, $"{op} changes inventory"); + + foreach (var op in new[] + { + AisOperation.Unknown, AisOperation.FetchItem, AisOperation.FetchCategoryChildren, + AisOperation.FetchCategoryCategories, AisOperation.FetchCategorySubset, AisOperation.FetchCOF, + AisOperation.FetchCategoryLinks, AisOperation.FetchOrphans, + }) + Assert.That(AisOperations.IsMutation(op), Is.False, $"{op} does not"); + } + + /// + /// Guards the cost claim. With DEBUG off nothing may be logged — the summariser is only reached inside the + /// level guard, so a production log level pays two predicates and no allocation. + /// + [Test] + public void nothing_is_logged_when_debug_is_off() + { + var (_, handler, linkId) = Inventory(); + + using var log = new CapturedLog { Enabled = LogLevel.Information }; + Send(handler, "DELETE", "/item/" + linkId); + + Assert.That(log.Messages(LogLevel.Debug), Is.Empty, "no DEBUG line may be emitted when DEBUG is disabled"); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisRouterTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisRouterTests.cs new file mode 100644 index 00000000000..15e90a0715a --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisRouterTests.cs @@ -0,0 +1,179 @@ +using NUnit.Framework; +using OpenMetaverse; +using OpenSim.Region.ClientStack.LindenCaps.AIS; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// One test per URL shape in Docs/feature/ais-v3/AIS-V3-SPEC.md §1a (the viewer's llaisapi.cpp request builders), +/// plus the "current" alias (§1b) and the cap-prefix / rejection cases. +/// +[TestFixture] +public class AisRouterTests +{ + private static readonly UUID Cat = new("11111111-1111-4111-8111-111111111111"); + private static readonly UUID Item = new("22222222-2222-4222-8222-222222222222"); + private static readonly UUID Tid = new("33333333-3333-4333-8333-333333333333"); + private static readonly UUID C1 = new("44444444-4444-4444-8444-444444444444"); + private static readonly UUID C2 = new("55555555-5555-4555-8555-555555555555"); + + [Test] // §1a #1, llaisapi.cpp:115 + public void CreateInventory_is_POST_category_parent_with_tid() + { + var r = AisRouter.Parse("POST", $"/category/{Cat}?tid={Tid}"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.CreateInventory)); + Assert.That(r.Id, Is.EqualTo(Cat)); + Assert.That(r.Tid, Is.EqualTo(Tid)); + Assert.That(r.IsAlias, Is.False); + } + + [Test] // §1a #2, :161 + public void SlamFolder_is_PUT_category_id_links_with_tid() + { + var r = AisRouter.Parse("PUT", $"/category/{Cat}/links?tid={Tid}"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.SlamFolder)); + Assert.That(r.Id, Is.EqualTo(Cat)); + Assert.That(r.Tid, Is.EqualTo(Tid)); + } + + [Test] // §1a #3, :197 + public void RemoveCategory_is_DELETE_category_id() + { + var r = AisRouter.Parse("DELETE", $"/category/{Cat}"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.RemoveCategory)); + Assert.That(r.Id, Is.EqualTo(Cat)); + } + + [Test] // §1a #4, :234 + public void RemoveItem_is_DELETE_item_id() + { + var r = AisRouter.Parse("DELETE", $"/item/{Item}"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.RemoveItem)); + Assert.That(r.Id, Is.EqualTo(Item)); + } + + [Test] // §1a #5, :275-278: the viewer appends ",depth=0" to the tid VALUE + public void CopyLibraryCategory_is_COPY_category_with_tid_and_comma_depth() + { + var r = AisRouter.Parse("COPY", $"/category/{Cat}?tid={Tid},depth=0"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.CopyCategory)); + Assert.That(r.Id, Is.EqualTo(Cat)); + Assert.That(r.Tid, Is.EqualTo(Tid), "the tid must be parsed off the comma-suffixed value"); + Assert.That(r.Depth, Is.EqualTo(0)); + var full = AisRouter.Parse("COPY", $"/category/{Cat}?tid={Tid}"); + Assert.That(full.Depth, Is.EqualTo(-1), "no depth means copy subfolders"); + } + + [Test] // §1a #6, :318 + public void PurgeDescendents_is_DELETE_category_id_children() + { + var r = AisRouter.Parse("DELETE", $"/category/{Cat}/children"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.PurgeDescendents)); + Assert.That(r.Id, Is.EqualTo(Cat)); + } + + [Test] // §1a #7, :355 + public void UpdateCategory_is_PATCH_category_id() + { + Assert.That(AisRouter.Parse("PATCH", $"/category/{Cat}").Operation, Is.EqualTo(AisOperation.UpdateCategory)); + } + + [Test] // §1a #8, :391 + public void UpdateItem_is_PATCH_item_id() + { + Assert.That(AisRouter.Parse("PATCH", $"/item/{Item}").Operation, Is.EqualTo(AisOperation.UpdateItem)); + } + + [Test] // §1a #9, :426 + public void FetchItem_is_GET_item_id() + { + var r = AisRouter.Parse("GET", $"/item/{Item}"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.FetchItem)); + Assert.That(r.Id, Is.EqualTo(Item)); + } + + [Test] // §1a #10, :461-474 + public void FetchCategoryChildren_is_GET_category_id_children_with_depth() + { + var r = AisRouter.Parse("GET", $"/category/{Cat}/children?depth=50"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.FetchCategoryChildren)); + Assert.That(r.Depth, Is.EqualTo(50)); + Assert.That(r.Children, Is.Empty); + Assert.That(AisRouter.Parse("GET", $"/category/{Cat}/children").Depth, Is.EqualTo(-1)); + } + + [Test] // §1a #11, :514: any identifier string, including the alias + public void FetchCategoryChildren_by_identifier_accepts_current() + { + var r = AisRouter.Parse("GET", "/category/current/children?depth=0"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.FetchCategoryChildren)); + Assert.That(r.IsAlias, Is.True); + Assert.That(r.Identifier, Is.EqualTo("current")); + Assert.That(r.Id, Is.EqualTo(UUID.Zero)); + Assert.That(r.Depth, Is.EqualTo(0)); + } + + [Test] // §1a #12, :565-578 + public void FetchCategoryCategories_is_GET_category_id_categories_with_depth() + { + var r = AisRouter.Parse("GET", $"/category/{Cat}/categories?depth=3"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.FetchCategoryCategories)); + Assert.That(r.Depth, Is.EqualTo(3)); + } + + [Test] // §1a #13, :642-648 + public void FetchCategorySubset_is_GET_children_with_children_list() + { + var r = AisRouter.Parse("GET", $"/category/{Cat}/children?depth=1&children={C1},{C2}"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.FetchCategorySubset)); + Assert.That(r.Depth, Is.EqualTo(1)); + Assert.That(r.Children, Is.EqualTo(new[] { C1, C2 })); + } + + [Test] // §1a #14 / §1b, :692 + public void FetchCOF_is_GET_category_current_links() + { + var r = AisRouter.Parse("GET", "/category/current/links"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.FetchCOF)); + Assert.That(r.IsAlias, Is.True); + Assert.That(r.Identifier, Is.EqualTo("current")); + } + + [Test] // §1a #15, :728 + public void FetchCategoryLinks_is_GET_category_id_links() + { + var r = AisRouter.Parse("GET", $"/category/{Cat}/links"); + Assert.That(r.Operation, Is.EqualTo(AisOperation.FetchCategoryLinks)); + Assert.That(r.Id, Is.EqualTo(Cat)); + Assert.That(r.IsAlias, Is.False); + } + + [Test] // §1a #16, :765 + public void FetchOrphans_is_GET_orphans() + { + Assert.That(AisRouter.Parse("GET", "/orphans").Operation, Is.EqualTo(AisOperation.FetchOrphans)); + Assert.That(AisRouter.Parse("DELETE", "/orphans").Operation, Is.EqualTo(AisOperation.Unknown)); + } + + [Test] + public void the_cap_prefix_is_stripped_and_unknown_query_keys_are_kept() + { + var cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + var r = AisRouter.Parse("get", $"{cap}/category/{Cat}/children?depth=2&simulate=true", cap); + Assert.That(r.Operation, Is.EqualTo(AisOperation.FetchCategoryChildren)); + Assert.That(r.Verb, Is.EqualTo("GET")); + Assert.That(r.Path, Is.EqualTo($"/category/{Cat}/children")); + Assert.That(r.Query["simulate"], Is.EqualTo("true")); + } + + [Test] + public void shapes_outside_the_spec_are_Unknown() + { + Assert.That(AisRouter.Parse("GET", $"/category/{Cat}").Operation, Is.EqualTo(AisOperation.Unknown), "no GET on a bare category in llaisapi.cpp"); + Assert.That(AisRouter.Parse("POST", $"/item/{Item}").Operation, Is.EqualTo(AisOperation.Unknown)); + Assert.That(AisRouter.Parse("GET", "/category/not-a-uuid/children").Operation, Is.EqualTo(AisOperation.Unknown), "only 'current' is an alias"); + Assert.That(AisRouter.Parse("GET", "/").Operation, Is.EqualTo(AisOperation.Unknown)); + Assert.That(AisRouter.Parse("GET", $"/category/{Cat}/children/extra").Operation, Is.EqualTo(AisOperation.Unknown)); + Assert.That(AisRouter.Parse("PUT", $"/category/{Cat}/children").Operation, Is.EqualTo(AisOperation.Unknown)); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisSlamCreateHttpTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisSlamCreateHttpTests.cs new file mode 100644 index 00000000000..de4009889e6 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisSlamCreateHttpTests.cs @@ -0,0 +1,699 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// SlamFolder and CreateInventory over HTTP. The tests that earn their keep here are the failure ones: a slam is +/// several independent writes with no transaction underneath (Ledger A-R2), so what matters is exactly what the +/// folder looks like when one of them fails. +/// +[TestFixture] +public class AisSlamCreateHttpTests +{ + private const string Cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + private static readonly UUID Agent = new("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + + private static readonly UUID Root = new("00000000-0000-4000-8000-000000000001"); + private static readonly UUID Clothing = new("11111111-1111-4111-8111-111111111111"); + private static readonly UUID Cof = new("cccccccc-cccc-4ccc-8ccc-cccccccccccc"); + + // five targets to link to, plus the links currently in the COF + private static readonly UUID[] Targets = + { + new("22222222-2222-4222-8222-222222222221"), new("22222222-2222-4222-8222-222222222222"), + new("22222222-2222-4222-8222-222222222223"), new("22222222-2222-4222-8222-222222222224"), + new("22222222-2222-4222-8222-222222222225"), + }; + private static readonly UUID OldLinkA = new("88888888-8888-4888-8888-888888888881"); + private static readonly UUID OldLinkB = new("88888888-8888-4888-8888-888888888882"); + private static readonly UUID PlainItem = new("99999999-9999-4999-8999-999999999991"); + + private sealed class SlamTestRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public SlamTestRequest(string verb, string url, OSD body) + { + HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; + InputStream = body is null ? new MemoryStream() : new MemoryStream(OSDParser.SerializeLLSDXmlBytes(body)); + } + + /// + /// AIS-SEC-2: a body the serialiser would never produce — truncated XML, XML that is not LLSD, or simply + /// too much of it. These are the shapes a real client hits on a dropped connection and an attacker sends + /// on purpose, and neither can be expressed through the constructor above. + /// + public SlamTestRequest(string verb, string url, byte[] raw) + { + HttpMethod = verb; Url = new Uri("http://sim.test" + url); RawUrl = url; + InputStream = new MemoryStream(raw ?? Array.Empty()); + } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => InputStream.Length > 0; + public long ContentLength => InputStream.Length; + public long ContentLength64 => InputStream.Length; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + /// A COF holding two links and one ordinary (non-link) item, plus five link targets in Clothing. + private static FakeAisBackend Inventory() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddFolder(Cof, Root, "Current Outfit", 11, (short)FolderType.CurrentOutfit); + b.CurrentOutfitId = Cof; + for (var i = 0; i < Targets.Length; i++) b.AddItem(Targets[i], Clothing, $"Target {i}"); + b.AddLink(OldLinkA, Cof, "old A", Targets[0]); + b.AddLink(OldLinkB, Cof, "old B", Targets[1]); + b.AddItem(PlainItem, Cof, "a note that is not a link"); + return b; + } + + private static OSDArray Body(params UUID[] targets) + { + var array = new OSDArray(); + foreach (var t in targets) + array.Add(new OSDMap + { + ["name"] = "link to " + t, + ["desc"] = "", + ["linked_id"] = t, + ["type"] = (int)AssetType.Link, + }); + return array; + } + + private static (int Status, OSDMap Body) Send(FakeAisBackend backend, string verb, string path, OSD body, AisMode mode = AisMode.Inventory) + { + var handler = new AisHandler(Cap, Agent, backend, mode); + var response = new TestOSHttpResponse(); + handler.Handle(new SlamTestRequest(verb, Cap + path, body), response); + var parsed = OSDParser.DeserializeLLSDXml(response.RawBuffer); + Assert.That(parsed, Is.InstanceOf()); + return (response.StatusCode, (OSDMap)parsed); + } + + /// AIS-SEC-2: the same round trip, but with the request body given as raw bytes. + private static (int Status, OSDMap Body) SendRaw(FakeAisBackend backend, string verb, string path, byte[] raw) + { + var handler = new AisHandler(Cap, Agent, backend); + var response = new TestOSHttpResponse(); + handler.Handle(new SlamTestRequest(verb, Cap + path, raw), response); + var parsed = OSDParser.DeserializeLLSDXml(response.RawBuffer); + Assert.That(parsed, Is.InstanceOf()); + return (response.StatusCode, (OSDMap)parsed); + } + + /// The ids of the link rows currently in a folder, which is what a slam is supposed to replace. + private static HashSet LinkIds(FakeAisBackend b, UUID folder) + => b.Items.Values.Where(i => i.Folder == folder && (i.AssetType == (int)AssetType.Link || i.AssetType == (int)AssetType.LinkFolder)) + .Select(i => i.ID).ToHashSet(); + + /// The link targets in a folder — what the outfit actually is, independent of link row ids. + private static HashSet LinkTargets(FakeAisBackend b, UUID folder) + => b.Items.Values.Where(i => i.Folder == folder && i.AssetType == (int)AssetType.Link) + .Select(i => i.AssetID).ToHashSet(); + + // ------------------------------------------------------------------ the happy paths + + [Test] + public void a_slam_replaces_the_links_and_reports_created_removed_and_the_new_version() + { + var b = Inventory(); + var before = b.Folders[Cof].Version; + var oldIds = LinkIds(b, Cof); + + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", Body(Targets[2], Targets[3], Targets[4])); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(LinkTargets(b, Cof), Is.EquivalentTo(new[] { Targets[2], Targets[3], Targets[4] })); + Assert.That(LinkIds(b, Cof).Intersect(oldIds), Is.Empty, "every old link row is gone"); + + var created = ((OSDArray)body["_created_items"]).Select(o => o.AsUUID()).ToList(); + Assert.That(created, Is.EquivalentTo(LinkIds(b, Cof))); + var embeddedLinks = (OSDMap)((OSDMap)body["_embedded"])["links"]; + Assert.That(embeddedLinks.Keys, Is.EquivalentTo(created.Select(c => c.ToString())), + "the created links must ride in _embedded.links, and the viewer only accepts embedded objects listed in _created_items"); + Assert.That(((OSDArray)body["_removed_items"]).Select(o => o.AsUUID()), Is.EquivalentTo(oldIds)); + Assert.That(((OSDMap)body["_updated_category_versions"])[Cof.ToString()].AsInteger(), + Is.EqualTo(b.Folders[Cof].Version), "the slammed folder's version, read fresh"); + Assert.That(b.Folders[Cof].Version, Is.GreaterThan(before)); + } + + [Test] + public void a_slam_of_an_empty_array_empties_the_folders_links() + { + var b = Inventory(); + var oldIds = LinkIds(b, Cof); + + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", new OSDArray()); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(LinkIds(b, Cof), Is.Empty); + Assert.That(body.ContainsKey("_created_items"), Is.False, "nothing was created, so the key is absent (an absent key and an empty one are identical to the viewer)"); + Assert.That(((OSDArray)body["_removed_items"]).Select(o => o.AsUUID()), Is.EquivalentTo(oldIds)); + Assert.That(body.ContainsKey("_updated_category_versions"), Is.True); + } + + [Test] + public void the_cof_alias_slams_the_current_outfit_folder() + { + var b = Inventory(); + var (status, _) = Send(b, "PUT", "/category/current/links", Body(Targets[4])); + Assert.That(status, Is.EqualTo(200)); + Assert.That(LinkTargets(b, Cof), Is.EquivalentTo(new[] { Targets[4] })); + Assert.That(b.Calls, Does.Contain("GetInventorySkeleton"), "resolved deterministically over the skeleton (A7)"); + } + + [Test] + public void a_slam_leaves_non_link_items_in_the_folder_alone() + { + // the viewer builds a slam body from AT_LINK / AT_LINK_FOLDER rows only (llappearancemgr.cpp:1795-1833), + // so a slam has nothing to say about ordinary items that happen to live in the folder + var b = Inventory(); + var (status, _) = Send(b, "PUT", $"/category/{Cof}/links", Body(Targets[2])); + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Items.ContainsKey(PlainItem), Is.True, "the non-link item must survive a slam"); + Assert.That(b.Items[PlainItem].Folder, Is.EqualTo(Cof)); + } + + [Test] + public void the_cof_is_slammable_even_though_it_is_a_protected_folder() + { + // lookupIsProtectedType governs move/delete/retype (llfoldertype.cpp:151-153); FT_CURRENT_OUTFIT is + // protected AND is the folder the viewer slams constantly (llappearancemgr.cpp:2251) + var b = Inventory(); + Assert.That(AisHandler.IsProtected(b.Folders[Cof]), Is.True, "the COF is protected against deletion"); + var (status, _) = Send(b, "PUT", $"/category/{Cof}/links", Body(Targets[3])); + Assert.That(status, Is.EqualTo(200), "but a slam is not a delete"); + } + + // ------------------------------------------------------------------ fault injection + + [Test] + public void a_failure_on_the_third_of_five_creations_leaves_the_folder_exactly_as_it_was() + { + var b = Inventory(); + var beforeIds = LinkIds(b, Cof); + var beforeTargets = LinkTargets(b, Cof); + var adds = 0; + b.AddItemGate = _ => ++adds != 3; // the third creation fails + + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", + Body(Targets[0], Targets[1], Targets[2], Targets[3], Targets[4])); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.InternalServerError)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(500)); + + // the whole point: the folder's link contents are byte-for-byte what they were + Assert.That(LinkIds(b, Cof), Is.EquivalentTo(beforeIds), "no link row was added or lost"); + Assert.That(LinkTargets(b, Cof), Is.EquivalentTo(beforeTargets), "and the outfit is unchanged"); + Assert.That(b.Items.ContainsKey(PlainItem), Is.True); + Assert.That(body.ContainsKey("_removed_items"), Is.False, "nothing was removed, so nothing is reported removed"); + Assert.That(body.ContainsKey("_created_items"), Is.False); + } + + [Test] + public void a_failure_during_the_rollback_itself_leaves_extra_links_never_fewer_and_says_so() + { + // This documents the worst case rather than pretending it cannot happen: the creation fails AND the + // compensating delete fails too. The folder then holds its original links PLUS the ones that could not be + // rolled back - more than it started with, never fewer, so the avatar is never stripped. + var b = Inventory(); + var beforeIds = LinkIds(b, Cof); + var adds = 0; + b.AddItemGate = _ => ++adds != 3; + b.DeleteItemsGate = _ => false; // the rollback cannot complete either + + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", + Body(Targets[0], Targets[1], Targets[2], Targets[3], Targets[4])); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.InternalServerError)); + Assert.That(body["message"].AsString(), Does.Contain("rollback also failed"), + "the response must name the state it left behind"); + + var after = LinkIds(b, Cof); + Assert.That(after.IsSupersetOf(beforeIds), Is.True, "every original link is still there"); + Assert.That(after.Count, Is.EqualTo(beforeIds.Count + 2), "plus the two that were created before the failure"); + Assert.That(LinkTargets(b, Cof).IsSupersetOf(new[] { Targets[0], Targets[1] }), Is.True, + "the leftovers are duplicates of the first two links, recoverable by the next slam"); + } + + [Test] + public void a_removal_failure_after_every_creation_keeps_both_sets_and_reports_the_failure() + { + var b = Inventory(); + var beforeIds = LinkIds(b, Cof); + b.DeleteItemsGate = _ => false; // creations all succeed; the removal of the old links fails + + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", Body(Targets[2], Targets[3])); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.InternalServerError)); + Assert.That(body["message"].AsString(), Does.Contain("could not be removed")); + var after = LinkIds(b, Cof); + Assert.That(after.IsSupersetOf(beforeIds), Is.True, "nothing was lost"); + Assert.That(after.Count, Is.EqualTo(beforeIds.Count + 2), "and the new links are there too"); + } + + // ------------------------------------------------------------------ create + + [Test] + public void create_makes_categories_and_links_with_the_right_deltas() + { + var b = Inventory(); + var before = b.Folders[Clothing].Version; + + // the shapes the viewer really sends: a categories array (llinventorymodel.cpp:1036-1041) and a links + // array whose entries are linked_id / type / inv_type / name / desc (llviewerinventory.cpp:1352-1370) + var (status, body) = Send(b, "POST", $"/category/{Clothing}?tid={UUID.Random()}", new OSDMap + { + ["categories"] = new OSDArray { new OSDMap { ["name"] = "New Folder", ["type_default"] = -1 } }, + ["links"] = new OSDArray + { + new OSDMap + { + ["linked_id"] = Targets[0], + ["type"] = (int)AssetType.Link, + ["inv_type"] = (int)InventoryType.Wearable, + ["name"] = "a link", + ["desc"] = "", + }, + }, + }); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(((OSDArray)body["_created_categories"]).Count, Is.EqualTo(1)); + Assert.That(((OSDArray)body["_created_items"]).Count, Is.EqualTo(1), "a link is a created item"); + + var embedded = (OSDMap)body["_embedded"]; + Assert.That(embedded.ContainsKey("categories"), Is.True); + Assert.That(embedded.ContainsKey("links"), Is.True, "links are their own collection, never items"); + Assert.That(embedded.ContainsKey("items"), Is.False); + var link = (OSDMap)((OSDMap)embedded["links"]).Values.First(); + Assert.That(link["linked_id"].AsUUID(), Is.EqualTo(Targets[0])); + Assert.That(link["inv_type"].AsInteger(), Is.EqualTo((int)InventoryType.Wearable), + "the body's inv_type is the target's and is kept"); + + Assert.That(((OSDMap)body["_updated_category_versions"])[Clothing.ToString()].AsInteger(), + Is.EqualTo(b.Folders[Clothing].Version)); + Assert.That(b.Folders[Clothing].Version, Is.GreaterThan(before)); + Assert.That(body["tid"].Type, Is.EqualTo(OSDType.UUID), "tid is echoed"); + Assert.That(b.Folders.Values.Count(f => f.ParentID == Clothing && f.Name == "New Folder"), Is.EqualTo(1)); + } + + /// + /// A4: an items array is refused with 501 rather than creating an item with no asset behind it. The viewer's + /// own builder wraps the item's asLLSD with a null asset_id for the server to fill + /// (llviewerinventory.cpp:1124-1157) and is compiled out behind USE_AIS_FOR_NC, above the comment "not yet + /// implemented within AIS3" (:1120-1121). A3 guessed and created assetless items; this replaces that. + /// + [Test] + public void an_items_create_array_is_refused_before_anything_is_written() + { + var b = Inventory(); + var foldersBefore = b.Folders.Count; + var itemsBefore = b.Items.Count; + + var (status, body) = Send(b, "POST", $"/category/{Clothing}", new OSDMap + { + // a mixed body: the categories must NOT be created either + ["categories"] = new OSDArray { new OSDMap { ["name"] = "should not appear" } }, + ["items"] = new OSDArray { new OSDMap { ["name"] = "New Note", ["type"] = (int)AssetType.Notecard } }, + }); + + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotImplemented)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(501)); + Assert.That(b.Folders.Count, Is.EqualTo(foldersBefore), "the refusal happens before any write"); + Assert.That(b.Items.Count, Is.EqualTo(itemsBefore)); + } + + /// An empty items array is not a request to create items, so it does not trip the refusal. + [Test] + public void an_empty_items_array_is_not_refused() + { + var b = Inventory(); + var (status, _) = Send(b, "POST", $"/category/{Clothing}", new OSDMap + { + ["items"] = new OSDArray(), + ["categories"] = new OSDArray { new OSDMap { ["name"] = "fine" } }, + }); + Assert.That(status, Is.EqualTo(200)); + Assert.That(b.Folders.Values.Any(f => f.Name == "fine"), Is.True); + } + [Test] + public void creating_a_second_object_with_the_same_name_is_allowed() + { + // inventory names are not unique in SL or in this tree; a duplicate is a normal outcome, not an error + var b = Inventory(); + var body = new OSDMap { ["categories"] = new OSDArray { new OSDMap { ["name"] = "Twin", ["type_default"] = -1 } } }; + + var (first, _) = Send(b, "POST", $"/category/{Clothing}", body); + var (second, _) = Send(b, "POST", $"/category/{Clothing}", body); + + Assert.That(first, Is.EqualTo(200)); + Assert.That(second, Is.EqualTo(200)); + Assert.That(b.Folders.Values.Count(f => f.ParentID == Clothing && f.Name == "Twin"), Is.EqualTo(2), + "two folders of the same name, with different ids"); + } + + [Test] + public void creating_a_link_to_a_target_that_does_not_exist_succeeds_as_a_broken_link() + { + // broken links are a normal inventory state - the viewer has a whole delta key for removing them + // (_broken_links_removed) - so creating one is not an error + var b = Inventory(); + var missing = UUID.Random(); + + var (status, body) = Send(b, "POST", $"/category/{Clothing}", new OSDMap + { + ["links"] = new OSDArray { new OSDMap { ["name"] = "dangling", ["linked_id"] = missing, ["type"] = (int)AssetType.Link } }, + }); + + Assert.That(status, Is.EqualTo(200)); + var created = ((OSDArray)body["_created_items"]).Single().AsUUID(); + Assert.That(b.Items[created].AssetID, Is.EqualTo(missing)); + Assert.That(b.Items[created].AssetType, Is.EqualTo((int)AssetType.Link)); + } + + [Test] + public void creating_in_an_unknown_category_is_404() + { + var (status, body) = Send(Inventory(), "POST", $"/category/{UUID.Random()}", new OSDMap()); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(404)); + } + + [Test] + public void slam_and_create_through_the_library_cap_are_405() + { + foreach (var (verb, path, body) in new (string, string, OSD)[] + { + ("PUT", $"/category/{Cof}/links", Body(Targets[2])), + ("POST", $"/category/{Clothing}", new OSDMap()), + }) + { + var b = Inventory(); + var (status, _) = Send(b, verb, path, body, AisMode.Library); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.MethodNotAllowed), $"{verb} {path}"); + Assert.That(LinkTargets(b, Cof), Is.EquivalentTo(new[] { Targets[0], Targets[1] }), "nothing changed"); + } + } + + [Test] + public void a_slam_body_that_is_not_an_array_is_400() + { + var (status, body) = Send(Inventory(), "PUT", $"/category/{Cof}/links", new OSDMap { ["nonsense"] = 1 }); + Assert.That(status, Is.EqualTo((int)HttpStatusCode.BadRequest)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(400)); + } + + /// + /// A5: the exact map asAISCreateCatLLSD sends (llinventory.cpp:1256-1276) - category_id null, parent_id, + /// type_default as an integer, name, and optionally thumbnail and favorite. Everything is accepted; the two + /// optional ones have no column in this tree and are dropped. + /// + [Test] + public void the_categories_create_map_is_accepted_exactly_as_the_viewer_sends_it() + { + var b = Inventory(); + + var (status, body) = Send(b, "POST", $"/category/{Clothing}", new OSDMap + { + ["categories"] = new OSDArray + { + new OSDMap + { + ["category_id"] = UUID.Zero, // null on a create: the server assigns it + ["parent_id"] = Clothing, + ["type_default"] = (int)FolderType.Outfit, + ["name"] = "Beach Outfit", + ["thumbnail"] = new OSDMap { ["asset_id"] = UUID.Random() }, + ["favorite"] = new OSDMap { ["toggled"] = true }, + }, + }, + }); + + Assert.That(status, Is.EqualTo(200), "thumbnail and favorite must not make the create fail"); + var created = ((OSDArray)body["_created_categories"]).Single().AsUUID(); + Assert.That(created, Is.Not.EqualTo(UUID.Zero), "the server assigned an id rather than echoing the null one"); + + var folder = b.Folders[created]; + Assert.That(folder.Name, Is.EqualTo("Beach Outfit")); + Assert.That(folder.Type, Is.EqualTo((short)FolderType.Outfit), "type_default is the integer folder type"); + Assert.That(folder.ParentID, Is.EqualTo(Clothing)); + Assert.That(folder.Owner, Is.EqualTo(Agent)); + } + + // ================================================================== AIS-SEC-2 + // + // A slam has REPLACEMENT semantics, so "I could not understand this body" and "this body asks for no links" + // are one keystroke apart in effect and a universe apart in meaning. Before this, ReadBodyOsd ended in + // `catch { return new OSDMap(); }` and ParseBody treated an empty map as an empty slam, so a truncated + // PUT /category/{COF}/links — a dropped connection is enough — was read as "remove every link" and emptied + // the wearer's Current Outfit. + // + // Every case below asserts the same two things: the status, and that NOT ONE backend write was attempted. + // The second is the real assertion. A 400 that had already called DeleteItems would be no fix at all. + + /// + /// The body ceiling the handler is required to enforce. Step 2 adds a cross-check against + /// AisHandler.MaxBodyBytes inside the 413 test, so the two cannot drift apart. + /// + private const int BodyCeiling = 1024 * 1024; + + /// The COF's link rows, id -> (name, target), so a test can prove they are untouched field by field. + private static Dictionary LinkRows(FakeAisBackend b, UUID folder) + => b.Items.Values + .Where(i => i.Folder == folder && (i.AssetType == (int)AssetType.Link || i.AssetType == (int)AssetType.LinkFolder)) + .ToDictionary(i => i.ID, i => (i.Name, i.AssetID)); + + /// A well-formed slam body, so the invalid cases differ from a good one in exactly one respect. + private static OSDMap GoodLink(UUID target) => new() + { + ["name"] = "link to " + target, + ["desc"] = "", + ["linked_id"] = target, + ["type"] = (int)AssetType.Link, + }; + + private static void AssertRefused(FakeAisBackend b, Dictionary before, + int status, OSDMap body, int expected) + { + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(expected)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(expected)); + Assert.That(b.Writes, Is.Empty, "a refused body must not reach a single backend write"); + Assert.That(LinkRows(b, Cof), Is.EqualTo(before), "the COF's link rows must be byte-for-byte what they were"); + Assert.That(body.ContainsKey("_removed_items"), Is.False, "and nothing may be reported as removed"); + Assert.That(body.ContainsKey("_updated_category_versions"), Is.False, + "no version delta either, or the viewer advances past a state the server never reached"); + }); + } + + [Test] + public void a_slam_body_of_truncated_xml_is_400_and_writes_nothing() + { + var b = Inventory(); + var before = LinkRows(b, Cof); + var raw = System.Text.Encoding.UTF8.GetBytes(""); + var (status, body) = SendRaw(b, "PUT", $"/category/{Cof}/links", raw); + AssertRefused(b, before, status, body, 400); + } + + [Test] + public void a_slam_body_of_valid_xml_that_is_not_llsd_is_400_and_writes_nothing() + { + var b = Inventory(); + var before = LinkRows(b, Cof); + var raw = System.Text.Encoding.UTF8.GetBytes(""); + var (status, body) = SendRaw(b, "PUT", $"/category/{Cof}/links", raw); + AssertRefused(b, before, status, body, 400); + } + + /// + /// No body at all. This is the one that made the defect reachable without malice: an interrupted PUT arrives + /// with nothing in it, and "nothing" used to mean "empty slam". + /// + [Test] + public void a_slam_with_no_body_is_400_and_writes_nothing() + { + var b = Inventory(); + var before = LinkRows(b, Cof); + var (status, body) = SendRaw(b, "PUT", $"/category/{Cof}/links", Array.Empty()); + AssertRefused(b, before, status, body, 400); + } + + /// + /// An empty MAP is not an empty slam. The viewer sends a bare LLSD array and never {} + /// (spec A-Q3, llappearancemgr.cpp:2209-2245), so {} can only be a client we do not know or a + /// body that arrived damaged — and under replacement semantics the safe reading of both is "refuse". + /// + [Test] + public void a_slam_body_of_an_empty_map_is_400_and_writes_nothing() + { + var b = Inventory(); + var before = LinkRows(b, Cof); + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", new OSDMap()); + AssertRefused(b, before, status, body, 400); + } + + /// + /// All-or-nothing. Accepting the good entries and dropping the bad one would delete the old links the viewer + /// meant to keep, which is the same outfit loss by a quieter route. + /// + [Test] + public void a_slam_array_holding_a_non_map_entry_is_400_and_writes_nothing() + { + var b = Inventory(); + var before = LinkRows(b, Cof); + var slam = new OSDArray { GoodLink(Targets[0]), OSD.FromString("not a link map"), GoodLink(Targets[1]) }; + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", slam); + AssertRefused(b, before, status, body, 400); + } + + [Test] + public void a_slam_link_with_a_zero_linked_id_is_400_and_writes_nothing() + { + var b = Inventory(); + var before = LinkRows(b, Cof); + var bad = GoodLink(Targets[0]); + bad["linked_id"] = UUID.Zero; + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", new OSDArray { GoodLink(Targets[1]), bad }); + AssertRefused(b, before, status, body, 400); + } + + /// + /// A slam body describes links and nothing else: both builders switch on AT_LINK / AT_LINK_FOLDER + /// (llappearancemgr.cpp:1795-1833). A type-0 (Texture) entry would have been stored as a link row with + /// an asset type no fetch route knows how to present. + /// + [Test] + public void a_slam_link_of_a_type_that_is_not_a_link_is_400_and_writes_nothing() + { + var b = Inventory(); + var before = LinkRows(b, Cof); + var bad = GoodLink(Targets[0]); + bad["type"] = (int)AssetType.Texture; // 0 + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", new OSDArray { GoodLink(Targets[1]), bad }); + AssertRefused(b, before, status, body, 400); + } + + /// + /// The ceiling is enforced while the stream is copied, so an oversized body is refused without ever being + /// held in memory — the point being that a body too big to trust is also a body too big to buffer. + /// + [Test] + public void a_slam_body_over_the_size_ceiling_is_413_and_writes_nothing() + { + var b = Inventory(); + var before = LinkRows(b, Cof); + Assert.That(AisHandler.MaxBodyBytes, Is.EqualTo(BodyCeiling), + "the handler's ceiling and the one this test exercises must be the same number"); + var raw = new byte[BodyCeiling + 1]; + for (var i = 0; i < raw.Length; i++) raw[i] = (byte)'a'; + var (status, body) = SendRaw(b, "PUT", $"/category/{Cof}/links", raw); + AssertRefused(b, before, status, body, 413); + } + + /// + /// Positive control. An empty ARRAY stays an intentional empty slam — that is how the viewer takes off the + /// last garment — and is the case the negative ones must not sweep up with them. + /// (a_slam_of_an_empty_array_empties_the_folders_links is the same assertion from A3's side.) + /// + [Test] + public void the_empty_array_is_still_an_intentional_empty_slam() + { + var b = Inventory(); + var oldIds = LinkIds(b, Cof); + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", new OSDArray()); + + Assert.That(status, Is.EqualTo(200)); + Assert.That(LinkIds(b, Cof), Is.Empty); + Assert.That(((OSDArray)body["_removed_items"]).Select(o => o.AsUUID()), Is.EquivalentTo(oldIds)); + Assert.That(b.Writes, Is.Not.Empty, "an intentional empty slam DOES write - it removes the old links"); + } + + /// Positive control: an ordinary two-link slam still replaces the folder's links. + [Test] + public void a_valid_two_link_slam_still_replaces_the_links() + { + var b = Inventory(); + var oldIds = LinkIds(b, Cof); + var (status, body) = Send(b, "PUT", $"/category/{Cof}/links", + new OSDArray { GoodLink(Targets[2]), GoodLink(Targets[3]) }); + + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(200)); + Assert.That(LinkTargets(b, Cof), Is.EquivalentTo(new[] { Targets[2], Targets[3] })); + Assert.That(LinkIds(b, Cof).Intersect(oldIds), Is.Empty, "the previous link rows are gone"); + Assert.That(((OSDArray)body["_created_items"]), Has.Count.EqualTo(2)); + Assert.That(((OSDArray)body["_removed_items"]).Select(o => o.AsUUID()), Is.EquivalentTo(oldIds)); + }); + } + + // ------------------------------------------------------------------ the other body-carrying mutations + + [Test] + public void a_PATCH_item_with_a_malformed_body_is_400_and_writes_nothing() + { + var b = Inventory(); + var nameBefore = b.Items[PlainItem].Name; + var raw = System.Text.Encoding.UTF8.GetBytes("name"); + var (status, body) = SendRaw(b, "PATCH", $"/item/{PlainItem}", raw); + + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(400)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(400)); + Assert.That(b.Writes, Is.Empty); + Assert.That(b.Items[PlainItem].Name, Is.EqualTo(nameBefore)); + }); + } + + [Test] + public void a_PATCH_category_with_a_malformed_body_is_400_and_writes_nothing() + { + var b = Inventory(); + var nameBefore = b.Folders[Clothing].Name; + var versionBefore = b.Folders[Clothing].Version; + var raw = System.Text.Encoding.UTF8.GetBytes("namex"); + var (status, body) = SendRaw(b, "PATCH", $"/category/{Clothing}", raw); + + Assert.Multiple(() => + { + Assert.That(status, Is.EqualTo(400)); + Assert.That(body["error_code"].AsInteger(), Is.EqualTo(400)); + Assert.That(b.Writes, Is.Empty); + Assert.That(b.Folders[Clothing].Name, Is.EqualTo(nameBefore)); + Assert.That(b.Folders[Clothing].Version, Is.EqualTo(versionBefore)); + }); + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/CapturedLog.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/CapturedLog.cs new file mode 100644 index 00000000000..9fe61ca97ba --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/CapturedLog.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using OpenSim.Framework; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// Captures what the code under test logged, for the assertions that care about an operator being able to see a +/// fault without opening the database (A7). +/// +/// Swapping is enough because loggers held in +/// static readonly fields are DeferredLoggers, which rebind whenever the factory reference changes +/// (DeferredLogger.cs:36-51). The previous factory is restored on dispose so fixtures do not leak into +/// each other. +/// +public sealed class CapturedLog : IDisposable, ILoggerFactory +{ + private readonly ILoggerFactory m_previous; + private readonly List<(LogLevel Level, string Message, Exception Exception)> m_entries = new(); + + public CapturedLog() + { + m_previous = LoggerProvider.LoggerFactory; + LoggerProvider.LoggerFactory = this; + } + + /// + /// The lowest level this capture reports as enabled. Set it above to assert that + /// code guarded by IsEnabled does no work — which is how the A11 logging keeps its cost claim honest. + /// + public LogLevel Enabled { get; init; } = LogLevel.Trace; + + public IReadOnlyList Warnings => Messages(LogLevel.Warning); + + public IReadOnlyList Messages(LogLevel level) + { + var found = new List(); + lock (m_entries) + foreach (var (entryLevel, message, _) in m_entries) + if (entryLevel == level) + found.Add(message); + return found; + } + + /// + /// AIS-SEC-5. The entries at a level with the exception object. returns only + /// the formatted message, and a structured LogError(ex, template, args) does not fold the exception + /// into that string - so asserting that a handler really passed the exception to the logger, rather than + /// merely mentioning a fault in the text, needs this. + /// + public IReadOnlyList<(string Message, Exception Exception)> Entries(LogLevel level) + { + var found = new List<(string, Exception)>(); + lock (m_entries) + foreach (var (entryLevel, message, exception) in m_entries) + if (entryLevel == level) + found.Add((message, exception)); + return found; + } + + public void Dispose() => LoggerProvider.LoggerFactory = m_previous; + + ILogger ILoggerFactory.CreateLogger(string categoryName) => new Recorder(this); + void ILoggerFactory.AddProvider(ILoggerProvider provider) { } + + private void Record(LogLevel level, string message, Exception exception) + { + lock (m_entries) + m_entries.Add((level, message, exception)); + } + + private sealed class Recorder : ILogger + { + private readonly CapturedLog m_owner; + public Recorder(CapturedLog owner) { m_owner = owner; } + + public IDisposable BeginScope(TState state) where TState : notnull => NullScope.Instance; + public bool IsEnabled(LogLevel logLevel) => logLevel >= m_owner.Enabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + Func formatter) + { + if (!IsEnabled(logLevel)) return; + m_owner.Record(logLevel, formatter(state, exception), exception); + } + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + public void Dispose() { } + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/FakeAisBackend.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/FakeAisBackend.cs new file mode 100644 index 00000000000..b3741d582bf --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/FakeAisBackend.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// An in-memory inventory for the AIS tests: folders and items keyed by id, with the same rules the region +/// backend has — a folder's contents are the rows whose parent is that folder, links are items with +/// AssetType.Link, and everything is scoped to one owner. Records what was asked of it so a test can prove +/// the handler resolved link targets with one batched call rather than N single ones. +/// +public sealed class FakeAisBackend : IAisInventoryBackend +{ + public readonly Dictionary Folders = new(); + public readonly Dictionary Items = new(); + public readonly List Calls = new(); + + /// + /// AIS-SEC-3. Invoked at the start of every backend call with the same label records, and + /// before the call touches the store. A test blocks in here on a ManualResetEventSlim to pin an + /// interleaving exactly — a sleep-based race test passes or fails on machine load, which is worse than no test. + /// + /// It fires outside every lock this class takes, deliberately. A thread parked in here holds nothing, so + /// the other thread can run to completion — which is what lets the concurrency tests produce a deterministic + /// interleaving with no two threads ever inside the store at once, and is also why the "different folders must + /// not block each other" test cannot pass for the wrong reason. + /// + public Action BeforeCall; + + /// + /// AIS-SEC-5. Return an exception from here to make a named backend call throw, which is the only way + /// to reach AisHandler.Dispatch's unexpected-exception path. The existing gates all return bool + /// and model a refusal, not a fault - a database or connector error is a fault, and that is the path whose + /// error hygiene AIS-SEC-5 is about. + /// + public Func ThrowOn; + + /// Records the call, then gives the hooks a chance to park or fault this thread. + private void Record(string label) + { + lock (Calls) Calls.Add(label); // two threads append in the concurrency tests + BeforeCall?.Invoke(label); + Exception fault = ThrowOn?.Invoke(label); + if (fault is not null) throw fault; + } + + /// A snapshot of safe to take while another thread may still be recording. + public IReadOnlyList CallSnapshot() { lock (Calls) return Calls.ToList(); } + + /// + /// The subset of that could change inventory — every backend member that writes, whether + /// or not it went on to succeed, because the member being reached at all is what AIS-SEC-2 is about. + /// A malformed body must leave this empty: not "a write that failed", but no write attempted. + /// + public IReadOnlyList Writes => Calls.Where(c => + c.StartsWith("AddFolder(") || c.StartsWith("AddItem(") || + c.StartsWith("UpdateItem(") || c.StartsWith("UpdateFolder(") || + c.StartsWith("DeleteItems[") || c.StartsWith("DeleteFolders[") || + c.StartsWith("PurgeFolder(") || c.StartsWith("ApplyAssetTransaction(")).ToList(); + + public UUID Owner; + public UUID CurrentOutfitId = UUID.Zero; + + public FakeAisBackend(UUID owner) { Owner = owner; } + + // ---------------- building ---------------- + + public InventoryFolderBase AddFolder(UUID id, UUID parent, string name, int version = 1, short type = -1) + { + var folder = new InventoryFolderBase(id, name, Owner, type, parent, (ushort)version); + Folders[id] = folder; + return folder; + } + + public InventoryItemBase AddItem(UUID id, UUID folder, string name, int assetType = (int)AssetType.Clothing, UUID assetId = default) + { + var item = new InventoryItemBase(id, Owner) + { + Folder = folder, + Name = name, + Description = "", + AssetType = assetType, + InvType = (int)InventoryType.Wearable, + AssetID = assetId.IsZero() ? UUID.Random() : assetId, + CreationDate = 1756900000, + Flags = 0, + CreatorId = Owner.ToString(), + BasePermissions = 0x7fffffff, + CurrentPermissions = 0x7fffffff, + NextPermissions = 532480, + SalePrice = 0, + SaleType = 0, + }; + Items[id] = item; + return item; + } + + /// A link row: an item of AssetType.Link whose asset id is the target item's id. + public InventoryItemBase AddLink(UUID id, UUID folder, string name, UUID target) + => AddItem(id, folder, name, (int)AssetType.Link, target); + + // ---------------- IAisInventoryBackend ---------------- + + public InventoryFolderBase GetFolderForType(UUID agentId, FolderType type) + { + Record($"GetFolderForType({type})"); + if (agentId != Owner) return null; + if (type == FolderType.CurrentOutfit && !CurrentOutfitId.IsZero()) + return Folders.TryGetValue(CurrentOutfitId, out var cof) ? cof : null; + return Folders.Values.FirstOrDefault(f => f.Type == (short)type); + } + + public InventoryFolderBase GetFolder(UUID agentId, UUID folderId) + { + Record($"GetFolder({folderId})"); + if (agentId != Owner) return null; + return Folders.TryGetValue(folderId, out var folder) ? folder : null; + } + + public InventoryCollection GetFolderContent(UUID agentId, UUID folderId) + { + Record($"GetFolderContent({folderId})"); + if (agentId != Owner || !Folders.TryGetValue(folderId, out var folder)) return null; + return new InventoryCollection + { + OwnerID = Owner, + FolderID = folderId, + Version = folder.Version, + Folders = Folders.Values.Where(f => f.ParentID == folderId).ToList(), + Items = Items.Values.Where(i => i.Folder == folderId).ToList(), + }; + } + + public IReadOnlyList GetSubFolders(UUID agentId, UUID folderId) + { + Record($"GetSubFolders({folderId})"); + if (agentId != Owner || !Folders.ContainsKey(folderId)) return Array.Empty(); + return Folders.Values.Where(f => f.ParentID == folderId).ToList(); + } + + public IReadOnlyList GetInventorySkeleton(UUID agentId) + { + Record("GetInventorySkeleton"); + return agentId != Owner ? Array.Empty() : Folders.Values.ToList(); + } + + public IReadOnlyList GetItems(UUID agentId, IReadOnlyList itemIds) + { + Record($"GetItems[{itemIds.Count}]"); + if (agentId != Owner) return Array.Empty(); + var found = new List(); + foreach (var id in itemIds) if (Items.TryGetValue(id, out var item)) found.Add(item); + return found; + } + + public InventoryItemBase GetItem(UUID agentId, UUID itemId) + { + Record($"GetItem({itemId})"); + if (agentId != Owner) return null; + return Items.TryGetValue(itemId, out var item) ? item : null; + } + + // ---------------- mutators (A2) ---------------- + + /// Set to false to make the service refuse writes, as XInventoryService does when AllowDelete is off. + public bool AllowWrite = true; + + /// + /// Set to true to reproduce this tree's real DeleteFolders behaviour: the two-argument overload on + /// IInventoryService is onlyIfTrash = true, so a folder outside Trash is silently skipped and true is still + /// returned (XInventoryService.cs:459-478). + /// + public bool DeleteFoldersOnlyIfTrash = false; + + /// Fault injection: return false to make this AddItem fail. Null means every add succeeds. + public Func AddItemGate; + + /// + /// Fault injection: return false to make this AddFolder fail. Null means every add succeeds. Added for + /// AIS-SEC-4, which needs a create to fail on the second of three categories - the whole point being + /// what the response says about the first one. + /// + public Func AddFolderGate; + + /// Fault injection: return false to make this PurgeFolder fail. Null means it succeeds. + public Func PurgeFolderGate; + + /// Fault injection: return false to make this DeleteFolders fail. Null means it succeeds. + public Func, bool> DeleteFoldersGate; + + /// Fault injection: return false to make this DeleteItems fail. Null means every delete succeeds. + public Func, bool> DeleteItemsGate; + + /// Runs after every successful write, so a test can change the store underneath the handler. + public Action OnWrite; + + /// The data layer bumps a folder's version on every store or delete of its contents (S0a V6). + private void Bump(UUID folderId) + { + if (Folders.TryGetValue(folderId, out var folder)) folder.Version = (ushort)(folder.Version + 1); + } + + public bool AddFolder(InventoryFolderBase folder) + { + Record($"AddFolder({folder.ID})"); + if (!AllowWrite) return false; + if (AddFolderGate is not null && !AddFolderGate(folder)) return false; + Folders[folder.ID] = folder; + Bump(folder.ParentID); + OnWrite?.Invoke(); + return true; + } + + public bool AddItem(InventoryItemBase item) + { + Record($"AddItem({item.Name})"); + if (!AllowWrite) return false; + if (AddItemGate is not null && !AddItemGate(item)) return false; + Items[item.ID] = item; + Bump(item.Folder); + OnWrite?.Invoke(); + return true; + } + + /// + /// Asset transactions the region has completed: transaction id -> the asset it uploaded. A16 — + /// stands in for the region's asset-transaction module, whose real + /// behaviour is to set the item's asset and store the item itself (AssetXferUploader.cs:425-430). + /// + public readonly Dictionary Transactions = new(); + + /// False when this backend cannot resolve transactions at all, as the library backend cannot. + public bool ResolvesTransactions = true; + + /// + /// A19: transactions the region's validator REFUSES - the uploaded asset referenced something the resident may + /// not use, so nothing is stored and the item keeps the asset it had. Distinct from a transaction this backend + /// cannot resolve at all, which is not a failure. + /// + public readonly HashSet RefusedTransactions = new(); + + public AisAssetTransaction ApplyAssetTransaction(UUID agentId, UUID transactionId, InventoryItemBase item) + { + Record($"ApplyAssetTransaction({transactionId})"); + if (!ResolvesTransactions || agentId != Owner) return AisAssetTransaction.NotResolvable; + if (RefusedTransactions.Contains(transactionId)) return AisAssetTransaction.Refused; + // An unknown transaction opens a pending uploader and the asset lands with the xfer; nothing is stored yet. + if (!Transactions.TryGetValue(transactionId, out var assetId)) return AisAssetTransaction.Applied; + item.AssetID = assetId; + UpdateItem(item); + return AisAssetTransaction.Applied; + } + + /// S9: every (item, newAsset) the handler reported as an asset change, in order. + public readonly List<(UUID Item, UUID Asset)> AssetChanges = new(); + + public void OnItemAssetChanged(UUID agentId, UUID itemId, UUID newAssetId) + { + Record($"OnItemAssetChanged({itemId})"); + if (agentId == Owner) AssetChanges.Add((itemId, newAssetId)); + } + + public bool UpdateItem(InventoryItemBase item) + { + Record($"UpdateItem({item.ID})"); + if (!AllowWrite || !Items.ContainsKey(item.ID)) return false; + Items[item.ID] = item; + Bump(item.Folder); + OnWrite?.Invoke(); + return true; + } + + public bool UpdateFolder(InventoryFolderBase folder) + { + Record($"UpdateFolder({folder.ID})"); + if (!AllowWrite || !Folders.ContainsKey(folder.ID)) return false; + Folders[folder.ID] = folder; + Bump(folder.ParentID); + OnWrite?.Invoke(); + return true; + } + + public bool DeleteItems(UUID agentId, IReadOnlyList itemIds) + { + Record($"DeleteItems[{itemIds.Count}]"); + if (!AllowWrite || agentId != Owner) return false; + if (DeleteItemsGate is not null && !DeleteItemsGate(itemIds)) return false; + foreach (var id in itemIds) + if (Items.TryGetValue(id, out var item)) { Items.Remove(id); Bump(item.Folder); } + OnWrite?.Invoke(); + return true; + } + + /// Recursive, and with the real service's trash gate available for a test to switch on. + public bool DeleteFolders(UUID agentId, IReadOnlyList folderIds, bool onlyIfTrash) + { + Record($"DeleteFolders[{folderIds.Count}, onlyIfTrash={onlyIfTrash}]"); + if (!AllowWrite || agentId != Owner) return false; + if (DeleteFoldersGate is not null && !DeleteFoldersGate(folderIds)) return false; + foreach (var id in folderIds) + { + if (!Folders.TryGetValue(id, out var folder)) continue; + if (onlyIfTrash && DeleteFoldersOnlyIfTrash && !UnderTrash(id)) continue; // as the real service does + Purge(id); + Folders.Remove(id); + Bump(folder.ParentID); + } + OnWrite?.Invoke(); + return true; + } + + public bool PurgeFolder(InventoryFolderBase folder) + { + Record($"PurgeFolder({folder.ID})"); + if (!AllowWrite) return false; + if (PurgeFolderGate is not null && !PurgeFolderGate(folder)) return false; + Purge(folder.ID); + Bump(folder.ID); + OnWrite?.Invoke(); + return true; + } + + /// Everything under a folder, depth first. + private void Purge(UUID folderId) + { + foreach (var child in Folders.Values.Where(f => f.ParentID == folderId).Select(f => f.ID).ToList()) + { + Purge(child); + Folders.Remove(child); + } + foreach (var item in Items.Values.Where(i => i.Folder == folderId).Select(i => i.ID).ToList()) + Items.Remove(item); + } + + /// The Trash / Lost And Found test the real service applies before it will delete a folder. + public UUID TrashId = UUID.Zero; + private bool UnderTrash(UUID folderId) + { + var id = folderId; + for (var guard = 0; guard < 64 && Folders.TryGetValue(id, out var folder); guard++) + { + if (folder.ParentID == TrashId && !TrashId.IsZero()) return true; + id = folder.ParentID; + } + return false; + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/category-fetch.xml b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/category-fetch.xml new file mode 100644 index 00000000000..dc778198d3c --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/category-fetch.xml @@ -0,0 +1,85 @@ + + + + + category_id11111111-1111-4111-8111-111111111111 + parent_id00000000-0000-0000-0000-000000000001 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + nameClothing + type_default5 + version7 + _embedded + + categories + + 66666666-6666-4666-8666-666666666666 + + category_id66666666-6666-4666-8666-666666666666 + parent_id11111111-1111-4111-8111-111111111111 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + nameOutfits + type_default-1 + version2 + _embedded + + categories + items + links + + + + items + + 22222222-2222-4222-8222-222222222222 + + item_id22222222-2222-4222-8222-222222222222 + parent_id11111111-1111-4111-8111-111111111111 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + asset_id77777777-7777-4777-8777-777777777777 + nameBlue Shirt + desc + type5 + inv_type18 + flags4 + created_at1756900000 + permissions + + base_mask2147483647 + owner_mask2147483647 + group_mask0 + everyone_mask0 + next_owner_mask532480 + creator_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + owner_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + group_id00000000-0000-0000-0000-000000000000 + is_owner_groupfalse + + sale_info + + sale_type0 + sale_price10 + + + + links + + 88888888-8888-4888-8888-888888888888 + + item_id88888888-8888-4888-8888-888888888888 + parent_id11111111-1111-4111-8111-111111111111 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + linked_id22222222-2222-4222-8222-222222222222 + nameBlue Shirt + desc@clothing + type24 + inv_type18 + flags4 + created_at1756900001 + + + + + diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/cof-links.xml b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/cof-links.xml new file mode 100644 index 00000000000..c312337751a --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/cof-links.xml @@ -0,0 +1,49 @@ + + + + + category_id99999999-9999-4999-8999-999999999999 + parent_id00000000-0000-0000-0000-000000000001 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + nameCurrent Outfit + type_default46 + version12 + _embedded + + links + + 88888888-8888-4888-8888-888888888888 + + item_id88888888-8888-4888-8888-888888888888 + parent_id99999999-9999-4999-8999-999999999999 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + linked_id22222222-2222-4222-8222-222222222222 + nameBlue Shirt + desc@clothing + type24 + inv_type18 + flags4 + created_at1756900001 + _embedded + + item + + item_id22222222-2222-4222-8222-222222222222 + parent_id11111111-1111-4111-8111-111111111111 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + asset_id77777777-7777-4777-8777-777777777777 + nameBlue Shirt + desc + type5 + inv_type18 + flags4 + created_at1756900000 + + + + + + + diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/error.xml b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/error.xml new file mode 100644 index 00000000000..9e464af5cb4 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/error.xml @@ -0,0 +1,11 @@ + + + + + error_code501 + error_descriptionNotImplemented + messageFetchCategoryChildren is not implemented + + diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/item-fetch.xml b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/item-fetch.xml new file mode 100644 index 00000000000..32135c9d020 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/item-fetch.xml @@ -0,0 +1,33 @@ + + + + + item_id22222222-2222-4222-8222-222222222222 + parent_id11111111-1111-4111-8111-111111111111 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + asset_id77777777-7777-4777-8777-777777777777 + nameBlue Shirt + desc + type5 + inv_type18 + flags4 + created_at1756900000 + permissions + + base_mask2147483647 + owner_mask2147483647 + group_mask0 + everyone_mask0 + next_owner_mask532480 + creator_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + owner_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + group_id00000000-0000-0000-0000-000000000000 + is_owner_groupfalse + + sale_info + + sale_type0 + sale_price10 + + + diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/link-fetch.xml b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/link-fetch.xml new file mode 100644 index 00000000000..e8e2d1b81ef --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/link-fetch.xml @@ -0,0 +1,34 @@ + + + + + item_id88888888-8888-4888-8888-888888888888 + parent_id99999999-9999-4999-8999-999999999999 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + linked_id22222222-2222-4222-8222-222222222222 + nameBlue Shirt + desc@clothing + type24 + inv_type18 + flags4 + created_at1756900001 + _embedded + + item + + item_id22222222-2222-4222-8222-222222222222 + parent_id11111111-1111-4111-8111-111111111111 + agent_idaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa + asset_id77777777-7777-4777-8777-777777777777 + nameBlue Shirt + desc + type5 + inv_type18 + flags4 + created_at1756900000 + + + + diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/mutation-envelope.xml b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/mutation-envelope.xml new file mode 100644 index 00000000000..289b5f2e7b2 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/Fixtures/mutation-envelope.xml @@ -0,0 +1,24 @@ + + + + + _categories_removed + _category_items_removed + _removed_items + _broken_links_removed + _created_items + _created_categories + _updated_category_versions + + 11111111-1111-4111-8111-1111111111117 + + _embedded + + categories + items + links + + + diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/PrincipalIgnoringInventoryService.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/PrincipalIgnoringInventoryService.cs new file mode 100644 index 00000000000..47740c30ad7 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/PrincipalIgnoringInventoryService.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Services.Interfaces; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// An that reproduces the one behaviour of XInventoryService the AIS backend +/// has to defend against: it looks objects up by UUID and disregards the principal it was handed. Every read +/// and every delete here answers for any row in the store regardless of who owns it, exactly as the real service +/// does - GetItem queries inventoryID alone (XInventoryService.cs:633-641), GetFolder +/// queries folderID alone (:653-663), GetFolderContent says so in a comment ("This method +/// doesn't receive a valud principal id from the connector. So we disregard the principal and look by ID", +/// :319-323), DeleteFolders says "Ignore principal ID, it's bogus at connector level" +/// (:482-492) and DeleteItems says "Just use the ID... *facepalms*" (:602-631). +/// +/// +/// This is the opposite of , which enforces owner scoping itself and therefore cannot +/// see the AIS-SEC-1 defect at all. Running the real AISv3Module.InventoryServiceBackend over this +/// double is what makes the cross-user cases in AisCrossUserHttpTests mean anything. +/// +/// +/// Two deliberate over-approximations, recorded so nobody reads this file as a description of the real +/// service: +/// +/// GetInventorySkeleton here returns every folder in the store. The real one does filter by +/// agentID (XInventoryService.cs:233-237). The double is harsher on purpose: it makes the backend +/// filter the skeleton itself rather than inherit a guarantee from the connector, which is the same assumption +/// the defect was made of. +/// GetFolderForType here returns the first folder of that type owned by anyone. The real one +/// resolves through GetRootFolder(principalID) (:277-292) and so is scoped. Same reason. +/// +/// +/// Mutations store whatever they are given, without an ownership opinion - again as the real service does. +/// Members the AIS backend never calls throw rather than pretend. +/// +public sealed class PrincipalIgnoringInventoryService : IInventoryService +{ + public readonly Dictionary Folders = new(); + public readonly Dictionary Items = new(); + + // ---------------- seeding, by owner, straight into the store ---------------- + + public InventoryFolderBase Seed(UUID id, UUID owner, UUID parent, string name, short type, int version = 1) + { + var folder = new InventoryFolderBase(id, name, owner, type, parent, (ushort)version); + Folders[id] = folder; + return folder; + } + + public InventoryItemBase SeedItem(UUID id, UUID owner, UUID folder, string name, + int assetType = (int)AssetType.Clothing, UUID assetId = default) + { + var item = new InventoryItemBase(id, owner) + { + Folder = folder, + Name = name, + Description = "", + AssetType = assetType, + InvType = (int)InventoryType.Wearable, + AssetID = assetId.IsZero() ? UUID.Random() : assetId, + CreationDate = 1756900000, + Flags = 0, + CreatorId = owner.ToString(), + BasePermissions = 0x7fffffff, + CurrentPermissions = 0x7fffffff, + NextPermissions = 532480, + }; + Items[id] = item; + return item; + } + + /// A link row: an item of AssetType.Link whose asset id is the target item's id. + public InventoryItemBase SeedLink(UUID id, UUID owner, UUID folder, string name, UUID target) + => SeedItem(id, owner, folder, name, (int)AssetType.Link, target); + + // ---------------- the reads that disregard the principal ---------------- + + /// By id alone (XInventoryService.cs:633-641). + public InventoryItemBase GetItem(UUID userID, UUID itemID) + => Items.TryGetValue(itemID, out var item) ? item : null; + + /// By id alone (:653-663). + public InventoryFolderBase GetFolder(UUID userID, UUID folderID) + => Folders.TryGetValue(folderID, out var folder) ? folder : null; + + /// One slot per requested id, null where the id is unknown (:643-651). + public InventoryItemBase[] GetMultipleItems(UUID userID, UUID[] ids) + { + var found = new InventoryItemBase[ids.Length]; + for (var i = 0; i < ids.Length; i++) found[i] = GetItem(userID, ids[i]); + return found; + } + + /// By parent id alone; the owner and version come off the folder row (:319-361). + public InventoryCollection GetFolderContent(UUID userID, UUID folderID) + { + var collection = new InventoryCollection + { + OwnerID = userID, + FolderID = folderID, + Folders = Folders.Values.Where(f => f.ParentID == folderID).ToList(), + Items = Items.Values.Where(i => i.Folder == folderID).ToList(), + }; + if (Folders.TryGetValue(folderID, out var folder)) + { + collection.Version = folder.Version; + collection.OwnerID = folder.Owner; + } + return collection; + } + + /// Every folder in the store - see the over-approximation note on the class. + public List GetInventorySkeleton(UUID userId) => Folders.Values.ToList(); + + /// The first folder of that type owned by anyone - see the over-approximation note on the class. + public InventoryFolderBase GetFolderForType(UUID userID, FolderType type) + => Folders.Values.FirstOrDefault(f => f.Type == (short)type); + + // ---------------- the deletes that disregard the principal ---------------- + + /// By id alone, and true whatever happened (:602-631). + public bool DeleteItems(UUID userID, List itemIDs) + { + foreach (var id in itemIDs) Items.Remove(id); + return true; + } + + public bool DeleteFolders(UUID userID, List folderIDs) => DeleteFolders(userID, folderIDs, true); + + /// By id alone, purging each folder first, and true whatever happened (:482-503). + public bool DeleteFolders(UUID userID, List folderIDs, bool onlyIfTrash) + { + foreach (var id in folderIDs) + { + if (!Folders.TryGetValue(id, out var folder)) continue; + if (onlyIfTrash && !ParentIsTrashOrLost(folder)) continue; + PurgeFolder(folder); + Folders.Remove(id); + } + return true; + } + + /// + /// By id alone, recursively. The trash restriction the real one-argument form applies (:503-528) is + /// deliberately not reproduced here: it would only mask the ownership question this double exists to ask. + /// + public bool PurgeFolder(InventoryFolderBase folder) + { + foreach (var child in Folders.Values.Where(f => f.ParentID == folder.ID).ToList()) + { + PurgeFolder(child); + Folders.Remove(child.ID); + } + foreach (var item in Items.Values.Where(i => i.Folder == folder.ID).ToList()) + Items.Remove(item.ID); + return true; + } + + private bool ParentIsTrashOrLost(InventoryFolderBase folder) + => Folders.TryGetValue(folder.ParentID, out var parent) + && (parent.Type == (short)FolderType.Trash || parent.Type == (short)FolderType.LostAndFound); + + // ---------------- the mutations: store whatever you are given ---------------- + + public bool AddFolder(InventoryFolderBase folder) { Folders[folder.ID] = folder; return true; } + public bool UpdateFolder(InventoryFolderBase folder) { Folders[folder.ID] = folder; return true; } + public bool AddItem(InventoryItemBase item) { Items[item.ID] = item; return true; } + public bool UpdateItem(InventoryItemBase item) { Items[item.ID] = item; return true; } + + // ---------------- never called by the AIS backend ---------------- + + public bool CreateUserInventory(UUID user) => throw new NotImplementedException(); + public InventoryFolderBase GetRootFolder(UUID userID) => throw new NotImplementedException(); + public InventoryCollection[] GetMultipleFoldersContent(UUID userID, UUID[] folderIDs) => throw new NotImplementedException(); + public List GetFolderItems(UUID userID, UUID folderID) => throw new NotImplementedException(); + public bool MoveFolder(InventoryFolderBase folder) => throw new NotImplementedException(); + public bool MoveItems(UUID ownerID, List items) => throw new NotImplementedException(); + public bool HasInventoryForUser(UUID userID) => throw new NotImplementedException(); + public List GetActiveGestures(UUID userId) => throw new NotImplementedException(); + public int GetAssetPermissions(UUID userID, UUID assetID) => throw new NotImplementedException(); +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/WornWearableRebakeTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/WornWearableRebakeTests.cs new file mode 100644 index 00000000000..4dca3e5e27c --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/WornWearableRebakeTests.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using NUnit.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; + +/// +/// S9. Editing a worn wearable — open it, change a colour, Save — changed the asset and produced no rebake. +/// +/// Why nothing fired. Three signals could have told the region, and none did. The worn SET does not +/// move, because the viewer keeps the item id when it saves (llagentwearables.cpp:319, +/// new_wearable->setItemID(old_item_id)), so no AgentIsNowWearing follows. The COF churns — the +/// edit panel replaces the link — but a COF version bump is not itself a trigger; only an appearance save is. +/// And the viewer's UpdateAvatarAppearance POST, which LLUpdateAppearanceOnDestroy schedules +/// (llappearancemgr.cpp:534-552:2571-2575), is deferred while any upload is pending +/// (:3849) and can arrive long after, carrying a cof_version the COF has moved past. Observed +/// 2026-09-05: four edits between 20:30:34 and 20:52:48, no bake after any of them, and a single cap POST at +/// 20:57:40 refused as stale — "client cof_version 578, server 579". +/// +/// The AIS UpdateItem that carries the new asset is the one moment the region reliably learns of +/// the change, so that is where the save is queued. +/// +[TestFixture] +public class WornWearableRebakeTests +{ + private const string Cap = "/CAP/0a1b2c3d-0000-4000-8000-000000000000"; + private static readonly UUID Agent = new("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + private static readonly UUID Root = new("00000000-0000-4000-8000-000000000001"); + private static readonly UUID Clothing = new("11111111-1111-4111-8111-111111111111"); + private static readonly UUID Shirt = new("22222222-2222-4222-8222-222222222222"); + + private sealed class Req : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest + { + public Req(string verb, string url, OSDMap body = null) + { + HttpMethod = verb; + Url = new Uri("http://sim.test" + url); + RawUrl = url; + InputStream = body is null ? new MemoryStream() : new MemoryStream(OSDParser.SerializeLLSDXmlBytes(body)); + } + public string HttpMethod { get; } + public Uri Url { get; } + public string RawUrl { get; } + public string UriPath => Url.AbsolutePath; + public Stream InputStream { get; set; } + public System.Collections.Specialized.NameValueCollection Headers { get; } = new(); + public bool HasEntityBody => InputStream.Length > 0; + public long ContentLength => InputStream.Length; + public long ContentLength64 => InputStream.Length; + public string ContentType => "application/llsd+xml"; + public string[] AcceptTypes => Array.Empty(); + public System.Text.Encoding ContentEncoding => System.Text.Encoding.UTF8; + public bool IsSecured => false; + public bool KeepAlive => false; + public System.Collections.Specialized.NameValueCollection QueryString => throw new NotImplementedException(); + public System.Collections.Hashtable Query => throw new NotImplementedException(); + public HashSet QueryFlags => throw new NotImplementedException(); + public Dictionary QueryAsDictionary => throw new NotImplementedException(); + public IPEndPoint RemoteIPEndPoint => new(IPAddress.Loopback, 1); + public IPEndPoint LocalIPEndPoint => new(IPAddress.Loopback, 2); + public string UserAgent => "test"; + public double ArrivalTS => 0; + } + + private static FakeAisBackend Inventory() + { + var b = new FakeAisBackend(Agent); + b.AddFolder(Root, UUID.Zero, "My Inventory", 3, (short)FolderType.Root); + b.AddFolder(Clothing, Root, "Clothing", 7, (short)FolderType.Clothing); + b.AddItem(Shirt, Clothing, "Blue Shirt"); + return b; + } + + private static int Send(FakeAisBackend backend, string path, OSDMap body) + { + var handler = new AisHandler(Cap, Agent, backend); + var response = new TestOSHttpResponse(); + handler.Handle(new Req("PATCH", Cap + path, body), response); + return response.StatusCode; + } + + // ------------------------------------------------------------------ the handler reports the change + + [Test] + public void a_patch_that_changes_an_items_asset_reports_it() + { + var b = Inventory(); + var uploaded = UUID.Random(); + + Assert.That(Send(b, $"/item/{Shirt}", new OSDMap { ["asset_id"] = uploaded }), Is.EqualTo(200)); + + Assert.That(b.AssetChanges, Has.Count.EqualTo(1), + "the AIS UpdateItem is the only reliable moment the region learns a worn wearable was edited"); + Assert.That(b.AssetChanges[0].Item, Is.EqualTo(Shirt)); + Assert.That(b.AssetChanges[0].Asset, Is.EqualTo(uploaded)); + } + + [Test] + public void a_transaction_resolved_asset_is_reported_too() + { + // the path a real wearable save takes: hash_id, not asset_id (llviewerinventory.cpp:445-452) + var b = Inventory(); + var transaction = UUID.Random(); + var uploaded = UUID.Random(); + b.Transactions[transaction] = uploaded; + + Assert.That(Send(b, $"/item/{Shirt}", new OSDMap { ["hash_id"] = transaction }), Is.EqualTo(200)); + + Assert.That(b.AssetChanges, Has.Count.EqualTo(1)); + Assert.That(b.AssetChanges[0].Asset, Is.EqualTo(uploaded)); + } + + [Test] + public void a_patch_that_does_not_change_the_asset_reports_nothing() + { + var b = Inventory(); + var unchanged = b.Items[Shirt].AssetID; + + Send(b, $"/item/{Shirt}", new OSDMap { ["name"] = "Red Shirt", ["asset_id"] = unchanged }); + + Assert.That(b.AssetChanges, Is.Empty, "a rename must not cost an appearance save"); + } + + [Test] + public void an_unresolvable_transaction_reports_nothing() + { + var b = Inventory(); + Send(b, $"/item/{Shirt}", new OSDMap { ["hash_id"] = UUID.Random() }); + Assert.That(b.AssetChanges, Is.Empty, "no asset landed, so there is nothing to rebake from"); + } + + // ------------------------------------------------------------------ the rule the module applies + + private static AvatarAppearance Wearing(WearableType slot, UUID itemId, UUID assetId) + { + var a = new AvatarAppearance(); + var worn = a.Wearables; + worn[(int)slot] = new AvatarWearable(itemId, assetId); + a.Wearables = worn; + return a; + } + + [Test] + public void editing_a_worn_wearable_points_it_at_the_new_asset() + { + var oldAsset = UUID.Random(); + var newAsset = UUID.Random(); + var appearance = Wearing(WearableType.Shirt, Shirt, oldAsset); + + Assert.That(AisWornAssets.ApplyTo(appearance, Shirt, newAsset), Is.True); + Assert.That(appearance.Wearables[(int)WearableType.Shirt].GetAsset(Shirt), Is.EqualTo(newAsset)); + Assert.That(appearance.Wearables[(int)WearableType.Shirt].Count, Is.EqualTo(1), + "the slot is updated in place, not appended to"); + } + + [Test] + public void editing_an_item_that_is_not_worn_changes_nothing() + { + // the case that would otherwise make every drawer edit cost an appearance save + var appearance = Wearing(WearableType.Shirt, Shirt, UUID.Random()); + Assert.That(AisWornAssets.ApplyTo(appearance, UUID.Random(), UUID.Random()), Is.False); + } + + [Test] + public void a_worn_item_already_carrying_the_asset_changes_nothing() + { + var asset = UUID.Random(); + var appearance = Wearing(WearableType.Shirt, Shirt, asset); + Assert.That(AisWornAssets.ApplyTo(appearance, Shirt, asset), Is.False, "a replayed PATCH must be free"); + } + + [Test] + public void a_zero_item_or_asset_changes_nothing() + { + var appearance = Wearing(WearableType.Shirt, Shirt, UUID.Random()); + Assert.That(AisWornAssets.ApplyTo(appearance, UUID.Zero, UUID.Random()), Is.False); + Assert.That(AisWornAssets.ApplyTo(appearance, Shirt, UUID.Zero), Is.False); + Assert.That(AisWornAssets.ApplyTo(null, Shirt, UUID.Random()), Is.False); + } + + [Test] + public void the_right_slot_is_found_whichever_one_it_is() + { + foreach (var slot in new[] { WearableType.Skin, WearableType.Gloves, WearableType.Socks, WearableType.Skirt }) + { + var newAsset = UUID.Random(); + var appearance = Wearing(slot, Shirt, UUID.Random()); + Assert.That(AisWornAssets.ApplyTo(appearance, Shirt, newAsset), Is.True, slot.ToString()); + Assert.That(appearance.Wearables[(int)slot].GetAsset(Shirt), Is.EqualTo(newAsset), slot.ToString()); + } + } +} diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests.csproj b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests.csproj new file mode 100644 index 00000000000..dff1cedabd8 --- /dev/null +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests.csproj @@ -0,0 +1,34 @@ + + + + + + true + false + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/OpenSim.Region.CoreModules.Tests/Asset/LibraryTextureInWearableTests.cs b/Tests/OpenSim.Region.CoreModules.Tests/Asset/LibraryTextureInWearableTests.cs new file mode 100644 index 00000000000..6c4a39713c5 --- /dev/null +++ b/Tests/OpenSim.Region.CoreModules.Tests/Asset/LibraryTextureInWearableTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using Xunit; +using OpenMetaverse; +using PermissionMask = OpenSim.Framework.PermissionMask; +using OpenSim.Region.CoreModules.Agent.AssetTransaction; + +namespace OpenSim.Region.CoreModules.Tests.Asset; + +/// +/// A19. A wearable may reference a texture from the grid's LIBRARY, which is what SL allows and what a resident +/// picking a library texture in the appearance editor produces. +/// +/// +/// Before this, AssetXferUploader.ValidateAssets asked GetAssetPermissions for the texture under +/// the resident's id only (AssetXferUploader.cs:566). A library texture is not in the resident's +/// inventory, so that returns nothing, the full-rights test failed, and the whole save was refused - observed on +/// Ebony at 2026-09-06 09:52:55 with the library texture +/// 00000000-0000-2222-3333-100000001002: "REJECTED update with texture ... because they do not own the +/// texture". The built-in system ids in defaultIDs (AssetXferUploader.cs:40-54) were already +/// exempt; a library asset was not. +/// +/// +public class LibraryTextureInWearableTests +{ + private const uint FullPerms = (uint)(PermissionMask.Modify | PermissionMask.Transfer | PermissionMask.Copy); + + private static readonly UUID LibraryOwner = new("11111111-1111-0000-0000-000100bba000"); + + + /// The library texture from the 09:52:55 rejection. + private static readonly UUID LibraryTexture = new("00000000-0000-2222-3333-100000001002"); + + /// Another resident's texture: held by somebody, but not by the library and not by us. + private static readonly UUID StrangersTexture = new("d4a1e4c2-0000-4000-8000-0000000000ff"); + + /// A stand-in for IInventoryService.GetAssetPermissions over a fixed (owner, asset) table. + private static Func Inventory(params (UUID Owner, UUID Asset, uint Perms)[] rows) + { + var table = new Dictionary<(UUID, UUID), uint>(); + foreach (var r in rows) table[(r.Owner, r.Asset)] = r.Perms; + return (owner, asset) => table.TryGetValue((owner, asset), out var p) ? (int)p : 0; + } + + [Fact] + public void a_texture_the_library_holds_with_full_rights_is_accepted() + { + var inv = Inventory((LibraryOwner, LibraryTexture, FullPerms)); + + Assert.True(AssetXferUploader.IsLibraryTexture(LibraryTexture, LibraryOwner, FullPerms, inv), + "a library texture must be usable in a wearable; refusing it loses the whole save"); + } + + [Fact] + public void another_residents_private_texture_is_still_refused() + { + // The stranger holds it with full rights; the library does not hold it at all. Nothing about the library + // rule may turn that into an acceptance - that would be the permission check gone. + var inv = Inventory( + (StrangersTexture, StrangersTexture, FullPerms), // some other owner entirely + (LibraryOwner, LibraryTexture, FullPerms)); + + Assert.False(AssetXferUploader.IsLibraryTexture(StrangersTexture, LibraryOwner, FullPerms, inv), + "a texture the library does not hold is not made acceptable by this rule"); + } + + [Fact] + public void a_library_texture_without_full_rights_is_refused() + { + // The library is asked for exactly the rights the resident would have needed, not waved through by owner. + var inv = Inventory((LibraryOwner, LibraryTexture, (uint)PermissionMask.Copy)); + + Assert.False(AssetXferUploader.IsLibraryTexture(LibraryTexture, LibraryOwner, FullPerms, inv)); + } + + [Fact] + public void a_region_with_no_library_changes_nothing() + { + // LibraryRootFolder.Owner is UUID.Zero when the region has no library service; the previous behaviour has + // to survive that untouched rather than accepting everything or throwing. + var inv = Inventory((LibraryOwner, LibraryTexture, FullPerms)); + + Assert.False(AssetXferUploader.IsLibraryTexture(LibraryTexture, UUID.Zero, FullPerms, inv)); + Assert.False(AssetXferUploader.IsLibraryTexture(LibraryTexture, LibraryOwner, FullPerms, null)); + } +} diff --git a/Tests/OpenSim.Tests.Common/Mock/TestOSHttpResponse.cs b/Tests/OpenSim.Tests.Common/Mock/TestOSHttpResponse.cs index 60ce52b9634..df08b3542ab 100644 --- a/Tests/OpenSim.Tests.Common/Mock/TestOSHttpResponse.cs +++ b/Tests/OpenSim.Tests.Common/Mock/TestOSHttpResponse.cs @@ -129,7 +129,14 @@ public class TestOSHttpResponse : IOSHttpResponse /// name /// string containing the header field /// value - public void AddHeader(string key, string value) { throw new NotImplementedException(); } + /// + /// The headers this response was given. Implemented rather than throwing (AIS-SEC-3): a handler that sets a + /// header on a real response must not fault inside a test, and a test that cares about the header - the + /// Retry-After on AIS's 503, for one - has no other way to see it. + /// + public readonly Dictionary AddedHeaders = new(); + + public void AddHeader(string key, string value) { AddedHeaders[key] = value; } public void Send() { } } \ No newline at end of file From 05a2ba6387251eef74403d209b6b8dfdea115aa8 Mon Sep 17 00:00:00 2001 From: JohnLegionH Date: Sun, 20 Sep 2026 12:46:43 -0500 Subject: [PATCH 3/8] =?UTF-8?q?feat(ssb):=20server-side=20baking=20?= =?UTF-8?q?=E2=80=94=20region=20module,=20Robust=20appearance=20service,?= =?UTF-8?q?=20and=20wire=20protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The simulator composes an avatar's bake itself instead of relaying the viewer's. Region side (OpenSim.Region.OptionalModules/Avatar/ServerSideBaking): ServerSideBakingRegion sets RegionProtocols bit 0 and registers the UpdateAvatarAppearance cap; CofHandshake implements the viewer's POST cof_version / success|error|expected contract; BakeOrchestrator drives OpenSimNGC.Appearance.Baking and persists results; BakeIndex keeps the Bake: index on the avatar record so a bake survives an appearance save. ScenePresence sends the AppearanceData block with appearance_version 1, and an NPC's appearance no longer claims a server bake it does not have. Robust side: AppearanceService + AppearanceServerConnector serve GET texture///, and LLLoginService advertises agent_appearance_service in the login response (empty by default, which omits the key and leaves the viewer compositing locally). Also carried here, because the bake reads what the appearance path stores: the appearance-integrity fixes (AgentIsNowWearing no longer wipes unlisted wearable slots; a refused wearable save is a 403; library textures are legal), the [AIS]/[Appearance] global config switch (S12), the env/LightShare fixes, and the NUnit-lifecycle test fixtures orphaned by the xunit migration. Tranquillity.sln gains the five Track L projects via `dotnet sln add`. Folded from feature/ais-v3, 28 commits over these paths: c1fc7fff3e test(fixtures): NUnit lifecycle hooks orphaned by the xunit migration — CoreModules.Tests 35→5 failing d43f8cb362 test(caps): LindenCaps.Tests restored to the solution 11a2456833 fix(appearance): AgentIsNowWearing no longer wipes unlisted wearable slots 564ca614d4 feat(ssb): OpenSimNGC.Appearance.Baking skeleton with embedded avatar_lad.xml 8aa8104875 feat(ais): AISv3Module skeleton, router, envelope fixtures; remove webapi template 99118ea1ab feat(ssb): ServerSideBakingModule — orchestrator + console serverbake 7c972b7dac fix(ssb): orchestrator keeps worn-but-assetless wearable slots 9b428e0994 fix(ssb): never store or apply a bake for a channel with nothing drawn 4d725a7b2d feat(ais): advertise InventoryAPIv3 and LibraryAPIv3 behind the region flag dc4e417bb3 fix(appearance): flush a pending appearance save when the presence closes 86880b7316 feat(ssb): persist bakes and skip recompute on unchanged inputs 162bfadcc3 perf(ssb): instrument the bake phases and answer Q-10 1e78b9a706 fix(ssb): an appearance save no longer wipes the bake index 7554bf9b51 feat(ssb): UpdateAvatarAppearance cap, AppearanceData, RegionProtocols bit 0 59b12538b9 feat(ssb): Robust appearance service for baked textures 51194ac754 feat(ssb): advertise agent_appearance_service in the login response eda2681fef fix(ssb): send appearance_version param 1 with the AppearanceData block d7ac58d187 feat(ssb): rebake on wearable change 9417a09402 test(ssb): BoM aux channels end to end b13f15add3 docs(ssb): withdraw the unsourced Q-6 "310 ms" measurement 483c2d7a13 fix(appearance): keep unresolvable wearables, never save from a child, refuse a bake that lost a body part 73d25e3175 feat(ssb): keep every COF link of a type, in the viewer's "@n" order b0b3c6b671 fix(appearance): persist wearable types 15 and 16, and record S11 bb4bcd03dc feat(config): S12 — one switch for the grid, and say which config decided 162e117383 fix(config): put S12's OpenSim.ini.example block in the right place dc0b7c03f1 fix(ssb): SSB-NPC-1 - an NPC's appearance must not claim a server bake it does not have c65cf09037 test(env): a non-array WindLight body must not reach DayCycle f6981e12f9 fix(env): guard on the cast result, not the parameter (ENV-1) --- Source/OpenSim.Framework/AvatarAppearance.cs | 46 ++ Source/OpenSim.Framework/IClientAPI.cs | 11 + Source/OpenSim.Framework/ViewerEnvironment.cs | 22 +- .../LLClientView.cs | 31 +- .../AvatarFactory/AvatarFactoryModule.cs | 228 ++++++- .../World/LightShare/EnvironmentModule.cs | 21 +- .../Interfaces/IServerSideBakingRegion.cs | 39 ++ .../Scenes/ScenePresence.cs | 29 +- .../Server/IRCClientView.cs | 4 + .../Avatar/ServerSideBaking/BakeIndex.cs | 174 ++++++ .../ServerSideBaking/BakeOrchestrator.cs | 469 ++++++++++++++ .../Avatar/ServerSideBaking/CofHandshake.cs | 131 ++++ .../Avatar/ServerSideBaking/CofWearables.cs | 164 +++++ .../ServerSideBaking/IServerSideBaker.cs | 71 +++ .../ServerSideBakingModule.cs | 576 ++++++++++++++++++ .../ServerSideBakingRegion.cs | 190 ++++++ .../OpenSim.Region.OptionalModules.csproj | 1 + .../PluginRegistration.cs | 1 + .../World/NPC/NPCAvatar.cs | 4 + .../AppData/Robust.ini.example | 24 + .../Appearance/AppearanceServerConnector.cs | 128 ++++ .../AppData/OpenSim.ini.example | 22 + .../AppData/OpenSimDefaults.ini | 64 ++ .../StandaloneCommon.ini.example | 7 + .../AppearanceService.cs | 133 ++++ .../AvatarService.cs | 54 ++ .../AvatarServiceBase.cs | 11 + .../AppearanceChannels.cs | 89 +++ .../IAppearanceService.cs | 34 ++ .../IAvatarService.cs | 96 ++- .../LLLoginResponse.cs | 25 + .../LLLoginService.cs | 18 + .../EventQueue/Tests/EventQueueTests.cs | 24 +- ...Region.ClientStack.LindenCaps.Tests.csproj | 31 +- .../AssemblyInfo.cs | 8 + .../AvatarFactoryAppearanceIntegrityTests.cs | 202 ++++++ .../Tests/AvatarFactoryNowWearingTests.cs | 163 +++++ .../Tests/AvatarFactorySaveFlushTests.cs | 249 ++++++++ .../Tests/InventoryArchiveTestCase.cs | 11 + .../Tests/ViewerEnvironmentWLOSDTests.cs | 114 ++++ .../World/Serialiser/Tests/SerialiserTests.cs | 8 + .../AppearanceServiceTests.cs | 205 +++++++ .../AppearanceVersionParamTests.cs | 231 +++++++ .../AvatarRecordWearableRangeTests.cs | 98 +++ .../BakeCostTests.cs | 131 ++++ .../BakeIndexSurvivesAppearanceSaveTests.cs | 244 ++++++++ .../BakeOrchestratorTests.cs | 279 +++++++++ .../BakeReuseTests.cs | 399 ++++++++++++ .../BodyPartLossTests.cs | 155 +++++ .../BomAuxChannelTests.cs | 272 +++++++++ .../ChangeTriggerTests.cs | 215 +++++++ .../CofVersionIdentityTests.cs | 144 +++++ .../CofWearableOrderTests.cs | 246 ++++++++ .../FakeAssetService.cs | 38 ++ .../FakeAvatarService.cs | 57 ++ .../LoginResponseAdvertisementTests.cs | 145 +++++ .../OneSwitchConfigTests.cs | 178 ++++++ ...ionalModules.ServerSideBaking.Tests.csproj | 40 ++ .../WireTests.cs | 381 ++++++++++++ Tests/OpenSim.Tests.Common/Mock/TestClient.cs | 13 + Tests/OpenSim.Tests.Common/OpenSimTestCase.cs | 25 +- Tranquillity.sln | 75 +++ 62 files changed, 7230 insertions(+), 68 deletions(-) create mode 100644 Source/OpenSim.Region.Framework/Interfaces/IServerSideBakingRegion.cs create mode 100644 Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/BakeIndex.cs create mode 100644 Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/BakeOrchestrator.cs create mode 100644 Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/CofHandshake.cs create mode 100644 Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/CofWearables.cs create mode 100644 Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/IServerSideBaker.cs create mode 100644 Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/ServerSideBakingModule.cs create mode 100644 Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/ServerSideBakingRegion.cs create mode 100644 Source/OpenSim.Server.Handlers/Appearance/AppearanceServerConnector.cs create mode 100644 Source/OpenSim.Services.AvatarService/AppearanceService.cs create mode 100644 Source/OpenSim.Services.Interfaces/AppearanceChannels.cs create mode 100644 Source/OpenSim.Services.Interfaces/IAppearanceService.cs create mode 100644 Tests/OpenSim.Region.CoreModules.Tests/AssemblyInfo.cs create mode 100644 Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactoryAppearanceIntegrityTests.cs create mode 100644 Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactoryNowWearingTests.cs create mode 100644 Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactorySaveFlushTests.cs create mode 100644 Tests/OpenSim.Region.CoreModules.Tests/World/LightShare/Tests/ViewerEnvironmentWLOSDTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AppearanceServiceTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AppearanceVersionParamTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AvatarRecordWearableRangeTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeCostTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeIndexSurvivesAppearanceSaveTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeOrchestratorTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeReuseTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BodyPartLossTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BomAuxChannelTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/ChangeTriggerTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/CofVersionIdentityTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/CofWearableOrderTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/FakeAssetService.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/FakeAvatarService.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/LoginResponseAdvertisementTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/OneSwitchConfigTests.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests.csproj create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/WireTests.cs diff --git a/Source/OpenSim.Framework/AvatarAppearance.cs b/Source/OpenSim.Framework/AvatarAppearance.cs index 2e79c230ff4..d5d81fc38ca 100644 --- a/Source/OpenSim.Framework/AvatarAppearance.cs +++ b/Source/OpenSim.Framework/AvatarAppearance.cs @@ -86,6 +86,52 @@ public byte[] VisualParams set { m_visualparams = value; } } + /// + /// Index of the AppearanceMessage_Version visual parameter (avatar_lad.xml id 11000) in the + /// wire array — the same slot names. + /// + public const int APPEARANCE_VERSION_PARAM_INDEX = (int)VPElement._APPEARANCEMESSAGE_VERSION; + + /// + /// A copy of with the appearance-version parameter set to + /// , for an avatar this simulator baked. + /// + /// + /// The viewer reads the appearance version from two places and prefers the parameter: + /// resolve_appearance_version takes mParamAppearanceVersion when it is set and only falls back + /// to the AppearanceData field otherwise (llvoavatar.cpp:9663-9690). Worse, when both are + /// present and they disagree it logs "inconsistent appearance_version settings" and returns false, and + /// the caller then discards the entire appearance message — "bad appearance version info, discarding" + /// (:9720-9723). The avatar never gets its TextureEntry, so it never fetches a bake and never leaves + /// the cloud state. + /// + /// + /// + /// The parameter is transmitted as a byte mapped through the parameter's own range, and id 11000 is declared + /// value_min="0" value_max="255" (avatar_lad.xml), so U8_to_F32 is the identity there + /// (llvoavatar.cpp:9628-9630, :9650-9658): the byte must literally equal the version in the + /// AppearanceData block, which for a server bake is 1. + /// + /// + /// + /// Never mutates its argument. A flag-off region must keep sending the avatar's own stored parameters + /// untouched, and this value is not part of the avatar's appearance — it is a property of the message. + /// An array too short to hold the slot is returned as-is: the viewer will not find the parameter among the + /// transmitted blocks either, and an absent parameter with a present field resolves to the field. + /// + /// + public static byte[] WithAppearanceVersion(byte[] visualParams, byte version) + { + if (visualParams is null || visualParams.Length <= APPEARANCE_VERSION_PARAM_INDEX) + return visualParams; + if (visualParams[APPEARANCE_VERSION_PARAM_INDEX] == version) + return visualParams; + + byte[] copy = (byte[])visualParams.Clone(); + copy[APPEARANCE_VERSION_PARAM_INDEX] = version; + return copy; + } + public Vector3 AvatarSize { get { return m_avatarSize; } diff --git a/Source/OpenSim.Framework/IClientAPI.cs b/Source/OpenSim.Framework/IClientAPI.cs index 1a770cae940..2ca9622811f 100644 --- a/Source/OpenSim.Framework/IClientAPI.cs +++ b/Source/OpenSim.Framework/IClientAPI.cs @@ -1068,6 +1068,17 @@ public interface IClientAPI /// void SendAppearance(UUID agentID, byte[] visualParams, byte[] textureEntry, float hoverheight); + /// + /// As , plus the LL viewer's AppearanceData block + /// for an avatar this simulator baked (viewer contract V4/V5). + /// + /// + /// The Current Outfit folder version the sim baked at, or negative for "this sim did not bake this + /// avatar", which sends the count-0 form that every release before server-side baking sent. The four-argument + /// overload is exactly this one with a negative version. + /// + void SendAppearance(UUID agentID, byte[] visualParams, byte[] textureEntry, float hoverheight, int cofVersion); + void SendCachedTextureResponse(ISceneEntity avatar, int serial, List cachedTextures); /// diff --git a/Source/OpenSim.Framework/ViewerEnvironment.cs b/Source/OpenSim.Framework/ViewerEnvironment.cs index e3f44ad3000..0f6fe7c950a 100644 --- a/Source/OpenSim.Framework/ViewerEnvironment.cs +++ b/Source/OpenSim.Framework/ViewerEnvironment.cs @@ -88,10 +88,30 @@ public class ViewerEnvironment public int version = 0; + /// + /// Load a legacy WindLight day cycle from an LLSD array. A body of any other shape is left alone: + /// this returns void and its existing contract for "cannot use this" is to leave + /// as it was, which is what the two already-guarded callers in EnvironmentModule assume. + /// + /// ENV-1: the guard tests array, and that matters more than it looks. It used to test + /// osd - the parameter rather than the cast result - and a non-array is still + /// non-null, so null reached DayCycle.FromWLOSD(OSDArray), which dereferences + /// array.Count on its first statement and threw. That throw was load-bearing. The legacy + /// WindLight setter (EnvironmentModule.SetEnvironmentSettings) parsed with the auto-detect entry, + /// held a bare OSD with no type check, and called StoreOnRegion - a write - immediately after + /// this call. The throw landed before that write, so nothing was corrupted; making + /// DayCycle.FromWLOSD null-tolerant, an obvious-looking hardening, would have removed the only thing + /// stopping a truncated request from storing a fresh default environment over the region's real one. + /// + /// Correcting this guard alone would have armed that, by turning the throw into a silent no-op - + /// so ENV-1 also added the type check at the setter, where the body is now refused at the boundary instead + /// of being stopped by a downstream accident. Found read-only in + /// Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md §5. + /// public void FromWLOSD(OSD osd) { OSDArray array = osd as OSDArray; - if(osd != null) + if(array != null) // ENV-1: the cast result, not the parameter { Cycle = new DayCycle(); Cycle.FromWLOSD(array); diff --git a/Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs b/Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs index 1abfaedb678..97201abe324 100644 --- a/Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs +++ b/Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs @@ -992,7 +992,13 @@ public void SendRegionHandshake() //RegionProtocols // bit 0 signals server side texture baking // bit 63 signals more than 6 baked textures support" - zc.AddUInt64(1UL << 63); + // Viewer contract V1 (llviewerregion.cpp:3097): the viewer chooses the server-bake path for this region + // iff bit 0 is set. It is set only where [Appearance] ServerSideBaking resolved true, so a region without + // the module, or with the flag off, sends the value it always sent. + ulong regionProtocols = 1UL << 63; + if (m_scene.RequestModuleInterface() is { ServerSideBakingEnabled: true }) + regionProtocols |= 1UL; + zc.AddUInt64(regionProtocols); buf.DataLength = zc.Finish(); m_udpServer.SendUDPPacket(m_udpClient, buf, ThrottleOutPacketType.Unknown); @@ -4512,6 +4518,15 @@ public void SendWearables(AvatarWearable[] wearables, int serial) }; public void SendAppearance(UUID targetID, byte[] visualParams, byte[] textureEntry, float hover) + => SendAppearance(targetID, visualParams, textureEntry, hover, -1); + + /// + /// negative reproduces the pre-SSB packet byte for byte: AppearanceData count 0. + /// Non-negative emits one AppearanceData block {AppearanceVersion=1, CofVersion, Flags=0} — viewer contract + /// V4 (llvoavatar.cpp:9779-9800: an appearance for self with no block is dropped as stale) and V5 + /// (:9727-9737: the version is 1 when the server bakes). + /// + public void SendAppearance(UUID targetID, byte[] visualParams, byte[] textureEntry, float hover, int cofVersion) { // doing post zero encode, because odds of beeing bad are not that low UDPPacketBuffer buf = OpenSimUDPBase.GetNewUDPBuffer(m_udpClient.RemoteEndPoint); @@ -4543,8 +4558,18 @@ public void SendAppearance(UUID targetID, byte[] visualParams, byte[] textureEnt if(len > 0) Buffer.BlockCopy(visualParams, 0, data, pos, len); pos += len; - // no AppearanceData - data[pos++] = 0; + // AppearanceData: one block for a sim-baked avatar, the historical count of 0 otherwise + if (cofVersion < 0) + { + data[pos++] = 0; + } + else + { + data[pos++] = 1; + data[pos++] = 1; // AppearanceVersion (U8), V5 + Utils.IntToBytesSafepos(cofVersion, data, pos); pos += 4; // CofVersion (S32) + Utils.UIntToBytesSafepos(0, data, pos); pos += 4; // Flags (U32), unused + } // AppearanceHover vector 3 data[pos++] = 1; Utils.FloatToBytesSafepos(0, data, pos); pos += 4; diff --git a/Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs b/Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs index 5fe777a0c4a..93918faf7a1 100755 --- a/Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs +++ b/Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs @@ -82,6 +82,7 @@ public void AddRegion(Scene scene) scene.RegisterModuleInterface(this); scene.EventManager.OnNewClient += SubscribeToClientEvents; + scene.EventManager.OnRemovePresence += FlushAppearanceSaveOnClose; } public void RemoveRegion(Scene scene) @@ -90,11 +91,63 @@ public void RemoveRegion(Scene scene) { scene.UnregisterModuleInterface(this); scene.EventManager.OnNewClient -= SubscribeToClientEvents; + scene.EventManager.OnRemovePresence -= FlushAppearanceSaveOnClose; } m_scene = null; } + /// + /// Write a pending appearance change before the presence goes away. + /// + /// The defect this closes. defers the write by + /// m_savetime seconds, and resolves the ScenePresence only when the + /// timer finally fires. If the agent left in between, the presence was gone and the write was skipped + /// **silently**: the change stayed in memory and died with the presence. A detach followed by a logout inside + /// the save window therefore left the stored appearance still wearing the garment, and the viewer put the item + /// back on the next login. Wear is affected identically. This predates AIS and hits the legacy path just as + /// hard; the evidence is in Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md of the AIS branch. + /// + /// Why OnRemovePresence is the right hook. Scene.RemoveClient raises it at + /// Scene.cs:3866, while the presence is still in the scene graph: it is not removed until + /// m_sceneGraph.RemoveScenePresence in the finally block at :3898, and not disposed until + /// :3905. So GetScenePresence still resolves here — which is precisely what + /// needs and precisely what it lacks when the timer fires later. Nothing between + /// the event and the removal changes appearance either: DeRezAttachments (:3871) saves the + /// attachment *objects* and calls ScenePresence.ClearAttachments, which only empties the group list + /// (ScenePresence.cs:5488-5492) and never touches Appearance. + /// + /// No extra write per session. The queue is consulted, never forced: with nothing pending this + /// writes nothing. A session that changed no appearance costs one dictionary lookup. + /// + /// Child agents are skipped. A child agent's Appearance is a copy of state owned by + /// whichever region holds the root, so writing it from here could publish a stale outfit over a newer one. On + /// a teleport the source's root is converted by ScenePresence.MakeChildAgent, not by + /// RemoveClient, and the appearance travels to the destination in the agent data — so skipping loses + /// nothing and not skipping risks a stale overwrite. + /// + private void FlushAppearanceSaveOnClose(UUID agentId) + { + // Only a genuinely pending change earns a write; an unchanged session must stay free. + if (!m_savequeue.TryRemove(agentId, out _)) + return; + + ScenePresence sp = m_scene?.GetScenePresence(agentId); + if (sp is null) + { + m_log.LogWarning( + "[AVFACTORY]: pending appearance change for {AgentId} could not be flushed on close - no presence. The change is lost.", + agentId); + return; + } + + if (sp.IsChildAgent) + return; + + m_log.LogDebug("[AVFACTORY]: flushing pending appearance save for {AgentId} on close", agentId); + SaveAppearance(new List { agentId }); + } + public void RegionLoaded(Scene scene) { m_updateTimer.Enabled = false; @@ -333,6 +386,17 @@ public void QueueAppearanceSend(UUID agentid) public void QueueAppearanceSave(UUID agentid) { + // S8: a child presence's appearance is a copy of the root's, carried for drawing. It is not authoritative + // and must never reach the avatar service - see the guard in SaveAppearance for what happened when it did. + var queueing = m_scene?.GetScenePresence(agentid); + if (queueing is not null && queueing.IsChildAgent) + { + m_log.LogDebug( + "[AVFACTORY]: not queueing an appearance save for {AgentId} in {Region}: child presence, the root region owns this appearance", + agentid, m_scene.Name); + return; + } + // m_log.LogDebug("[AVFACTORY]: Queueing appearance save for {0}", agentid); // 10000 ticks per millisecond, 1000 milliseconds per second @@ -816,7 +880,29 @@ private void SaveAppearance(List ids) { ScenePresence sp = m_scene.GetScenePresence(id); if(sp == null) + { + // The presence went away between queueing and firing, so the change can no longer be read and is + // lost. FlushAppearanceSaveOnClose exists to make this unreachable on a normal close; if it is + // ever reached again, something closes a presence by a path that does not raise OnRemovePresence, + // and that must not be silent a second time. + m_log.LogWarning( + "[AVFACTORY]: dropping queued appearance save for {AgentId}: no presence when the save fired. The change is lost.", + id); + continue; + } + if (sp.IsChildAgent) + { + // S8: the authoritative write barrier. Everything below resolves items against THIS region's + // inventory view and then writes the result to the avatar service; on a child presence that is a + // write about an avatar another region owns, made from a presence that is only a copy. On + // 2026-09-05 a save that ran on a non-root presence resolved four body-part items it could not + // see, and the stored record lost skin, hair, eyes and shirt (slots 1-4). + m_log.LogDebug( + "[AVFACTORY]: skipping appearance save for {AgentId} in {Region}: child presence", + id, m_scene.Name); continue; + } + // This could take awhile since it needs to pull inventory // We need to do it at the point of save so that there is a sufficient delay for any upload of new body part/shape // assets and item asset id changes to complete. @@ -826,7 +912,13 @@ private void SaveAppearance(List ids) SetAppearanceAssets(id, sp.Appearance); m_scene.AvatarService.SetAppearance(id, sp.Appearance); - //m_scene.EventManager.TriggerAvatarAppearanceChanged(sp); + + // The appearance is now applied AND persisted: SetAppearanceAssets has resolved every worn item to + // its asset id, and the avatar service has the result. This is the only point in the region where + // both are true, which is why server-side baking triggers off it rather than off the arrival of a + // change (Design Brief §4.6, Ledger Q-16). Uncommented in S5; the event has existed unused since + // before this fork. + m_scene.EventManager.TriggerAvatarAppearanceChanged(sp); } } @@ -870,11 +962,26 @@ private void SetAppearanceAssets(UUID userID, AvatarAppearance appearance) } else { + // S8: KEEP the slot. The message this replaced said "setting to default" and the code + // then did something worse than that - it removed the wearable outright, so the slot went + // empty, and because the very next statement in SaveAppearance persists the whole + // appearance (and AvatarService.SetAvatar deletes every row before rewriting, + // AvatarService.cs:93), the slot vanished from the stored record altogether. An item id + // this region cannot resolve is a statement about the inventory lookup, not about what + // the avatar is wearing: a stale viewer cache, an inventory service that answered late or + // not at all, or an item from another grid will all produce it, and in each case the + // wearable the agent already has is the better answer than none. + // + // This is the same failure S0c fixed for a different input. There the viewer LISTED fewer + // slots than were worn and the unlisted ones were dropped; here the slot IS listed and + // the item behind it cannot be resolved. S0c merged instead of replacing; this keeps + // instead of removing. Both leave the last known good wearable in place. + // + // Inherited from upstream unchanged (OpenSim-NGC develop a68d59f232, + // AvatarFactoryModule.cs:871-878). m_log.LogWarning( - "[AVFACTORY]: Can't find inventory item {0} for {1}, setting to default", - appearance.Wearables[i][j].ItemID, (WearableType)i); - - appearance.Wearables[i].RemoveItem(appearance.Wearables[i][j].ItemID); + "[AVFACTORY]: agent {AgentId} slot {Slot} ({SlotIndex}) names item {ItemId}, which this region cannot resolve; keeping the wearable already in the slot and leaving it out of this save's asset resolution", + userID, (WearableType)i, i, appearance.Wearables[i][j].ItemID); } } } @@ -1201,38 +1308,115 @@ private void Client_OnAvatarNowWearing(IClientAPI client, AvatarWearingArgs e) return; } - // operate on a copy of the appearance so we don't have to lock anything yet - AvatarAppearance avatAppearance = new AvatarAppearance(sp.Appearance, false); + // S0c (Ledger R-4 / Q-3): merge the viewer's list INTO the existing wearables instead of + // starting from an empty set. Historically this built a fresh AvatarAppearance with + // copyWearables=false and filled only the slots the viewer mentioned, so any partial + // AgentIsNowWearing (incomplete inventory fetch, bot, gateway) silently deleted every + // wearable it did not list, and the loss was persisted and self-reinforcing. + // + // Semantics preserved from the old code for slots the viewer DID mention: the slot is + // replaced by exactly the listed items, and an ItemID of UUID.Zero contributes nothing + // (AvatarWearable.Add ignores Zero), so "type X, item Zero" still means "clear slot X". + // Only unlisted slots change behaviour: they now keep their current contents. + AvatarWearable[] merged = MergeNowWearing(sp.Appearance.Wearables, e.NowWearing, out bool changed); - foreach (AvatarWearingArgs.Wearable wear in e.NowWearing) + if (!changed) { - // If the wearable type is larger than the current array, expand it - if (avatAppearance.Wearables.Length <= wear.Type) - { - int currentLength = avatAppearance.Wearables.Length; - AvatarWearable[] wears = avatAppearance.Wearables; - Array.Resize(ref wears, wear.Type + 1); - for (int i = currentLength ; i <= wear.Type ; i++) - wears[i] = new AvatarWearable(); - avatAppearance.Wearables = wears; - } - avatAppearance.Wearables[wear.Type].Add(wear.ItemID, UUID.Zero); + // m_log.LogDebug("[AVFACTORY]: AgentIsNowWearing for {0} matches stored wearables; nothing to persist", client.AgentId); + return; } - avatAppearance.GetAssetsFrom(sp.Appearance); - lock (m_setAppearanceLock) { // Update only those fields that we have changed. This is important because the viewer // often sends AvatarIsWearing and SetAppearance packets at once, and AvatarIsWearing // shouldn't overwrite the changes made in SetAppearance. - sp.Appearance.Wearables = avatAppearance.Wearables; + sp.Appearance.Wearables = merged; // We don't need to send the appearance here since the "iswearing" will trigger a new set // of visual param and baked texture changes. When those complete, the new appearance will be sent QueueAppearanceSave(client.AgentId); } } + /// + /// Apply an AgentIsNowWearing list to an existing wearable set. + /// + /// + /// Pure function; neither argument is mutated. Rules: + /// + /// A wearable type that appears in is replaced by exactly the + /// listed items for that type (in order, capped by ). Asset ids are + /// carried over from for items already known in that slot; new items get + /// and are resolved later on save, as before. + /// A listed item with as its id contributes nothing, so a type listed only + /// with Zero ends up empty. This is the historical meaning of Zero ("not wearing this type"). + /// A wearable type that does not appear in keeps its current items. + /// + /// + /// The agent's current wearables. May be null (treated as empty). + /// The viewer's list. + /// True if the returned set differs from in any item id. + /// A new array; safe to assign to . + public static AvatarWearable[] MergeNowWearing( + AvatarWearable[] existing, IEnumerable nowWearing, out bool changed) + { + existing ??= Array.Empty(); + + int length = existing.Length; + var listedTypes = new HashSet(); + foreach (AvatarWearingArgs.Wearable wear in nowWearing) + { + listedTypes.Add(wear.Type); + if (wear.Type >= length) + length = wear.Type + 1; + } + + AvatarWearable[] merged = new AvatarWearable[length]; + for (int i = 0; i < length; i++) + { + merged[i] = new AvatarWearable(); + if (listedTypes.Contains(i)) + continue; + + // Unlisted slot: keep what the agent already has. + if (i < existing.Length && existing[i] != null) + { + for (int j = 0; j < existing[i].Count; j++) + merged[i].Add(existing[i][j].ItemID, existing[i][j].AssetID); + } + } + + foreach (AvatarWearingArgs.Wearable wear in nowWearing) + { + // Listed slot: exactly the listed items. Add() ignores UUID.Zero, so a Zero entry clears. + UUID assetID = UUID.Zero; + if (wear.Type < existing.Length && existing[wear.Type] != null) + assetID = existing[wear.Type].GetAsset(wear.ItemID); + merged[wear.Type].Add(wear.ItemID, assetID); + } + + changed = !SameItemIds(existing, merged); + return merged; + } + + private static bool SameItemIds(AvatarWearable[] a, AvatarWearable[] b) + { + int length = Math.Max(a.Length, b.Length); + for (int i = 0; i < length; i++) + { + int ca = (i < a.Length && a[i] != null) ? a[i].Count : 0; + int cb = (i < b.Length && b[i] != null) ? b[i].Count : 0; + if (ca != cb) + return false; + for (int j = 0; j < ca; j++) + { + if (a[i][j].ItemID != b[i][j].ItemID) + return false; + } + } + return true; + } + /* /// /// Respond to the cached textures request from the client diff --git a/Source/OpenSim.Region.CoreModules/World/LightShare/EnvironmentModule.cs b/Source/OpenSim.Region.CoreModules/World/LightShare/EnvironmentModule.cs index 85f125b7f00..8922f474521 100644 --- a/Source/OpenSim.Region.CoreModules/World/LightShare/EnvironmentModule.cs +++ b/Source/OpenSim.Region.CoreModules/World/LightShare/EnvironmentModule.cs @@ -753,8 +753,27 @@ private void SetEnvironmentSettings(IOSHttpRequest request, IOSHttpResponse resp } try { - ViewerEnvironment VEnv = new(); OSD env = OSDParser.Deserialize(request.InputStream); + + // ENV-1: refuse a body that is not the LLSD array a WindLight setting is, at the boundary, before + // anything is constructed or stored. OSDParser.Deserialize returns a bare OSD with OSDType.Unknown + // for a truncated or non-LLSD body rather than throwing or returning null (AIS-AUDIT-1 1b), so + // without this check a malformed request reached FromWLOSD and was stopped only because + // DayCycle.FromWLOSD dereferenced a null array and threw. That throw was the sole thing preventing + // StoreOnRegion from writing a fresh DEFAULT environment over the region's real one - a truncated + // request blanking the environment. The two other callers of FromWLOSD (:181 and :637) already + // test `is OSDArray`; this is the same check, in the one place it was missing. + // See Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md section 5. + if (env is not OSDArray) + { + m_log.LogWarning( + "[{0}]: rejected a legacy WindLight setting for region {1} from agentID {2}: the body is {3}, an LLSD array was expected", + Name, m_scene.Name, agentID, env is null ? "null" : env.Type.ToString()); + fail_reason = string.Format("Environment settings for region {0} were not in the expected format, settings not saved.", m_scene.Name); + goto Error; + } + + ViewerEnvironment VEnv = new(); VEnv.FromWLOSD(env); StoreOnRegion(VEnv); diff --git a/Source/OpenSim.Region.Framework/Interfaces/IServerSideBakingRegion.cs b/Source/OpenSim.Region.Framework/Interfaces/IServerSideBakingRegion.cs new file mode 100644 index 00000000000..e24caea2099 --- /dev/null +++ b/Source/OpenSim.Region.Framework/Interfaces/IServerSideBakingRegion.cs @@ -0,0 +1,39 @@ +using OpenMetaverse; + +namespace OpenSim.Region.Framework.Interfaces; + +/// +/// The wire-visible part of server-side baking, registered per scene by the baking module and read by the client +/// stack. It exists so that LLClientView and ScenePresence can gate two pieces of the LL viewer +/// contract on the region's [Appearance] ServerSideBaking flag without either of them referencing the +/// optional module or the bake library: +/// +/// V1RegionHandshake.RegionProtocols bit 0 tells the viewer to expect server bakes +/// (llviewerregion.cpp:3097). +/// V4/V5 — an AvatarAppearance for a sim-baked avatar carries an AppearanceData +/// block; without it the viewer discards its own appearance as stale +/// (llvoavatar.cpp:9779-9800, :9727-9737). +/// +/// +/// +/// A region with no baking module registers nothing, RequestModuleInterface returns null, and every call +/// site keeps its pre-SSB behaviour — which is the ADR-001 rule that Firestorm's client-bake path must be +/// untouched wherever the flag is off. +/// +/// +public interface IServerSideBakingRegion +{ + /// + /// Whether [Appearance] ServerSideBaking resolved true for this region. Gates bit 0 of + /// RegionProtocols and nothing else on its own: an avatar this sim has not baked still gets the + /// count-0 AppearanceData form. + /// + bool ServerSideBakingEnabled { get; } + + /// + /// The Current Outfit folder version of the bake this sim last applied to the agent in this region, or + /// -1 when this sim has not baked that agent — in which case the appearance goes out in exactly the + /// form it always has, with no AppearanceData block. + /// + int BakedCofVersion(UUID agentId); +} diff --git a/Source/OpenSim.Region.Framework/Scenes/ScenePresence.cs b/Source/OpenSim.Region.Framework/Scenes/ScenePresence.cs index 41b9b9f2c33..2aeff197876 100644 --- a/Source/OpenSim.Region.Framework/Scenes/ScenePresence.cs +++ b/Source/OpenSim.Region.Framework/Scenes/ScenePresence.cs @@ -4346,7 +4346,34 @@ public void SendAppearanceToAgent(ScenePresence avatar) public void SendAppearanceToAgentNF(ScenePresence avatar) { - avatar.ControllingClient.SendAppearance(UUID, Appearance.VisualParams, Appearance.Texture.GetBakesBytes(), Appearance.AvatarPreferencesHoverZ); + // -1 unless this simulator baked this avatar in this region, in which case the appearance carries the + // AppearanceData block the LL viewer needs to accept its own appearance (V4/V5). On a region with no + // baking module, or with the flag off, this is -1 for everyone and the packet is unchanged. + int cofVersion = m_scene.RequestModuleInterface()?.BakedCofVersion(UUID) ?? -1; + + // The AppearanceData block is not enough on its own. The viewer prefers the appearance-version *parameter* + // over the block's field, and discards the whole message when the two disagree + // (llvoavatar.cpp:9663-9690, :9720-9723) — which is what kept a bit-0 region's avatars in the cloud + // state: block said 1, parameter said 0. The two are made to agree here, on a copy, for exactly the + // avatars that carry the block. cofVersion < 0 leaves both the parameters and the packet untouched - + // with one exception, below. + byte[] visualParams; + if (cofVersion >= 0) + visualParams = AvatarAppearance.WithAppearanceVersion(Appearance.VisualParams, 1); + else if (IsNPC) + // SSB-NPC-1: an NPC's appearance is a clone of its owner's, parameter 11000 included, and an owner on a + // server-bake region sends that parameter as 1. Nothing ever bakes an NPC, so its message has no + // AppearanceData block - and a parameter of 1 beside no block puts the viewer on the server-bake path + // for this avatar (resolve_appearance_version prefers the parameter), which fetches every baked face + // from the appearance service under the NPC's UUID. No index lives there, every channel is a 404, and + // the NPC never leaves the cloud state. Say 0 - the message every avatar carried before SSB - and the + // viewer fetches the stored bake assets the faces name from the region, as it always did. On a copy: + // the parameter is a property of the message, not of the appearance. + visualParams = AvatarAppearance.WithAppearanceVersion(Appearance.VisualParams, 0); + else + visualParams = Appearance.VisualParams; + + avatar.ControllingClient.SendAppearance(UUID, visualParams, Appearance.Texture.GetBakesBytes(), Appearance.AvatarPreferencesHoverZ, cofVersion); } public void SendAnimPackToAgent(ScenePresence p) diff --git a/Source/OpenSim.Region.OptionalModules/Agent/InternetRelayClientView/Server/IRCClientView.cs b/Source/OpenSim.Region.OptionalModules/Agent/InternetRelayClientView/Server/IRCClientView.cs index 5f08631301e..b062678726b 100644 --- a/Source/OpenSim.Region.OptionalModules/Agent/InternetRelayClientView/Server/IRCClientView.cs +++ b/Source/OpenSim.Region.OptionalModules/Agent/InternetRelayClientView/Server/IRCClientView.cs @@ -961,6 +961,10 @@ public void SendWearables(AvatarWearable[] wearables, int serial) } + public void SendAppearance(UUID agentID, byte[] visualParams, byte[] textureEntry, float hoverheight, int cofVersion) + { + } + public void SendAppearance(UUID agentID, byte[] visualParams, byte[] textureEntry, float hoverheight) { diff --git a/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/BakeIndex.cs b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/BakeIndex.cs new file mode 100644 index 00000000000..a287372f119 --- /dev/null +++ b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/BakeIndex.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using OpenMetaverse; +using OpenSim.Services.Interfaces; +using OpenSimNGC.Appearance.Baking; + +namespace OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; + +/// One channel's stored bake: the asset that holds it and the hash of the inputs that made it. +public sealed record StoredBake(UUID AssetId, string Hash); + +/// +/// ADR-004's per-agent bake index, held in the avatar service's key/value Avatars table. No schema change +/// and no service change: the table is already (PrincipalID, Name, Value) and +/// already exposes the three calls this needs — +/// to read every key, to write a +/// batch and to drop one. All three exist on the local service +/// (OpenSim.Services.AvatarService/AvatarService.cs) and on the Robust connector +/// (OpenSim.Services.Connectors/Avatar/AvatarServicesConnector.cs, methods getavatar, +/// setitems, removeitems), so a grid region and a standalone use the same code. +/// +/// The keys, per agent: +/// +/// Bake:<channel> — the stored bake's asset UUID (channel is the name, e.g. Bake:Head). +/// BakeHash:<channel> — the of the inputs that produced it. +/// BakeCOFVersion — the Current Outfit folder's Version at bake time (ADR-006). +/// BakeSize — the [Appearance] BakeSize the bakes were made at. +/// BakeUpdated — UTC, round-trip ("o") format; what the TTL reaper will compare against. +/// +/// The longest key is BakeHash:LeftArm at 16 characters, well inside the table's Name varchar(32) +/// (OpenSim.Data.MySQL/Resources/Avatar.migrations:7). +/// +/// +/// Why every key here starts with "Bake". AvatarService.SetAvatar deletes every row for the +/// principal before rewriting the appearance keys, and it has to: the appearance keys are of variable cardinality +/// and are read back additively, so a row left behind by a garment that was taken off would put it back on. Until +/// S3 that delete took this index with it, and every appearance save destroyed it (Ledger Q-14). The service now +/// preserves the names accepts, and this class derives its two prefixes +/// from so the two cannot drift apart. +/// +/// +/// +/// The bake path still does not queue an appearance save (see ): a bake +/// changes only the baked faces, which the avatar service does not persist at all, so the save would be pure +/// cost. And a missing index remains safe in every case — it means "re-bake", never a wrong bake. +/// +/// +public sealed class BakeIndex +{ + // All five derive from AvatarDataKeys.BakeIndexPrefix, which is what AvatarService.SetAvatar preserves. Adding + // a key here that does not start with it would be silently wiped by the next appearance save. + public const string BakeKeyPrefix = AvatarDataKeys.BakeIndexPrefix + ":"; + public const string HashKeyPrefix = AvatarDataKeys.BakeIndexPrefix + "Hash:"; + public const string CofVersionKey = AvatarDataKeys.BakeIndexPrefix + "COFVersion"; + public const string SizeKey = AvatarDataKeys.BakeIndexPrefix + "Size"; + public const string UpdatedKey = AvatarDataKeys.BakeIndexPrefix + "Updated"; + + /// An index with nothing in it: every channel re-bakes. + public static readonly BakeIndex Empty = new(new Dictionary(), 0, 0, default); + + private readonly Dictionary m_bakes; + + private BakeIndex(Dictionary bakes, int cofVersion, int size, DateTime updatedUtc) + { + m_bakes = bakes; + CofVersion = cofVersion; + Size = size; + UpdatedUtc = updatedUtc; + } + + /// The stored bake per channel, for channels that have one. + public IReadOnlyDictionary Bakes => m_bakes; + + /// The COF folder version recorded at bake time, or 0 if none is stored. + public int CofVersion { get; } + + /// The bake size the stored bakes were made at, or 0 if none is stored. + public int Size { get; } + + /// When the index was last written (UTC), or default if never. + public DateTime UpdatedUtc { get; } + + public bool TryGet(BakeChannel ch, out StoredBake bake) => m_bakes.TryGetValue(ch, out bake); + + public static string BakeKey(BakeChannel ch) => BakeKeyPrefix + ch; + public static string HashKey(BakeChannel ch) => HashKeyPrefix + ch; + + // ------------------------------------------------------------------ read + + /// Read the index for one agent. A null service, an absent record or an unparseable value all read as empty. + public static BakeIndex Read(IAvatarService avatars, UUID agentId) + { + if (avatars is null) return Empty; + AvatarData data; + try { data = avatars.GetAvatar(agentId); } + catch (Exception) { return Empty; } + return Parse(data?.Data); + } + + /// The parsing half of , over the raw key/value map. + public static BakeIndex Parse(IReadOnlyDictionary data) + { + if (data is null || data.Count == 0) return Empty; + + var assetIds = new Dictionary(); + var hashes = new Dictionary(); + foreach (var (key, value) in data) + { + // "BakeHash:Head" does not start with "Bake:", so the two prefixes cannot collide. + if (key.StartsWith(BakeKeyPrefix, StringComparison.Ordinal)) + { + if (Enum.TryParse(key.Substring(BakeKeyPrefix.Length), out var ch) + && UUID.TryParse(value, out var id) && !id.IsZero()) + assetIds[ch] = id; + } + else if (key.StartsWith(HashKeyPrefix, StringComparison.Ordinal)) + { + if (Enum.TryParse(key.Substring(HashKeyPrefix.Length), out var ch) && !string.IsNullOrEmpty(value)) + hashes[ch] = value; + } + } + + // only a channel with both halves counts: a UUID with no hash can never match, and a hash with no UUID + // has nothing to reuse. + var bakes = new Dictionary(); + foreach (var (ch, id) in assetIds) + if (hashes.TryGetValue(ch, out var hash)) bakes[ch] = new StoredBake(id, hash); + + var cof = data.TryGetValue(CofVersionKey, out var c) && int.TryParse(c, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cv) ? cv : 0; + var size = data.TryGetValue(SizeKey, out var s) && int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sv) ? sv : 0; + var updated = data.TryGetValue(UpdatedKey, out var u) + && DateTime.TryParse(u, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var uv) + ? uv.ToUniversalTime() : default; + + return new BakeIndex(bakes, cof, size, updated); + } + + // ------------------------------------------------------------------ write + + /// + /// Write the index for one agent: one Bake:/BakeHash: pair per live bake plus the three scalars, + /// in a single call. Channels absent from are + /// left exactly as they are — a channel that stops being produced (a skirt taken off) keeps its key and its + /// asset, because the agent's face still points at that asset; expiry is the reaper's job (ADR-004). + /// + public static bool Write(IAvatarService avatars, UUID agentId, IEnumerable> bakes, + int cofVersion, int bakeSize, DateTime updatedUtc) + { + if (avatars is null) return false; + var names = new List(); + var values = new List(); + foreach (var (ch, bake) in bakes) + { + if (bake is null || bake.AssetId.IsZero() || string.IsNullOrEmpty(bake.Hash)) continue; + names.Add(BakeKey(ch)); values.Add(bake.AssetId.ToString()); + names.Add(HashKey(ch)); values.Add(bake.Hash); + } + if (names.Count == 0) return false; + names.Add(CofVersionKey); values.Add(cofVersion.ToString(CultureInfo.InvariantCulture)); + names.Add(SizeKey); values.Add(bakeSize.ToString(CultureInfo.InvariantCulture)); + names.Add(UpdatedKey); values.Add(updatedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)); + return avatars.SetItems(agentId, names.ToArray(), values.ToArray()); + } + + /// Drop the keys for the named channels (the reaper's per-channel half; nothing in the bake path calls it). + public static bool Remove(IAvatarService avatars, UUID agentId, IEnumerable channels) + { + if (avatars is null) return false; + var names = channels.SelectMany(ch => new[] { BakeKey(ch), HashKey(ch) }).ToArray(); + return names.Length != 0 && avatars.RemoveItems(agentId, names); + } +} diff --git a/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/BakeOrchestrator.cs b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/BakeOrchestrator.cs new file mode 100644 index 00000000000..88412534e5d --- /dev/null +++ b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/BakeOrchestrator.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Services.Interfaces; +using OpenSimNGC.Appearance.Baking; + +namespace OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; + +/// +/// The scene-free part of the pipeline (Design Brief §4.2 steps 2, 4-6): resolve the agent's wearables and +/// textures from the asset service into a , run the backend, store the bakes as assets +/// and write the baked faces. Takes only the wearable set, the visual params, an asset service, a backend and +/// the to write into, so it is unit-tested with fakes and no ScenePresence. +/// Sending and the appearance save (steps 7) stay in . +/// +/// +/// S2 adds the ADR-004 reuse path: the per-channel input hash is computed from the wearables alone, before any +/// texture is fetched, so a channel whose inputs have not changed and whose stored asset still resolves costs +/// nothing at all — no asset fetch, no J2K decode, no composite, no encode, no store. Its face is still written +/// and the appearance is still sent, because the reason for the bake may be that a viewer has never seen it. +/// +/// +public static class BakeOrchestrator +{ + /// Why a wearable or texture could not be used and which channels it takes down. + public sealed record InputFailure(BakeChannel Channel, string Reason); + + /// The resolved inputs: the request the backend gets, plus the channels already lost to unusable inputs. + public sealed record ResolvedInputs(BakeRequest Request, IReadOnlyList Failures, IReadOnlyList Notes) + { + public bool IsFailed(BakeChannel ch) { foreach (var f in Failures) if (f.Channel == ch) return true; return false; } + public string FailureReason(BakeChannel ch) => string.Join("; ", Failures.Where(f => f.Channel == ch).Select(f => f.Reason)); + } + + /// + /// The wearable half of : everything that can be known before a single texture is + /// fetched. It is enough to compute every channel's , which is what lets the reuse + /// check run before the expensive half. + /// + public sealed record ResolvedWearables( + IReadOnlyList Wearables, + IReadOnlyList Parsed, + IReadOnlyDictionary VisualParams, + IReadOnlyList Failures, + IReadOnlyList Notes); + + private static readonly BakeChannel[] AllChannels = Enum.GetValues(); + private static readonly Dictionary NoTextures = new(); + + /// The TextureEntry face a channel is written to: AvatarAppearance.BAKE_INDICES in BakeChannel order. + public static int FaceOf(BakeChannel ch) => AvatarAppearance.BAKE_INDICES[(int)ch]; + + /// The asset name a stored bake carries (ADR-004): bake:<agent>:<channel>. + public static string AssetNameFor(UUID agentId, BakeChannel ch) => $"bake:{agentId}:{ch.ToString().ToLowerInvariant()}"; + + // ------------------------------------------------------------------ step 2 + 4: inputs + + /// + /// Fetch every worn wearable asset (types 5/13) and parse it. A wearable that cannot be fetched or parsed + /// fails the channels its type feeds (the shape feeds them all). Nothing else is refused (ADR-005). No + /// texture is fetched here. + /// + public static ResolvedWearables ResolveWearables(AvatarWearable[] wearables, byte[] visualParams, IAssetService assets, TexLayerCompositor compositor, BakeTimings timings = null) + { + ArgumentNullException.ThrowIfNull(assets); + ArgumentNullException.ThrowIfNull(compositor); + var inputs = new List(); + var parsed = new List(); + var failures = new List(); + var notes = new List(); + + if (wearables is not null) + { + for (var type = 0; type < wearables.Length; type++) + { + var slot = wearables[type]; + if (slot is null) continue; + for (var j = 0; j < slot.Count; j++) + { + var assetId = slot[j].AssetID; + if (assetId.IsZero()) + { + // The slot is worn, there is just no asset behind it (typically a default system wearable + // item). That is still a worn wearable: the viewer counts wearables, not textures + // (LLTexLayerTemplate::updateWearableCache, lltexlayer.cpp:1615-1638), so it contributes its + // layers' morph masks with the avatar's own parameter values. Passing it on as an empty + // WearableInput is what the library expects (S1c, MORPH-MASK-PASS.md §2.4); dropping it here + // was Ledger Q-12. It carries no textures, so nothing is fetched for it. + inputs.Add(new WearableInput(UUID.Zero, type, "")); + parsed.Add(new ParsedWearable((WearableKind)type, "", new Dictionary(), new Dictionary())); + notes.Add($"wearable type {(WearableKind)type}:{j} is worn with no asset; kept as a worn instance with no textures"); + continue; + } + var t0 = BakeTimings.Now; + var asset = assets.Get(assetId.ToString()); + timings?.AddAssetFetch(t0, asset?.Data?.Length ?? 0); + if (asset?.Data is not { Length: > 0 }) + { + Fail(failures, ChannelsFedBy((WearableKind)type, compositor), $"wearable type {(WearableKind)type} asset {assetId} not found"); + continue; + } + var text = Encoding.UTF8.GetString(asset.Data); + ParsedWearable pw; + try { pw = WearableParser.Parse(text); } + catch (FormatException ex) + { + Fail(failures, ChannelsFedBy((WearableKind)type, compositor), $"wearable type {(WearableKind)type} asset {assetId} unparseable: {ex.Message}"); + continue; + } + inputs.Add(new WearableInput(assetId, type, text)); + parsed.Add(pw with { Kind = (WearableKind)type }); + } + } + } + + // the presence's VisualParams, decoded through the parameter table as an overlay (the wearables' own values win) + var overlay = new Dictionary(); + if (visualParams is not null) + { + var list = VisualParamEncoder.SendList(compositor.Lad); + if (visualParams.Length == list.Count) + { + for (var i = 0; i < list.Count; i++) + overlay[list[i].Id] = list[i].Min + visualParams[i] / 255f * (list[i].Max - list[i].Min); + } + else notes.Add($"VisualParams has {visualParams.Length} bytes, the parameter table {list.Count}; not overlaid"); + } + + return new ResolvedWearables(inputs, parsed, overlay, failures, notes); + } + + /// + /// Fetch, once each, every texture the given channels' layer sets can draw. A texture that cannot be fetched + /// fails the channels among whose layer sets draw its slot — never a channel that + /// was not asked for, since such a channel is being reused and its face is not being rewritten. + /// + public static (IReadOnlyDictionary Textures, IReadOnlyList Failures) ResolveTextures( + IReadOnlyList parsed, IReadOnlyCollection channels, IAssetService assets, TexLayerCompositor compositor, BakeTimings timings = null) + { + ArgumentNullException.ThrowIfNull(assets); + ArgumentNullException.ThrowIfNull(compositor); + var textures = new Dictionary(); + var failures = new List(); + if (channels.Count == 0) return (textures, failures); + + var drawn = channels.SelectMany(compositor.SlotsOf).ToHashSet(); + foreach (var pw in parsed) + { + foreach (var (texSlot, id) in pw.Textures) + { + if (!drawn.Contains(texSlot)) continue; + if (id.IsZero() || id == BakeConstants.DefaultAvatarTexture || textures.ContainsKey(id)) continue; + var t0 = BakeTimings.Now; + var asset = assets.Get(id.ToString()); + timings?.AddAssetFetch(t0, asset?.Data?.Length ?? 0); + if (asset?.Data is not { Length: > 0 }) + { + Fail(failures, ChannelsDrawing(texSlot, compositor).Where(channels.Contains), $"texture {id} ({texSlot}) not found"); + continue; + } + textures[id] = new TextureInput(id, asset.Data); + } + } + return (textures, failures); + } + + /// + /// Wearables and every texture they reference, in one step: the S1 entry point, kept for callers that want a + /// full bake with no reuse. uses the two halves separately so the reuse check can run + /// between them. + /// + public static ResolvedInputs Resolve(AvatarWearable[] wearables, byte[] visualParams, IAssetService assets, TexLayerCompositor compositor, int bakeSize) + { + var w = ResolveWearables(wearables, visualParams, assets, compositor); + var (textures, texFailures) = ResolveTextures(w.Parsed, AllChannels, assets, compositor); + var failures = new List(w.Failures); + failures.AddRange(texFailures); + return new ResolvedInputs(new BakeRequest(w.Wearables, w.VisualParams, textures, bakeSize), failures, w.Notes); + } + + private static void Fail(List failures, IEnumerable channels, string reason) + { + foreach (var ch in channels) failures.Add(new InputFailure(ch, reason)); + } + + /// Channels whose layer sets draw a slot owned by the wearable type; the shape (parameters only) feeds every channel. + public static IEnumerable ChannelsFedBy(WearableKind kind, TexLayerCompositor compositor) + { + if (kind == WearableKind.Shape) return AllChannels; + return AllChannels.Where(ch => compositor.SlotsOf(ch).Any(s => TexLayerCompositor.WearableOf(s) == kind)); + } + + /// Channels whose layer sets draw the given texture slot. + public static IEnumerable ChannelsDrawing(TextureSlot slot, TexLayerCompositor compositor) + => AllChannels.Where(ch => compositor.SlotsOf(ch).Contains(slot)); + + // ------------------------------------------------------------------ step 3: the reuse decision (ADR-004) + + /// What the reuse check decided for one run. + /// Channels whose stored bake is being kept, with the asset and hash to write back. + /// Channels that must be composited. + /// The freshly computed input hash of every channel the outfit needs. + /// One line per channel that could have been reused but was not, and why. + public sealed record ReuseDecision( + IReadOnlyDictionary Reused, + IReadOnlyList ToBake, + IReadOnlyDictionary Hashes, + IReadOnlyList Notes); + + /// + /// Decide, per channel, whether the stored bake can be kept (ADR-004). A channel is reused when + /// + /// the index records a bake and a hash for it, and + /// the stored index was written at the bake size now in force, and + /// the hash of the current inputs equals the stored hash, and + /// the stored asset still resolves in the asset service. + /// + /// (2) is belt and braces: already folds the size into the hash, so a + /// [Appearance] BakeSize change invalidates every channel through (3) as well. (4) is the rule that a + /// stored hash whose asset has vanished — deleted by an operator, lost in an asset-service migration — must + /// never be trusted: the face would point at nothing and the avatar would go untextured for good. + /// + public static ReuseDecision DecideReuse(IReadOnlyList needed, BakeRequest hashRequest, BakeIndex index, + IAssetService assets, TexLayerCompositor compositor, int bakeSize) + { + ArgumentNullException.ThrowIfNull(assets); + var hashes = new Dictionary(needed.Count); + foreach (var ch in needed) hashes[ch] = BakeHash.Compute(ch, hashRequest, compositor); + + var notes = new List(); + var candidates = new List(); + foreach (var ch in needed) + { + if (!index.TryGet(ch, out var stored)) { notes.Add($"{ch}: no stored bake"); continue; } + if (index.Size != bakeSize) { notes.Add($"{ch}: stored at size {index.Size}, now {bakeSize}"); continue; } + if (!string.Equals(stored.Hash, hashes[ch], StringComparison.Ordinal)) { notes.Add($"{ch}: inputs changed"); continue; } + candidates.Add(ch); + } + + var reused = new Dictionary(); + if (candidates.Count > 0) + { + var ids = candidates.Select(ch => index.Bakes[ch].AssetId.ToString()).ToArray(); + var exists = Exists(assets, ids); + for (var i = 0; i < candidates.Count; i++) + { + var ch = candidates[i]; + if (exists[i]) reused[ch] = index.Bakes[ch]; + else notes.Add($"{ch}: stored asset {ids[i]} has vanished from the asset service"); + } + } + + var toBake = needed.Where(ch => !reused.ContainsKey(ch)).ToList(); + return new ReuseDecision(reused, toBake, hashes, notes); + } + + /// + /// Does each asset still resolve? One call where the service answers + /// it (RegionAssetConnectorModule returns null for a mixed local/HG batch), otherwise one metadata fetch each. + /// Anything that cannot be answered counts as absent, which costs a re-bake and never a broken face. + /// + private static bool[] Exists(IAssetService assets, string[] ids) + { + bool[] exists = null; + try { exists = assets.AssetsExist(ids); } + catch (Exception) { } + if (exists is not null && exists.Length == ids.Length) return exists; + + exists = new bool[ids.Length]; + for (var i = 0; i < ids.Length; i++) + { + try { exists[i] = assets.GetMetadata(ids[i]) is not null; } + catch (Exception) { exists[i] = false; } + } + return exists; + } + + // ------------------------------------------------------------------ step 6: store + faces + supersede + + /// + /// Store each bake as a texture asset (ADR-004 marker: name bake:<agent>:<channel>, description = input + /// hash, not temporary, not local, creator = agent) and write its UUID to the channel's baked face. Channels in + /// keep their stored asset and have their face written to it without any store. + /// Channels in 's failures and channels the backend produced nothing for keep their + /// current face. + /// + /// + /// Supersede (ADR-004): once a channel's new asset is confirmed stored and its face has been moved to + /// it, the asset the channel held before is deleted. Never before the store — a store that fails must leave + /// the old bake serving — and never an asset any baked face still points at. + /// + /// + public static IReadOnlyList StoreAndApply(IReadOnlyList results, ResolvedInputs inputs, UUID agentId, + IAssetService assets, AvatarAppearance appearance, + IReadOnlyDictionary reused = null, IReadOnlyDictionary previous = null, + List superseded = null, BakeTimings timings = null) + { + ArgumentNullException.ThrowIfNull(results); + ArgumentNullException.ThrowIfNull(assets); + ArgumentNullException.ThrowIfNull(appearance); + var byChannel = results.ToDictionary(r => r.Channel); + var outcomes = new List(AllChannels.Length); + var empty = new FidelityReport(Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty()); + foreach (var ch in AllChannels) + { + // Reuse first: a reused channel was never fetched for, so it cannot have acquired a texture failure, + // and its stored bake is by definition the bake its unchanged inputs produce. + if (reused is not null && reused.TryGetValue(ch, out var keep)) + { + appearance.Texture.CreateFace((uint)FaceOf(ch)).TextureID = keep.AssetId; + outcomes.Add(new ChannelOutcome(ch, ChannelStatus.Reused, keep.AssetId, keep.Hash, "inputs unchanged", empty)); + continue; + } + byChannel.TryGetValue(ch, out var result); + if (inputs.IsFailed(ch)) + { + outcomes.Add(new ChannelOutcome(ch, ChannelStatus.Failed, UUID.Zero, result?.InputHash ?? "", inputs.FailureReason(ch), result?.Fidelity ?? empty)); + continue; + } + if (result is null) + { + outcomes.Add(new ChannelOutcome(ch, ChannelStatus.Skipped, UUID.Zero, "", "nothing worn for this channel", empty)); + continue; + } + if (result.J2kBytes is not { Length: > 0 }) + { + outcomes.Add(new ChannelOutcome(ch, ChannelStatus.Failed, UUID.Zero, result.InputHash, "backend returned no bytes", result.Fidelity)); + continue; + } + if (result.NothingDrawn) + { + // Every layer of this channel was skipped, so the bake is whatever the canvas was cleared to — + // opaque, not blank (S1d measured 96.5% opaque near-black on an assetless skirt slot). Storing it + // and writing the face would paint that over the avatar, replacing a viewer bake that may be + // perfectly good. The face keeps what it has. Note this is a fact about the layer decisions, not + // the pixels: a channel that drew a fully transparent texture (a bald hair) is stored normally. + outcomes.Add(new ChannelOutcome(ch, ChannelStatus.Skipped, UUID.Zero, result.InputHash, "nothing drawn for this channel", result.Fidelity)); + continue; + } + var asset = new AssetBase(UUID.Random(), AssetNameFor(agentId, ch), (sbyte)AssetType.Texture, agentId.ToString()) + { + Data = result.J2kBytes, + Description = result.InputHash, + Temporary = false, + Local = false, + }; + var tStore = BakeTimings.Now; + var storedId = assets.Store(asset); + timings?.AddAssetStore(tStore); + if (string.IsNullOrEmpty(storedId) || !UUID.TryParse(storedId, out var id) || id.IsZero()) + { + // the store failed: the channel's previous bake, if any, is still the one its face points at and + // is emphatically not superseded. + outcomes.Add(new ChannelOutcome(ch, ChannelStatus.Failed, UUID.Zero, result.InputHash, "asset service refused the bake", result.Fidelity)); + continue; + } + appearance.Texture.CreateFace((uint)FaceOf(ch)).TextureID = id; + outcomes.Add(new ChannelOutcome(ch, ChannelStatus.Baked, id, result.InputHash, "", result.Fidelity)); + Supersede(assets, appearance, previous, ch, id, superseded, timings); + } + return outcomes; + } + + /// + /// Delete the asset the channel held before this run, now that the new one is stored and the face has moved. + /// Refuses to delete an asset that any baked face still points at — including the one just written, and + /// including a face of some other channel that was left alone this run. + /// + private static void Supersede(IAssetService assets, AvatarAppearance appearance, IReadOnlyDictionary previous, + BakeChannel ch, UUID newId, List superseded, BakeTimings timings = null) + { + if (previous is null || !previous.TryGetValue(ch, out var old)) return; + if (old.AssetId.IsZero() || old.AssetId == newId) return; + foreach (var other in AllChannels) + { + var face = appearance.Texture.FaceTextures[FaceOf(other)]; + if (face is not null && face.TextureID == old.AssetId) return; + } + try + { + var t0 = BakeTimings.Now; + var gone = assets.Delete(old.AssetId.ToString()); + timings?.AddAssetStore(t0, deleted: true); + if (gone) superseded?.Add(old.AssetId); + } + catch (Exception) { } + } + + // ------------------------------------------------------------------ the whole scene-free run + + /// A full bake with no persistence: every channel is composited and nothing is written to the avatar service. + public static BakeOutcome Run(UUID agentId, BakeReason reason, AvatarWearable[] wearables, byte[] visualParams, AvatarAppearance appearance, + IAssetService assets, IBakeBackend backend, TexLayerCompositor compositor, int bakeSize, CancellationToken ct) + => Run(agentId, reason, wearables, visualParams, appearance, assets, null, backend, compositor, bakeSize, 0, ct); + + /// + /// Resolve, decide what can be reused, bake the rest, store, write faces, supersede, and record the bake index + /// in the avatar service. Sending is the caller's. + /// + /// The avatar service holding the ADR-004 index, or null for a run that neither reads nor writes it. + /// The Current Outfit folder's version at bake time; stored, not compared (the hash is the stronger test). + public static BakeOutcome Run(UUID agentId, BakeReason reason, AvatarWearable[] wearables, byte[] visualParams, AvatarAppearance appearance, + IAssetService assets, IAvatarService avatars, IBakeBackend backend, TexLayerCompositor compositor, int bakeSize, int cofVersion, + CancellationToken ct) + { + var sw = Stopwatch.StartNew(); + var timings = new BakeTimings(); + var w = ResolveWearables(wearables, visualParams, assets, compositor, timings); + ct.ThrowIfCancellationRequested(); + + // the channels this outfit needs at all, and their input hashes — computed from the wearables alone, so + // the reuse decision happens before a single texture is fetched + var needed = SkiaBakeBackend.ChannelsFor(w.Parsed); + var hashRequest = new BakeRequest(w.Wearables, w.VisualParams, NoTextures, bakeSize); + var index = BakeIndex.Read(avatars, agentId); + var reuse = DecideReuse(needed, hashRequest, index, assets, compositor, bakeSize); + ct.ThrowIfCancellationRequested(); + + var (textures, texFailures) = ResolveTextures(w.Parsed, reuse.ToBake, assets, compositor, timings); + var failures = new List(w.Failures); + failures.AddRange(texFailures); + var notes = new List(w.Notes); + notes.AddRange(reuse.Notes); + var inputs = new ResolvedInputs( + new BakeRequest(w.Wearables, w.VisualParams, textures, bakeSize) { Channels = reuse.ToBake, Timings = timings }, + failures, notes); + ct.ThrowIfCancellationRequested(); + + IReadOnlyList results; + try + { + results = reuse.ToBake.Count == 0 + ? Array.Empty() + : backend.BakeAsync(inputs.Request, ct).GetAwaiter().GetResult(); + } + catch (OperationCanceledException) { throw; } + catch (ArgumentException ex) + { + // ADR-005: corrupt input is the one refusal; every channel keeps its existing face and the index is + // left exactly as it was. + var all = AllChannels.Select(ch => new ChannelOutcome(ch, ChannelStatus.Failed, UUID.Zero, "", $"backend refused the inputs: {ex.Message}", + new FidelityReport(Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty()))).ToList(); + return new BakeOutcome(agentId, reason, all, sw.ElapsedMilliseconds) { Timings = timings }; + } + + var superseded = new List(); + var outcomes = StoreAndApply(results, inputs, agentId, assets, appearance, reuse.Reused, index.Bakes, superseded, timings); + + // the index: one row pair per channel that now has a live bake, plus the three scalars + var live = outcomes + .Where(o => o.Status is ChannelStatus.Baked or ChannelStatus.Reused && !o.AssetId.IsZero()) + .Select(o => new KeyValuePair(o.Channel, new StoredBake(o.AssetId, o.InputHash))) + .ToList(); + var indexWritten = live.Count > 0 && BakeIndex.Write(avatars, agentId, live, cofVersion, bakeSize, DateTime.UtcNow); + + return new BakeOutcome(agentId, reason, outcomes, sw.ElapsedMilliseconds) + { + Superseded = superseded, + IndexWritten = indexWritten, + Notes = notes, + Timings = timings, + }; + } +} diff --git a/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/CofHandshake.cs b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/CofHandshake.cs new file mode 100644 index 00000000000..69953b1cea5 --- /dev/null +++ b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/CofHandshake.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Concurrent; +using OpenMetaverse; + +namespace OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; + +/// What the cap should do with one UpdateAvatarAppearance POST. +public enum CofVerdict +{ + /// The viewer and the sim agree on the COF version: bake (or reuse) and answer {success:true}. + Bake, + + /// The viewer is behind the sim: answer {success:false, expected:<server>} and let it re-request. + Stale, + + /// + /// Too many mismatches too quickly (Ledger R-2). Bake at the server's version and answer + /// {success:true} rather than trade refusals with the viewer forever. + /// + LivelockBake, +} + +/// One decision: what to do, and the version to quote back. +/// The action. +/// The server's COF version after any re-read — the value to bake at, or to return as expected. +/// A short human-readable account, for the log and the test failure message. +public sealed record CofDecision(CofVerdict Verdict, int Version, string Reason) +{ + /// Whether the cap answers success:true. + public bool Success => Verdict is CofVerdict.Bake or CofVerdict.LivelockBake; +} + +/// +/// The Design Brief §4.3 handshake, kept free of Scene, HTTP and the clock so every branch is a plain unit +/// test. One instance per region; it holds only the per-agent mismatch counters the anti-livelock rule needs. +/// +/// +/// cof_version == server. +/// cof_version < server, quoting the server's version. +/// cof_version > server → the viewer changed the COF by a path this sim has not seen yet, so the +/// folder is re-read once and the comparison repeated. Equal after the re-read is a +/// ; anything else is quoting the freshly read +/// version, which is the only number the sim can honestly offer. +/// After Stale verdicts for one agent inside , the next +/// POST is a instead: bake at the server's version and log it +/// (Ledger R-2). A successful bake clears the agent's counter. +/// +/// +public sealed class CofHandshake +{ + /// Consecutive mismatches inside before the anti-livelock rule fires. + public int MaxMismatches { get; init; } = 5; + + /// The window the mismatches have to fall inside. + public TimeSpan Window { get; init; } = TimeSpan.FromSeconds(30); + + private sealed class Counter + { + public int Count; + public DateTime FirstUtc; + } + + private readonly ConcurrentDictionary m_mismatches = new(); + + /// Mismatches currently counted for an agent; 0 when it has none or they have aged out. + public int MismatchesFor(UUID agentId) => m_mismatches.TryGetValue(agentId, out var c) ? c.Count : 0; + + /// Forget an agent's counter — on a successful bake, and when the agent leaves the region. + public void Clear(UUID agentId) => m_mismatches.TryRemove(agentId, out _); + + /// + /// Decide one POST. + /// + /// The posting agent. + /// The cof_version the viewer sent. + /// The COF folder version the sim has just read. + /// + /// Reads the COF folder version again. Called at most once per decision, and only on the greater-than branch. + /// + /// The clock, injected so the window is testable. + public CofDecision Decide(UUID agentId, int clientVersion, int serverVersion, Func reread, DateTime nowUtc) + { + if (clientVersion == serverVersion) + { + Clear(agentId); + return new CofDecision(CofVerdict.Bake, serverVersion, "cof_version matches"); + } + + if (clientVersion > serverVersion) + { + // The viewer is ahead: it changed the COF through a path this sim has not observed (AIS, most + // likely). Read the folder again before refusing — the write may simply have landed after our read. + int fresh = serverVersion; + try { fresh = reread is null ? serverVersion : reread(); } + catch (Exception) { /* keep the version we already had; the mismatch path below is still correct */ } + + if (clientVersion == fresh) + { + Clear(agentId); + return new CofDecision(CofVerdict.Bake, fresh, "cof_version matched after re-reading the folder"); + } + serverVersion = fresh; + } + + // Not equal, and the re-read did not rescue it. Count the mismatch and either refuse or, if the viewer + // has been refused too often too fast, bake anyway so the two cannot trade refusals forever. + var counter = m_mismatches.AddOrUpdate( + agentId, + _ => new Counter { Count = 1, FirstUtc = nowUtc }, + (_, existing) => + { + if (nowUtc - existing.FirstUtc > Window) + { + existing.Count = 1; + existing.FirstUtc = nowUtc; + } + else existing.Count++; + return existing; + }); + + if (counter.Count >= MaxMismatches) + { + Clear(agentId); + return new CofDecision(CofVerdict.LivelockBake, serverVersion, + $"{counter.Count} mismatches within {Window.TotalSeconds:F0}s (client {clientVersion}, server {serverVersion}); baking at the server's version"); + } + + return new CofDecision(CofVerdict.Stale, serverVersion, + $"client cof_version {clientVersion}, server {serverVersion}"); + } +} diff --git a/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/CofWearables.cs b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/CofWearables.cs new file mode 100644 index 00000000000..1288e5843e7 --- /dev/null +++ b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/CofWearables.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using OpenMetaverse; +using OpenSim.Framework; + +namespace OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; + +/// +/// One Current Outfit Folder link that names a wearable, already resolved against this region's inventory. +/// +/// The link item's own id, in the COF. Only used for logging. +/// +/// The link item's description — the viewer's ordering information for same-type wearables. See +/// . +/// +/// The linked-to inventory item: the id that goes into . +/// The target's wearable type (the index into ). +/// The target's asset id, or when this region has not resolved it yet. +public sealed record CofWearableLink(UUID LinkItemId, string Description, UUID TargetItemId, int WearableType, UUID AssetId); + +/// +/// S10. Turns the Current Outfit Folder's wearable links into , keeping +/// every link of a type and putting them in the order the viewer layers them. +/// +/// +/// The order is the viewer's, and the viewer writes it into the COF. LLAppearanceMgr stores each +/// clothing link's position in the link item's description as "@" + (type * 100 + index) +/// (build_order_string, llappearancemgr.cpp:3637-3642, written by +/// getWearableOrderingDescUpdates :3676-3702 and pushed to the server by updateClothingOrderingInfo +/// :3733), and sorts by that description before wearing (sortItemsByActualDescription :4346-4351, called +/// at :2683). The bake must layer by the same key: LLTexLayerTemplate::render walks the wearables of a +/// type from index 0 upwards and draws each over the last (lltexlayer.cpp:1659-1689 over the cache built at +/// :1615-1638), so the higher index is on top. +/// +/// +/// +/// Two rules are inherited and must not regress. S0c: a type this COF read says nothing about keeps what +/// the agent already wears, rather than being emptied by a partial view of the outfit. S8: a link whose +/// target this region cannot resolve is a statement about the inventory lookup, not about what the avatar is +/// wearing (AvatarFactoryModule.SetAppearanceAssets, AvatarFactoryModule.cs:975-989) — such a link never +/// reaches here, and because it does not, its type is one this read says nothing about and keeps its contents. +/// The caller must therefore drop unresolvable links rather than pass them with a guessed type. +/// +/// +/// Pure, and free of Scene and IInventoryService, so every rule above is a plain unit test. +/// +public static class CofWearables +{ + /// + /// The viewer's sort key for a COF link's description, or -1 when the description carries no ordering + /// information. WearablesOrderComparator (llappearancemgr.cpp:3644-3674) accepts a description of + /// exactly the width build_order_string produces and beginning with '@', compares those + /// lexicographically, and sinks everything else below them (an empty description, "Broken link", a link the + /// viewer has not numbered yet). Same width and the same leading '@' makes the lexicographic compare + /// a numeric one, which is what this returns. + /// + /// + /// The number is type * 100 + index, so within one wearable type the key orders exactly as the index + /// does — which is all needs, and it means a link numbered for another type still sorts + /// deterministically instead of being dropped, as it does in the viewer before it renumbers it. + /// + /// + public static int OrderKey(string description) + { + if (string.IsNullOrEmpty(description) || description[0] != ORDER_NUMBER_SEPARATOR) return -1; + return int.TryParse(description.AsSpan(1), out var n) && n >= 0 ? n : -1; + } + + /// ORDER_NUMBER_SEPARATOR, llappearancemgr.cpp:111. + private const char ORDER_NUMBER_SEPARATOR = '@'; + + /// + /// The index build_order_string encoded for a link of this type, or -1 when the description carries no + /// ordering information for it. Diagnostic: sorts on , which needs + /// no type. + /// + public static int OrderOf(string description, int wearableType) + { + var key = OrderKey(description); + return key >= 0 && key / 100 == wearableType ? key % 100 : -1; + } + + /// + /// Apply a COF read to an existing wearable set. + /// + /// + /// Neither argument is mutated; the result is a new array, safe to assign to + /// . Rules: + /// + /// A type that any link names is replaced by exactly that type's links, ordered by + /// ascending, with unnumbered links kept in the order given and sunk below the + /// numbered ones. Asset ids come from the links; a link may carry and be resolved + /// later by SetAppearanceAssets, exactly as an AgentIsNowWearing item is. + /// A type no link names keeps its current items (S0c), which is also what makes an unresolvable link + /// harmless (S8). + /// caps a type at five items and ignores item + /// ids, so a sixth link of a type and a link to nothing are both dropped here, as they are on every other + /// path into the wearable table. + /// + /// + /// The agent's current wearables. May be null (treated as empty). + /// The COF's resolved wearable links, in any order. + /// True when the result differs from in any item id or order. + public static AvatarWearable[] Derive(AvatarWearable[] existing, IReadOnlyList links, out bool changed) + { + existing ??= Array.Empty(); + links ??= Array.Empty(); + + var byType = new Dictionary>(); + var length = existing.Length; + foreach (var link in links) + { + if (link is null || link.WearableType < 0) continue; + if (!byType.TryGetValue(link.WearableType, out var list)) byType[link.WearableType] = list = new List(); + list.Add(link); + if (link.WearableType >= length) length = link.WearableType + 1; + } + + var derived = new AvatarWearable[length]; + for (var type = 0; type < length; type++) + { + derived[type] = new AvatarWearable(); + if (byType.TryGetValue(type, out var list)) + { + // A stable sort on the viewer's key: numbered links ascending, unnumbered ones after them in the + // order the folder gave, which is what WearablesOrderComparator's "sink the invalid ones" does. + var ordered = new List(list); + ordered.Sort((a, b) => + { + int ka = OrderKey(a.Description), kb = OrderKey(b.Description); + if (ka == kb) return list.IndexOf(a).CompareTo(list.IndexOf(b)); + if (ka < 0) return 1; + if (kb < 0) return -1; + return ka.CompareTo(kb); + }); + foreach (var link in ordered) derived[type].Add(link.TargetItemId, link.AssetId); + continue; + } + + // Untouched by this read: keep what the agent already wears (S0c / S8). + if (type < existing.Length && existing[type] is not null) + for (var j = 0; j < existing[type].Count; j++) + derived[type].Add(existing[type][j].ItemID, existing[type][j].AssetID); + } + + changed = !SameItems(existing, derived); + return derived; + } + + /// Item ids, per type, in order: the comparison that decides whether a save is worth making. + private static bool SameItems(AvatarWearable[] a, AvatarWearable[] b) + { + var length = Math.Max(a.Length, b.Length); + for (var i = 0; i < length; i++) + { + var ca = i < a.Length && a[i] is not null ? a[i].Count : 0; + var cb = i < b.Length && b[i] is not null ? b[i].Count : 0; + if (ca != cb) return false; + for (var j = 0; j < ca; j++) + if (a[i][j].ItemID != b[i][j].ItemID) return false; + } + return true; + } +} diff --git a/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/IServerSideBaker.cs b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/IServerSideBaker.cs new file mode 100644 index 00000000000..d89a57e8a2f --- /dev/null +++ b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/IServerSideBaker.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenMetaverse; +using OpenSim.Region.Framework.Scenes; +using OpenSimNGC.Appearance.Baking; + +namespace OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; + +/// Why a server-side bake was requested. S1 uses only . +public enum BakeReason +{ + Console, + Login, + CofChanged, + Cap, +} + +/// What happened to one bake channel. +public enum ChannelStatus +{ + /// Composited, stored as an asset, written to the TextureEntry face. + Baked, + /// + /// Inputs unchanged and the stored asset still resolves: the bake was not recomputed (ADR-004). The channel's + /// face is still written to the stored asset and the appearance is still sent — reuse saves the compute, not + /// the delivery. + /// + Reused, + /// The library produced nothing for the channel (nothing worn for it); the face was left as it was. + Skipped, + /// An input for the channel was unusable (missing/unparseable wearable, missing/undecodable texture); the face was left as it was. + Failed, +} + +/// One channel's outcome. is the stored bake for Baked/Reused, UUID.Zero otherwise; is the bake's BakeHash (the stored asset's Description) or "". +public sealed record ChannelOutcome(BakeChannel Channel, ChannelStatus Status, UUID AssetId, string InputHash, string Reason, FidelityReport Fidelity); + +/// The result of one bake run for one agent. +public sealed record BakeOutcome(UUID AgentId, BakeReason Reason, IReadOnlyList Channels, long ElapsedMs) +{ + public int Count(ChannelStatus status) + { + var n = 0; + foreach (var c in Channels) if (c.Status == status) n++; + return n; + } + + /// Assets deleted because a new bake for the same channel superseded them (ADR-004). + public IReadOnlyList Superseded { get; init; } = Array.Empty(); + + /// Whether the ADR-004 bake index was written to the avatar service on this run. + public bool IndexWritten { get; init; } + + /// Resolver and reuse notes: why a channel was not reused, a wearable worn with no asset, and so on. + public IReadOnlyList Notes { get; init; } = Array.Empty(); + + /// Where the time went: asset fetch, J2K decode, composite, J2K encode, asset store (Ledger Q-10). + public BakeTimings Timings { get; init; } = new(); +} + +/// +/// Region-scoped server-side baker (Design Brief §4.2, ADR-002/004/005). Composites the agent's current wearables +/// through , stores the bakes as assets, writes the baked faces of the presence's +/// TextureEntry, sends the appearance and queues the normal appearance save. +/// +public interface IServerSideBaker +{ + Task BakeAsync(ScenePresence sp, BakeReason reason, CancellationToken ct); +} diff --git a/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/ServerSideBakingModule.cs b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/ServerSideBakingModule.cs new file mode 100644 index 00000000000..162a7866600 --- /dev/null +++ b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/ServerSideBakingModule.cs @@ -0,0 +1,576 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Net; +using Nini.Config; +using OpenMetaverse.StructuredData; +using Caps = OpenSim.Framework.Capabilities.Caps; +using OpenSim.Framework.Servers.HttpServer; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Framework.Console; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Services.Interfaces; +using OpenSim.Region.Framework.Scenes; +using OpenSimNGC.Appearance.Baking; +using Microsoft.Extensions.Logging; + +namespace OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; + +/// +/// Server-side baking (Design Brief §4.2 C2, ADR-001/002/004/005). S1 was the orchestrator plus a console +/// command; S2 added the ADR-004 index and the input-hash skip; S3 is the wire — RegionProtocols bit 0, +/// the AppearanceData block, the UpdateAvatarAppearance cap with the §4.3 handshake, and a +/// login-time bake. Config: +/// +/// [Appearance] +/// ServerSideBaking = false ; simulator-wide default for the wire flag +/// BakeSize = 1024 ; 512, 1024 or 2048 +/// BakeQuality = 0.85 +/// +/// [<Region Name>] +/// ServerSideBaking = true ; per-region override, same idiom as [AIS] AIS_Enabled +/// +/// +/// +/// Everything the flag gates is add-only (ADR-001). On a region where it is off, the handshake carries the value +/// it always carried, no cap is advertised, no login bake runs, and SendAppearance emits the count-0 +/// AppearanceData form — Firestorm keeps client-baking there exactly as before. The module still always +/// loads, so appearance serverbake <first> <last> exists on every region console. +/// +/// +public class ServerSideBakingModule : ISharedRegionModule, IServerSideBaker +{ + private static readonly ILogger m_log = LoggerProvider.CreateLogger(typeof(ServerSideBakingModule)); + + public const string ConfigSection = "Appearance"; + + private readonly List m_scenes = new(); + private readonly Dictionary m_regions = new(); + private IBakeBackend m_backend; + private TexLayerCompositor m_compositor; + + /// The cap the LL viewer POSTs to after every COF change (viewer contract V3). + public const string CapName = "UpdateAvatarAppearance"; + + /// The simulator-wide default for the wire flag; a [<Region Name>] section overrides it. + public bool ServerSideBakingEnabled { get; private set; } + public int BakeSize { get; private set; } = 1024; + public double BakeQuality { get; private set; } = 0.85; + + public string Name => "ServerSideBakingModule"; + public Type ReplaceableInterface => null; + + public void Initialise(IConfigSource source) + { + IConfig config = source.Configs[ConfigSection]; + if (config is not null) + { + ServerSideBakingEnabled = config.GetBoolean("ServerSideBaking", false); + BakeSize = config.GetInt("BakeSize", 1024); + BakeQuality = config.GetDouble("BakeQuality", 0.85); + } + if (BakeSize is not (512 or 1024 or 2048)) + { + m_log.LogWarning("[SSB]: [Appearance] BakeSize {Size} is not 512, 1024 or 2048; using 1024", BakeSize); + BakeSize = 1024; + } + BakeQuality = Math.Clamp(BakeQuality, 0.1, 1.0); + m_log.LogInformation("[SSB]: ServerSideBaking={Flag} (wire flag; not acted on before S3), BakeSize={Size}, BakeQuality={Quality}", + ServerSideBakingEnabled, BakeSize, BakeQuality); + } + + public void PostInitialise() { } + public void Close() { } + + public void AddRegion(Scene scene) + { + scene.RegisterModuleInterface(this); + } + + public void RemoveRegion(Scene scene) + { + scene.UnregisterModuleInterface(this); + ServerSideBakingRegion region; + lock (m_scenes) + { + m_scenes.Remove(scene); + m_regions.Remove(scene, out region); + } + if (region is null) return; + scene.UnregisterModuleInterface(region); + scene.EventManager.OnMakeRootAgent -= OnMakeRootAgent; + scene.EventManager.OnRemovePresence -= region.Forget; + scene.EventManager.OnAvatarAppearanceChange -= OnAvatarAppearanceChanged; + } + + /// This region's state, or null on a region the module has not finished loading. + public ServerSideBakingRegion RegionOf(Scene scene) + { + lock (m_scenes) return m_regions.TryGetValue(scene, out var r) ? r : null; + } + + public void RegionLoaded(Scene scene) + { + // The per-region flag is resolved once, here, and everything wire-facing reads it off this object. + var regionName = scene.RegionInfo?.RegionName; + var enabled = ServerSideBakingRegion.ResolveEnabled(ServerSideBakingEnabled, scene.Config, regionName); + var source = ServerSideBakingRegion.EnabledSource(scene.Config, regionName); + + // S12: one line per region, naming which config decided. This is what the flip verify reads - after the + // two global lines go in and a region section comes out, every region must say "on (global)". + m_log.LogInformation("[SSB]: region {Region}: server-side baking {State} ({Source})", + scene.Name, enabled ? "ON" : "off", source); + + var region = new ServerSideBakingRegion(enabled, new CofHandshake()); + lock (m_scenes) + { + m_scenes.Add(scene); + m_regions[scene] = region; + } + scene.RegisterModuleInterface(region); + + if (enabled) + { + // V3: the viewer POSTs here after every COF change. V1: bit 0 of RegionProtocols is what makes it do + // so, and LLClientView reads that off the same object. + scene.EventManager.OnRegisterCaps += (agentID, caps) => RegisterCaps(scene, agentID, caps); + scene.EventManager.OnMakeRootAgent += OnMakeRootAgent; + scene.EventManager.OnRemovePresence += region.Forget; + scene.EventManager.OnAvatarAppearanceChange += OnAvatarAppearanceChanged; + m_log.LogInformation( + "[SSB]: region {Region} has server-side baking ON: RegionProtocols bit 0 set, {Cap} advertised, " + + "login bake armed, appearances carry AppearanceData. Firestorm there will stop client-baking.", + scene.Name, CapName); + } + else + { + m_log.LogInformation("[SSB]: region {Region} has server-side baking off; the wire is unchanged there", scene.Name); + } + + scene.AddCommand( + "Users", this, "appearance serverbake", + "appearance serverbake ", + "Bake the avatar's current wearables on the server, store the bakes as assets, write them into the avatar's " + + "texture entry and send the appearance. (Not 'appearance rebake', which asks the viewer to re-upload its own bakes.)", + HandleServerBakeCommand); + } + + /// The backend is created on first use so a region without any bake never loads the compositor's resources. + private IBakeBackend Backend + { + get + { + if (m_backend is null) + { + lock (m_scenes) + { + if (m_backend is null) + { + m_compositor = new TexLayerCompositor(); + m_backend = new SkiaBakeBackend(m_compositor) { Quality = BakeQuality }; + } + } + } + return m_backend; + } + } + + // ------------------------------------------------------------------ IServerSideBaker + + public async Task BakeAsync(ScenePresence sp, BakeReason reason, CancellationToken ct) + { + ArgumentNullException.ThrowIfNull(sp); + if (sp.IsChildAgent) throw new InvalidOperationException($"{sp.Name} is a child agent here"); + var scene = sp.Scene; + var backend = Backend; + + var cofVersion = CofVersionOf(scene, sp.UUID); + var region = RegionOf(scene); + + // S8, step 0: refuse a bake whose wearable set has lost a body part since the last one. Composing from a + // set with no skin produces a valid-looking bake of nothing, and storing it supersedes - deletes - the + // good bakes it replaces, so the damage is not recoverable by baking again. Observed 2026-09-05: four + // unresolvable item ids emptied slots 1-4 and the CofChanged bake that followed stored 4 and superseded 4. + var refusal = region?.RefusalForBodyPartLoss(sp.UUID, sp.Appearance.Wearables); + if (refusal is not null) + { + m_log.LogWarning("[SSB]: bake for {Name} ({Agent}) reason={Reason} REFUSED: {Reason2}", + sp.Name, sp.UUID, reason, refusal); + return new BakeOutcome(sp.UUID, reason, Array.Empty(), 0); + } + + // steps 2, 4-6, scene-free; the ADR-004 index in the avatar service is read for the reuse decision and + // written back at the end of the run + var outcome = await Task.Run(() => BakeOrchestrator.Run(sp.UUID, reason, sp.Appearance.Wearables, sp.Appearance.VisualParams, sp.Appearance, + scene.AssetService, scene.AvatarService, backend, m_compositor, BakeSize, cofVersion, ct), ct).ConfigureAwait(false); + + // Record the bake before sending: SendAppearanceToAgentNF asks IServerSideBakingRegion for the version, + // and an appearance sent before the record would go out without its AppearanceData block. RecordBake + // ignores the call on a flag-off region, which is what keeps a console bake there off the wire. + if (outcome.Count(ChannelStatus.Baked) + outcome.Count(ChannelStatus.Reused) > 0) + { + region?.RecordBake(sp.UUID, cofVersion); + // The baseline for the next body-part check is what a bake actually succeeded from, never what one + // was refused for: recording a refused set would let the second attempt through unchallenged. + region?.RecordGoodBodyParts(sp.UUID, sp.Appearance.Wearables); + } + + // step 7: send to everyone in view and to self. A reused channel is sent exactly like a fresh one — the + // reason for the bake may be that nobody has seen it yet. + // + // No QueueAppearanceSave here, deliberately. A bake changes only the baked faces of the TextureEntry, and + // the avatar service does not persist those at all: AvatarData(AvatarAppearance) carries the serial, + // height, wearables, visual params and attachments and nothing else (IAvatarService.cs:142-189). What a + // save would do is destroy this bake's index, because AvatarService.SetAvatar deletes every row for the + // agent before rewriting those keys (AvatarService.cs:93). The bake index written above IS the + // persistence of the baked faces. + if (outcome.Count(ChannelStatus.Baked) + outcome.Count(ChannelStatus.Reused) > 0) + { + sp.SendAppearanceToAllOtherAgents(); + sp.SendAppearanceToAgent(sp); + } + + // step 8: one INFO line per bake, carrying the phase split (Ledger Q-10); the fidelity evidence at DEBUG + var t = outcome.Timings; + m_log.LogInformation("[SSB]: bake for {Name} ({Agent}) reason={Reason}: {Summary} in {Ms} ms [{Split}, other={Other} ms]", + sp.Name, sp.UUID, reason, Summarise(outcome), outcome.ElapsedMs, t.Summary, + Math.Max(0, outcome.ElapsedMs - (long)t.Accounted.TotalMilliseconds)); + if (m_log.IsEnabled(LogLevel.Debug)) + foreach (var c in outcome.Channels) + { + if (c.Fidelity.Refusals.Count > 0) m_log.LogDebug("[SSB]: {Channel} refusals: {R}", c.Channel, string.Join("; ", c.Fidelity.Refusals)); + if (c.Fidelity.MissingTextures.Count > 0) m_log.LogDebug("[SSB]: {Channel} missing textures: {T}", c.Channel, string.Join(", ", c.Fidelity.MissingTextures)); + if (c.Fidelity.UnsupportedLayers.Count > 0) m_log.LogDebug("[SSB]: {Channel} unsupported layers: {L}", c.Channel, string.Join(" | ", c.Fidelity.UnsupportedLayers)); + foreach (var note in c.Fidelity.Notes) m_log.LogDebug("[SSB]: {Channel} layer {Note}", c.Channel, note); + } + return outcome; + } + + private static string Summarise(BakeOutcome o) + => string.Join(", ", o.Channels.Where(c => c.Status != ChannelStatus.Skipped).Select(c => $"{c.Channel}={c.Status}{(c.Status == ChannelStatus.Failed ? $"({c.Reason})" : "")}")) + + $" [{o.Count(ChannelStatus.Skipped)} skipped]" + + $" reused {o.Count(ChannelStatus.Reused)}/{o.Count(ChannelStatus.Baked) + o.Count(ChannelStatus.Reused)}" + + (o.Superseded.Count > 0 ? $", superseded {o.Superseded.Count}" : "") + + (o.IndexWritten ? "" : ", index NOT written"); + + /// + /// The Current Outfit folder's version, stored with the bake as BakeCOFVersion (ADR-006: the sim reads + /// the COF folder's own Version and needs no AIS). Zero when there is no inventory service or no COF. + /// + private static int CofVersionOf(Scene scene, UUID agentId) => CofVersionOf(scene?.InventoryService, agentId); + + /// + /// The same read, over the service alone, so the identity with AIS's number is testable. AIS reports the COF + /// version from the very same field — AisMutation.ReportVersion writes (int)folder.Version of + /// the it gets back from the same , and + /// AisEnvelope.Category does the same for version. So the cof_version the viewer sends + /// back and the number read here are one quantity with one writer, the data layer's folder-version bump. + /// + public static int CofVersionOf(IInventoryService inventory, UUID agentId) + { + try + { + var cof = inventory?.GetFolderForType(agentId, FolderType.CurrentOutfit); + return cof?.Version ?? 0; + } + catch (Exception ex) + { + m_log.LogDebug(ex, "[SSB]: could not read the COF version for {Agent}", agentId); + return 0; + } + } + + // ------------------------------------------------------------------ S3: the cap and the login trigger + + /// + /// Register UpdateAvatarAppearance for one agent. Only ever called on a flag-on region: advertising it + /// where the flag is off would tell the viewer to expect server bakes that are not coming. + /// + private void RegisterCaps(Scene scene, UUID agentID, Caps caps) + { + string capPath = "/" + UUID.Random(); + caps.RegisterSimpleHandler(CapName, + new SimpleStreamHandler(capPath, (httpRequest, httpResponse) => HandleUpdateAvatarAppearance(httpRequest, httpResponse, scene, agentID))); + m_log.LogDebug("[SSB]: registered {Cap} at {Path} for agent {Agent} in {Region}", CapName, capPath, agentID, scene.Name); + } + + /// + /// The §4.3 handshake over HTTP. The viewer POSTs {cof_version:N} after every COF change (V3) and + /// expects {success, expected, error}. The decision itself is , which knows + /// nothing about HTTP; this method reads the body, reads the folder version fresh (ADR-006), and turns the + /// verdict into a bake and a response. + /// + private void HandleUpdateAvatarAppearance(IOSHttpRequest httpRequest, IOSHttpResponse httpResponse, Scene scene, UUID agentID) + { + if (httpRequest.HttpMethod != "POST") + { + httpResponse.StatusCode = (int)HttpStatusCode.NotFound; + return; + } + + var region = RegionOf(scene); + if (region is null || !region.ServerSideBakingEnabled) + { + WriteCapResult(httpResponse, false, -1, "server-side baking is not enabled on this region"); + return; + } + + int clientVersion; + try + { + var body = (OSDMap)OSDParser.DeserializeLLSDXml(httpRequest.InputStream); + clientVersion = body is not null && body.TryGetValue("cof_version", out var v) ? v.AsInteger() : -1; + } + catch (Exception) + { + httpResponse.StatusCode = (int)HttpStatusCode.BadRequest; + return; + } + + ScenePresence sp = scene.GetScenePresence(agentID); + if (sp is null || sp.IsChildAgent) + { + WriteCapResult(httpResponse, false, -1, "no root presence for this agent here"); + return; + } + + // ADR-006: read the folder's Version fresh, never a cached copy. AIS mutates the same field, so the + // number the viewer sends and the number read here are the same quantity (see AisMutation.ReportVersion). + int serverVersion = CofVersionOf(scene, agentID); + CofDecision decision = region.Handshake.Decide(agentID, clientVersion, serverVersion, () => CofVersionOf(scene, agentID), DateTime.UtcNow); + + if (decision.Verdict == CofVerdict.LivelockBake) + m_log.LogWarning("[SSB]: anti-livelock for {Name} ({Agent}) in {Region}: {Reason}", sp.Name, agentID, scene.Name, decision.Reason); + + if (!decision.Success) + { + m_log.LogDebug("[SSB]: {Cap} for {Name}: stale — {Reason}", CapName, sp.Name, decision.Reason); + WriteCapResult(httpResponse, false, decision.Version, null); + return; + } + + // Q-16: do NOT bake here. The POST is the viewer telling us its COF moved, and it arrives before the + // region has resolved the new items to asset ids. The POST arrives ahead of the appearance save that + // resolves them, and that save is up to DelayBeforeAppearanceSave (5 s) away — the only interval here + // that is measured. Baking now would composite an outfit whose wearables still carry UUID.Zero asset ids + // and store the result as if it were the new look. + // + // S10: but DO read the folder. On a bit-0 region this POST is the only notice the sim gets that the worn + // SET changed - the LL viewer's AgentIsNowWearing is a four-item dummy with no callers + // (llagentwearables.cpp:819-851), so nothing else turns a COF link into a wearable. Without this the save + // below persists the wearables the sim already had and the bake reuses every channel, which is exactly + // what happened on Ebony on 2026-09-06: two shirts linked in the COF, one shirt in the Avatars record. + ApplyCofToWearables(scene, sp); + + // Instead the cap joins the same path the legacy route already takes: queue an appearance save, and let + // the bake happen when that save completes (OnAvatarAppearanceChanged). Both signals therefore converge + // on one trigger and one ordering. The queue is keyed by agent, so a POST arriving alongside an + // AgentIsNowWearing costs nothing extra. + scene.AvatarFactory?.QueueAppearanceSave(agentID); + + WriteCapResult(httpResponse, true, decision.Version, null); + } + + /// + /// S10. Read the agent's Current Outfit Folder and put every wearable link it holds into the presence's + /// , in the viewer's order. The rules are + /// 's; this is only the read. + /// + /// + /// A link is kept when its target resolves in this region and is a wearable. A link whose target cannot be + /// read is dropped rather than guessed at, which is what makes the S8 rule hold: its type then looks + /// untouched by this read and keeps what the agent already wears, instead of being emptied by an inventory + /// lookup that failed. + /// + /// + private static void ApplyCofToWearables(Scene scene, ScenePresence sp) + { + var inventory = scene?.InventoryService; + if (inventory is null || sp is null) return; + + List links; + try + { + var cof = inventory.GetFolderForType(sp.UUID, FolderType.CurrentOutfit); + if (cof is null) return; + var content = inventory.GetFolderContent(sp.UUID, cof.ID); + if (content?.Items is null) return; + + links = new List(content.Items.Count); + var unresolved = 0; + foreach (var link in content.Items) + { + if (link is null || link.AssetType != (int)AssetType.Link || link.AssetID.IsZero()) continue; + var target = inventory.GetItem(sp.UUID, link.AssetID); + if (target is null) { unresolved++; continue; } + if (target.InvType != (int)InventoryType.Wearable) continue; // an attachment or a gesture link + var type = (int)(target.Flags & 0xff); + if (type < 0 || type >= AvatarWearable.MAX_WEARABLES) { unresolved++; continue; } + links.Add(new CofWearableLink(link.ID, link.Description, target.ID, type, target.AssetID)); + } + if (unresolved > 0) + m_log.LogWarning("[SSB]: {Count} COF link(s) for {Name} name an item this region cannot resolve as a wearable; their types keep what the agent already wears (S8)", unresolved, sp.Name); + } + catch (Exception ex) + { + m_log.LogWarning(ex, "[SSB]: could not read the COF for {Name}; the wearables are left as they are", sp.Name); + return; + } + + var derived = CofWearables.Derive(sp.Appearance?.Wearables, links, out var changed); + if (!changed) return; + + sp.Appearance.Wearables = derived; + m_log.LogDebug("[SSB]: {Name}'s worn set from the COF: {Summary}", sp.Name, DescribeWearables(derived)); + } + + /// "Shirt x2, Pants" — the worn types and how many of each, for the log. + private static string DescribeWearables(AvatarWearable[] wearables) + { + var parts = new List(); + for (var type = 0; type < wearables.Length; type++) + { + var count = wearables[type]?.Count ?? 0; + if (count == 0) continue; + parts.Add(count == 1 ? $"{(WearableType)type}" : $"{(WearableType)type} x{count}"); + } + return parts.Count == 0 ? "nothing" : string.Join(", ", parts); + } + + /// The V3 response body: success always, expected when there is a version to quote, error when there is something to say. + private static void WriteCapResult(IOSHttpResponse response, bool success, int expected, string error) + { + var map = new OSDMap { ["success"] = success }; + if (expected >= 0) map["expected"] = expected; + if (!string.IsNullOrEmpty(error)) map["error"] = error; + response.RawBuffer = Util.UTF8NBGetbytes(OSDParser.SerializeLLSDXmlString(map)); + response.StatusCode = (int)HttpStatusCode.OK; + } + + /// + /// The change trigger (Design Brief §4.6, Ledger Q-16). Fires when the region has finished applying an + /// appearance change and persisted it — AvatarFactoryModule.SaveAppearance raises it right after + /// SetAppearanceAssets has resolved every worn item to its asset id and the avatar service has stored + /// the result. Baking any earlier composites an outfit whose wearables are still UUID.Zero. + /// + /// + /// Both signal paths reach here. The legacy route arrives as AgentIsNowWearing and queues a save + /// in Client_OnAvatarNowWearing (AvatarFactoryModule.cs:1292); the cap route now queues one too + /// (see ). Attachment changes and the login/teleport cache check + /// also queue saves, so this fires for those as well — deliberately. A spurious trigger costs one hash check + /// per channel and re-sends the appearance; a missed one leaves the avatar wrong until relog, so the bias is + /// towards triggering (S5 brief). + /// + /// + /// Runs on the appearance-save thread pool thread. The bake goes to its own work item so a slow bake + /// cannot hold up the rest of the save queue. + /// + private void OnAvatarAppearanceChanged(ScenePresence sp) + { + if (sp is null || sp.IsChildAgent || sp.IsNPC) return; + var scene = sp.Scene; + var region = RegionOf(scene); + if (region is not { ServerSideBakingEnabled: true }) return; + + if (!region.TryClaimChangeBake(sp.UUID, DateTime.UtcNow)) + { + m_log.LogDebug("[SSB]: change bake for {Name} coalesced into the one just done", sp.Name); + return; + } + + Util.FireAndForget(_ => + { + try + { + // BakeAsync sends the appearance itself when anything is live, reused included, so a change whose + // hashes all match still reaches the viewer — which it must, because the viewer is waiting for an + // AvatarAppearance it can accept and will not re-request one. + BakeAsync(sp, BakeReason.CofChanged, CancellationToken.None).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + m_log.LogError(ex, "[SSB]: change bake for {Name} threw", sp.Name); + } + }, null, "SSB change bake"); + } + + /// + /// Login-time bake (Design Brief §4.2 step 1). Only armed on flag-on regions. It runs off the login thread: + /// a bake is ~2.8 s cold and must never sit in the path that makes the agent root. + /// + /// A warm agent — one whose stored bakes still match its wearables — costs about 70 ms and stores + /// nothing, so this is cheap for everyone but a first login or an outfit change (Ledger Q-10). + /// + private void OnMakeRootAgent(ScenePresence sp) + { + if (sp is null || sp.IsChildAgent || sp.IsNPC) return; + var scene = sp.Scene; + if (RegionOf(scene) is not { ServerSideBakingEnabled: true }) return; + + Util.FireAndForget(_ => + { + try + { + BakeAsync(sp, BakeReason.Login, CancellationToken.None).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + m_log.LogError(ex, "[SSB]: login bake for {Name} threw", sp.Name); + } + }, null, "SSB login bake"); + } + + // ------------------------------------------------------------------ console + + private void HandleServerBakeCommand(string module, string[] cmd) + { + if (cmd.Length != 4) + { + MainConsole.Instance.Output("Usage: appearance serverbake "); + return; + } + string firstname = cmd[2], lastname = cmd[3]; + List scenes; + lock (m_scenes) scenes = new List(m_scenes); + var found = false; + foreach (var scene in scenes) + { + ScenePresence sp = scene.GetScenePresence(firstname, lastname); + if (sp is null || sp.IsChildAgent) continue; + found = true; + BakeOutcome outcome; + try { outcome = BakeAsync(sp, BakeReason.Console, CancellationToken.None).GetAwaiter().GetResult(); } + catch (Exception ex) + { + MainConsole.Instance.Output("Server bake for {0} in {1} threw: {2}", sp.Name, scene.RegionInfo.RegionName, ex.Message); + m_log.LogError(ex, "[SSB]: console bake for {Name} threw", sp.Name); + continue; + } + var sb = new StringBuilder(); + sb.AppendLine($"Server bake for {sp.Name} in {scene.RegionInfo.RegionName}: {outcome.ElapsedMs} ms, size {BakeSize}"); + sb.AppendLine($"time split: {outcome.Timings.Summary}, other {Math.Max(0, outcome.ElapsedMs - (long)outcome.Timings.Accounted.TotalMilliseconds)}"); + sb.AppendLine($"{"channel",-8} {"face",4} {"status",-8} {"asset",-36} detail"); + foreach (var c in outcome.Channels) + { + var detail = c.Status switch + { + ChannelStatus.Reused => $"hash {c.InputHash[..Math.Min(12, c.InputHash.Length)]}; inputs unchanged, not recomputed", + ChannelStatus.Baked => $"hash {c.InputHash[..Math.Min(12, c.InputHash.Length)]}" + + (c.Fidelity.MissingTextures.Count > 0 ? $"; missing textures {c.Fidelity.MissingTextures.Count}" : "") + + (c.Fidelity.UnsupportedLayers.Count > 0 ? $"; unsupported layers {c.Fidelity.UnsupportedLayers.Count}" : ""), + _ => c.Reason, + }; + sb.AppendLine($"{c.Channel,-8} {BakeOrchestrator.FaceOf(c.Channel),4} {c.Status,-8} {(c.AssetId.IsZero() ? "-" : c.AssetId.ToString()),-36} {detail}"); + } + sb.AppendLine($"reused {outcome.Count(ChannelStatus.Reused)} of {outcome.Count(ChannelStatus.Baked) + outcome.Count(ChannelStatus.Reused)} live channels; " + + $"superseded {outcome.Superseded.Count} old asset(s); index {(outcome.IndexWritten ? "written" : "NOT written")}, COF version {CofVersionOf(scene, sp.UUID)}"); + foreach (var note in outcome.Notes) sb.AppendLine($" note: {note}"); + MainConsole.Instance.Output(sb.ToString().TrimEnd()); + } + if (!found) MainConsole.Instance.Output("No root agent named {0} {1} in any region here", firstname, lastname); + } +} diff --git a/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/ServerSideBakingRegion.cs b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/ServerSideBakingRegion.cs new file mode 100644 index 00000000000..0c2889c91d6 --- /dev/null +++ b/Source/OpenSim.Region.OptionalModules/Avatar/ServerSideBaking/ServerSideBakingRegion.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using OpenSim.Framework; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Region.Framework.Interfaces; + +namespace OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; + +/// +/// One region's server-side-baking state: whether the flag resolved on here, which agents this sim has baked and +/// at what COF version, and the §4.3 handshake's counters. Registered on the scene as +/// so LLClientView and ScenePresence can read the two wire +/// facts without referencing this module. +/// +public sealed class ServerSideBakingRegion : IServerSideBakingRegion +{ + private readonly ConcurrentDictionary m_bakedCof = new(); + + public ServerSideBakingRegion(bool enabled, CofHandshake handshake) + { + ServerSideBakingEnabled = enabled; + Handshake = handshake; + } + + /// + public bool ServerSideBakingEnabled { get; } + + /// This region's copy of the §4.3 handshake, with its own per-agent mismatch counters. + public CofHandshake Handshake { get; } + + /// + public int BakedCofVersion(UUID agentId) => m_bakedCof.TryGetValue(agentId, out var v) ? v : -1; + + /// + /// Record that this sim applied a bake to the agent at the given COF version, which is what makes the + /// appearance carry an AppearanceData block from now on (V4/V5). + /// + /// Only ever called on a flag-on region. On a flag-off region a console bake still writes faces and + /// sends the appearance, but it must not change the wire — Firestorm is client-baking there and expects the + /// packet it has always had (ADR-001). + /// + public void RecordBake(UUID agentId, int cofVersion) + { + if (!ServerSideBakingEnabled || cofVersion < 0) return; + m_bakedCof[agentId] = cofVersion; + } + + /// Forget an agent on close, so a returning agent is not credited with a bake this sim no longer knows about. + public void Forget(UUID agentId) + { + m_bakedCof.TryRemove(agentId, out _); + Handshake.Clear(agentId); + m_lastChangeBake.TryRemove(agentId, out _); + m_lastGoodBodyParts.TryRemove(agentId, out _); + } + + // ------------------------------------------------------------------ S8: the body-part guard + + /// + /// The four body-part slots. A body part is not something a resident takes off: a viewer will not let you + /// remove your skin, and every avatar has all four from creation. So a wearable set that HAD one of these and + /// now does not is, in practice, always the result of a failed resolution rather than an outfit change - which + /// is exactly what happened on 2026-09-05, when four unresolvable item ids emptied slots 1-4 and the bake that + /// followed superseded the four good bakes with "no Skin worn / no Eyes worn / no Hair worn". + /// + public static readonly int[] BodyPartSlots = + { + (int)WearableType.Shape, (int)WearableType.Skin, (int)WearableType.Hair, (int)WearableType.Eyes, + }; + + private readonly ConcurrentDictionary m_lastGoodBodyParts = new(); + + /// Which of the four body-part slots this wearable set actually has something in. + public static int[] BodyPartsPresent(AvatarWearable[] wearables) + { + var present = new List(BodyPartSlots.Length); + foreach (var slot in BodyPartSlots) + if (wearables is not null && slot < wearables.Length && wearables[slot] is { Count: > 0 }) + present.Add(slot); + return present.ToArray(); + } + + /// + /// Whether this bake must be refused because a body-part slot the agent had is now empty. Returns null to + /// proceed. The check is against the last set this sim actually baked from, so the first bake of a session + /// always proceeds - there is nothing to compare with, and refusing would leave a new arrival unbaked. + /// + /// Nothing is recorded here. A refusal must not become the new baseline, or the second attempt would + /// see no loss and go through; is called only after a bake succeeds. + /// + public string RefusalForBodyPartLoss(UUID agentId, AvatarWearable[] wearables) + { + if (!m_lastGoodBodyParts.TryGetValue(agentId, out var before)) return null; + + var now = BodyPartsPresent(wearables); + var lost = new List(); + foreach (var slot in before) + if (Array.IndexOf(now, slot) < 0) + lost.Add($"{(WearableType)slot} (slot {slot})"); + + return lost.Count == 0 + ? null + : $"body-part slots lost since the last bake: {string.Join(", ", lost)}. A body part is never removed by a resident, " + + "so this is a resolution failure upstream of the bake, not an outfit change; nothing is baked and nothing is superseded."; + } + + /// Remember what a successful bake was made from, as the baseline for the next one. + public void RecordGoodBodyParts(UUID agentId, AvatarWearable[] wearables) + => m_lastGoodBodyParts[agentId] = BodyPartsPresent(wearables); + + private readonly ConcurrentDictionary m_lastChangeBake = new(); + + /// + /// How close together two change-triggered bakes for one agent have to be before the second is treated as + /// part of the same outfit change. + /// + /// + /// The real coalescing is done upstream: every route into a rebake goes through + /// AvatarFactoryModule.QueueAppearanceSave, whose queue is keyed by agent and drains on a timer + /// (DelayBeforeAppearanceSave, default 5 s), so signals arriving within one drain already collapse + /// into one save and one event. This window is the second guard, for signals that land either side of a drain + /// boundary. It is sized against the one interval that is actually measured — the 5 s save delay — and is + /// deliberately shorter than it, so it cannot suppress a genuinely distinct change that completed its own + /// save cycle. The spread between the two signals of a single change has not been measured (Ledger + /// Q-6); 2 s is an estimate comfortably above any plausible value and comfortably below the save delay, and + /// the S5 live verify is what will replace the estimate. + /// + /// + public TimeSpan ChangeDebounce { get; init; } = TimeSpan.FromSeconds(2); + + /// + /// Claim the right to bake this agent for a change at , or report that a bake for + /// the same change has just happened. Atomic, because appearance saves run on the thread pool and two can + /// land at once. + /// + public bool TryClaimChangeBake(UUID agentId, DateTime nowUtc) + { + // A flag-off region never claims, so the change trigger cannot fire there even if something subscribes + // it by mistake. Nothing about the wire changes where the flag is off (ADR-001). + if (!ServerSideBakingEnabled) return false; + + bool claimed = false; + m_lastChangeBake.AddOrUpdate( + agentId, + _ => { claimed = true; return nowUtc; }, + (_, previous) => + { + if (nowUtc - previous < ChangeDebounce) return previous; + claimed = true; + return nowUtc; + }); + return claimed; + } + + /// + /// Whether the flag is on for one region. The simulator-wide [Appearance] ServerSideBaking is the + /// default and a [<Region Name>] section may override it with the same key — the per-region idiom + /// AISv3Module.ResolveEnabled uses for AIS_Enabled, which in turn follows + /// AutoBackupModule. Static and free of Scene so it can be tested with a plain config source. + /// + /// The reason it is per region is the same as AIS's: flipping it hands the LL viewer's whole appearance + /// path to this code, and Firestorm on that region stops client-baking the moment bit 0 appears in the + /// handshake. It has to be possible to try it on exactly one region (Design Brief §4.5). + /// + public static bool ResolveEnabled(bool simulatorDefault, IConfigSource sceneConfig, string regionName) + { + if (sceneConfig is null || string.IsNullOrEmpty(regionName)) return simulatorDefault; + IConfig regionConfig = sceneConfig.Configs[regionName]; + return regionConfig is null ? simulatorDefault : regionConfig.GetBoolean("ServerSideBaking", simulatorDefault); + } + + /// + /// S12: which config decided this region's flag - "region section" when the region's own section carries + /// a ServerSideBaking key, "global" otherwise. Purely for the startup line: an operator who has + /// just added the two global lines needs the log to say the global path is what turned the region on, and an + /// operator debugging one wrong region needs to know a section is overriding them. + /// + public static string EnabledSource(IConfigSource sceneConfig, string regionName) + { + if (sceneConfig is null || string.IsNullOrEmpty(regionName)) return GlobalSource; + IConfig regionConfig = sceneConfig.Configs[regionName]; + return regionConfig is not null && regionConfig.Contains("ServerSideBaking") ? RegionSource : GlobalSource; + } + + /// The two answers gives, named so the log line and its test cannot drift. + public const string GlobalSource = "global"; + public const string RegionSource = "region section"; +} diff --git a/Source/OpenSim.Region.OptionalModules/OpenSim.Region.OptionalModules.csproj b/Source/OpenSim.Region.OptionalModules/OpenSim.Region.OptionalModules.csproj index 7c69423c6be..6b6309a4b6e 100644 --- a/Source/OpenSim.Region.OptionalModules/OpenSim.Region.OptionalModules.csproj +++ b/Source/OpenSim.Region.OptionalModules/OpenSim.Region.OptionalModules.csproj @@ -31,6 +31,7 @@ + diff --git a/Source/OpenSim.Region.OptionalModules/PluginRegistration.cs b/Source/OpenSim.Region.OptionalModules/PluginRegistration.cs index 8176943c782..63348c3f1fc 100644 --- a/Source/OpenSim.Region.OptionalModules/PluginRegistration.cs +++ b/Source/OpenSim.Region.OptionalModules/PluginRegistration.cs @@ -40,6 +40,7 @@ public void RegisterPlugins(PluginRegistry registry) RegisterByName(registry, "/OpenSim/RegionModules", "AssetInfoModule", "OpenSim.Region.OptionalModules.Asset.AssetInfoModule", "AssetInfoModule"); RegisterByName(registry, "/OpenSim/RegionModules", "AnimationsCommandModule", "OpenSim.Region.OptionalModules.Avatar.Animations.AnimationsCommandModule", "AnimationsCommandModule"); RegisterByName(registry, "/OpenSim/RegionModules", "AppearanceInfoModule", "OpenSim.Region.OptionalModules.Avatar.Appearance.AppearanceInfoModule", "AppearanceInfoModule"); + RegisterByName(registry, "/OpenSim/RegionModules", "ServerSideBakingModule", "OpenSim.Region.OptionalModules.Avatar.ServerSideBaking.ServerSideBakingModule", "ServerSideBakingModule"); RegisterByName(registry, "/OpenSim/RegionModules", "IRCBridgeModule", "OpenSim.Region.OptionalModules.Avatar.Chat.IRCBridgeModule", "IRCBridgeModule"); RegisterByName(registry, "/OpenSim/RegionModules", "ConciergeModule", "OpenSim.Region.OptionalModules.Avatar.Concierge.ConciergeModule", "ConciergeModule"); RegisterByName(registry, "/OpenSim/RegionModules", "FriendsCommandModule", "OpenSim.Region.OptionalModules.Avatar.Friends.FriendsCommandsModule", "FriendsCommandModule"); diff --git a/Source/OpenSim.Region.OptionalModules/World/NPC/NPCAvatar.cs b/Source/OpenSim.Region.OptionalModules/World/NPC/NPCAvatar.cs index c5f2233659c..4fcd79eaad5 100644 --- a/Source/OpenSim.Region.OptionalModules/World/NPC/NPCAvatar.cs +++ b/Source/OpenSim.Region.OptionalModules/World/NPC/NPCAvatar.cs @@ -661,6 +661,10 @@ public virtual void SendWearables(AvatarWearable[] wearables, int serial) { } + public virtual void SendAppearance(UUID agentID, byte[] visualParams, byte[] textureEntry, float hover, int cofVersion) + { + } + public virtual void SendAppearance(UUID agentID, byte[] visualParams, byte[] textureEntry, float hover) { } diff --git a/Source/OpenSim.Server.GridServer/AppData/Robust.ini.example b/Source/OpenSim.Server.GridServer/AppData/Robust.ini.example index 279b38955f8..65601533854 100644 --- a/Source/OpenSim.Server.GridServer/AppData/Robust.ini.example +++ b/Source/OpenSim.Server.GridServer/AppData/Robust.ini.example @@ -127,6 +127,11 @@ ;; Uncomment to provide bakes caching ; BakedTextureService = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:XBakesConnector" + ;; Server-side baking read path (Docs/feature/ssb-appearance, ADR-002). Uncomment together with the + ;; [AppearanceService] section below and [LoginService] AgentAppearanceServiceURL. PUBLIC port: the viewer + ;; fetches from it directly. Only needed where a region sets [Appearance] ServerSideBaking = true. + ; AppearanceServiceConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:AppearanceServerConnector" + ;; Uncomment for UserProfiles see [UserProfilesService] to configure... ; UserProfilesServiceConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:UserProfilesConnector" @@ -494,6 +499,14 @@ ; Url to search service ; SearchURL = "${Const|BaseURL}:${Const|PublicPort}/"; + ;; Server-side baking (ADR-002, viewer contract V6). Where viewers fetch other avatars' baked textures. + ;; Must point at the AppearanceServiceConnector's port and end with "/". Leave it unset and the login + ;; response omits agent_appearance_service entirely, which is correct for a grid with no such service -- + ;; the viewer then keeps compositing locally. Set it only once the connector is running, because on a + ;; bit-0 region a viewer with no service URL never textures anyone. + ; AgentAppearanceServiceURL = "${Const|BaseURL}:${Const|PublicPort}/" + + ; For V3 destination guide ; DestinationGuide = "${Const|BaseURL}/guide" @@ -653,6 +666,17 @@ AuthenticationServiceModule = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" +;; Server-side baking: serves GET texture/// from the bakes a simulator stored, resolving +;; the channel through the avatar service's Bake: index. Inert unless AppearanceServiceConnector is +;; uncommented in [ServiceList] above. The token is a name -- head, upper, lower, eyes, skirt, hair, +;; leftarm, leftleg, aux1, aux2, aux3 -- which is what the viewer sends (llvoavatar.cpp getImageURL). +;; Whatever URL you put in [LoginService] AgentAppearanceServiceURL must reach this connector's port and end +;; with a "/", because the viewer appends "texture/..." to it with no separator. +[AppearanceService] + LocalServiceModule = "OpenSim.Services.AvatarService.dll:AppearanceService" + AvatarService = "OpenSim.Services.AvatarService.dll:AvatarService" + AssetService = "OpenSim.Services.AssetService.dll:AssetService" + [BakedTextureService] LocalServiceModule = "OpenSim.Server.Handlers.dll:XBakes" ;; This directory must be writable by the user ROBUST runs as. It will be created automatically. diff --git a/Source/OpenSim.Server.Handlers/Appearance/AppearanceServerConnector.cs b/Source/OpenSim.Server.Handlers/Appearance/AppearanceServerConnector.cs new file mode 100644 index 00000000000..018ceb77857 --- /dev/null +++ b/Source/OpenSim.Server.Handlers/Appearance/AppearanceServerConnector.cs @@ -0,0 +1,128 @@ +using System; +using System.Net; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Framework.ServiceAuth; +using OpenSim.Framework.Servers.HttpServer; +using OpenSim.Server.Base; +using OpenSim.Server.Handlers.Base; +using OpenSim.Services.Interfaces; + +namespace OpenSim.Server.Handlers.Appearance; + +/// +/// Robust connector for the agent_appearance_service read path (Design Brief C4, ADR-002), in the shape +/// XBakesConnector established: a that loads its +/// LocalServiceModule from its own config section and registers one +/// on the server it is given. +/// +/// Robust.HG.ini.example / Robust.ini.example: +/// +/// [ServiceList] +/// AppearanceServiceConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:AppearanceServerConnector" +/// +/// [AppearanceService] +/// LocalServiceModule = "OpenSim.Services.AvatarService.dll:AppearanceService" +/// AvatarService = "OpenSim.Services.AvatarService.dll:AvatarService" +/// AssetService = "OpenSim.Services.AssetService.dll:AssetService" +/// +/// +/// +/// It goes on the public port because the viewer fetches from it directly, unlike most Robust connectors. +/// The same class registers on a standalone's HTTP server, which hosts Robust connectors in process already +/// (ADR-002). +/// +/// +public class AppearanceServerConnector : ServiceConnector +{ + private string m_ConfigName = "AppearanceService"; + + public AppearanceServerConnector(IConfigSource config, IHttpServer server, string configName) : + base(config, server, configName) + { + if (configName != string.Empty) + m_ConfigName = configName; + + IConfig serverConfig = config.Configs[m_ConfigName]; + if (serverConfig is null) + throw new Exception($"No section '{m_ConfigName}' in config file"); + + string serviceName = serverConfig.GetString("LocalServiceModule", string.Empty); + if (string.IsNullOrWhiteSpace(serviceName)) + throw new Exception($"No LocalServiceModule in [{m_ConfigName}]"); + + object[] args = new object[] { config }; + IAppearanceService service = ServerUtils.LoadPlugin(serviceName, args); + if (service is null) + throw new Exception($"Could not load an IAppearanceService from '{serviceName}'"); + + IServiceAuth auth = ServiceAuth.Create(config, m_ConfigName); + + server.AddSimpleStreamHandler(new AppearanceServerHandler(service, auth), true); + } +} + +/// +/// GET /texture/<agent>/<channel>/<uuid>. +/// +/// +/// The path is exactly what LLVOAvatar::getImageURL builds — appearance_service_url with +/// "texture/" appended and no separator (indra/newview/llvoavatar.cpp:5912) — so the configured +/// AgentAppearanceServiceURL must end in / and this handler sits at /texture. The channel is +/// a name, not a number; see for the eleven tokens and where they come from. +/// +/// +/// +/// Everything that is not a hit is 404: an unknown channel token, an agent with no bake index, an index +/// whose UUID disagrees with the one in the path, an index pointing at an asset the asset service has lost. The +/// one thing this must never do is answer a mismatched request with whatever it does have. +/// +/// +public class AppearanceServerHandler : SimpleStreamHandler +{ + private readonly IAppearanceService m_service; + + public AppearanceServerHandler(IAppearanceService service, IServiceAuth auth) : + base("/texture", auth) + { + m_service = service; + } + + protected override void ProcessRequest(IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) + { + httpRequest.InputStream?.Dispose(); + + if (m_service is null) + { + httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError; + return; + } + + if (httpRequest.HttpMethod != "GET") + { + httpResponse.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + return; + } + + string[] p = GetParam(httpRequest.UriPath).Split(new char[] { '/', '?', '&' }, StringSplitOptions.RemoveEmptyEntries); + if (p.Length < 3 + || !UUID.TryParse(p[0], out UUID agentId) + || !UUID.TryParse(p[2], out UUID expectedAssetId)) + { + httpResponse.StatusCode = (int)HttpStatusCode.NotFound; + return; + } + + AssetBase asset = m_service.GetBake(agentId, p[1], expectedAssetId); + if (asset?.Data is not { Length: > 0 }) + { + httpResponse.StatusCode = (int)HttpStatusCode.NotFound; + return; + } + + httpResponse.ContentType = "image/x-j2c"; + httpResponse.RawBuffer = asset.Data; + httpResponse.StatusCode = (int)HttpStatusCode.OK; + } +} diff --git a/Source/OpenSim.Server.RegionServer/AppData/OpenSim.ini.example b/Source/OpenSim.Server.RegionServer/AppData/OpenSim.ini.example index 901a6b9115f..5e0d7109f22 100644 --- a/Source/OpenSim.Server.RegionServer/AppData/OpenSim.ini.example +++ b/Source/OpenSim.Server.RegionServer/AppData/OpenSim.ini.example @@ -1297,6 +1297,28 @@ ;; AdminsOnly — grid administrators/gods only ; ExperienceCreators = EstateManagersAndRegionOwners +[AIS] + ;; AIS v3 (InventoryAPIv3 / LibraryAPIv3) for the whole simulator. See [AIS] in OpenSimDefaults.ini for what + ;; enabling it means - it routes ALL of an LL viewer's inventory traffic through it. One line, no region named: + ;; + ; Enabled = true + ;; + ;; A single region opts OUT with AIS_Enabled = false in its own [] section. A region section is + ;; never the way to opt in. + + +[Appearance] + ;; Server-side baking for the whole simulator. See [Appearance] in OpenSimDefaults.ini. One line, no region + ;; named: + ;; + ; ServerSideBaking = true + ;; + ;; A single region opts OUT with ServerSideBaking = false in its own [] section. + ;; + ;; Those two lines - [AIS] Enabled and [Appearance] ServerSideBaking - are the whole switch. Each region logs + ;; one line at INFO when it loads saying which config decided, "(global)" or "(region section)". + + [Architecture] ;# {Include-Architecture} {} {Choose one of the following architectures} {config-include/Standalone.ini config-include/StandaloneHypergrid.ini config-include/Grid.ini config-include/GridHypergrid.ini} config-include/Standalone.ini ;; Uncomment one of the following includes as required. For instance, to create a standalone OpenSim, diff --git a/Source/OpenSim.Server.RegionServer/AppData/OpenSimDefaults.ini b/Source/OpenSim.Server.RegionServer/AppData/OpenSimDefaults.ini index 12e48eb9cb1..b32b2689f4d 100644 --- a/Source/OpenSim.Server.RegionServer/AppData/OpenSimDefaults.ini +++ b/Source/OpenSim.Server.RegionServer/AppData/OpenSimDefaults.ini @@ -800,6 +800,40 @@ ; ; SupportViewerObjectsCache = true +[AIS] + ;# {Enabled} {} {Advertise InventoryAPIv3 and LibraryAPIv3 (AIS v3) to viewers?} {true false} false + ;; + ;; READ THIS BEFORE SETTING IT TRUE. + ;; + ;; Enabling AIS routes ALL of an LL viewer's inventory traffic through it: fetching your inventory, + ;; opening folders, renaming, deleting an item, deleting a folder, emptying Trash, wearing and removing + ;; clothing (outfit "slam"), and creating folders and links. For the mutations the LL viewer has NO + ;; fallback path - if AIS answers an error, the operation simply does not happen and the viewer logs + ;; "Tried to use inventory without AIS API". See Docs/feature/ais-v3/AIS-V3-SPEC.md section 1g. + ;; + ;; TO TURN IT ON FOR THE WHOLE SIMULATOR, set this one line and name no region: + ;; + ;; [AIS] + ;; Enabled = true + ;; + ;; That is the switch. A [] section is an OPTIONAL OVERRIDE and never the way to opt in - a + ;; single region opts OUT of a simulator-wide true with: + ;; + ;; [My Region] + ;; AIS_Enabled = false + ;; + ;; (The per-region key is AIS_Enabled, not Enabled: a region section holds settings for many modules, so the + ;; key has to say which one it belongs to. The global key sits in this section and needs no prefix.) + ;; + ;; Every region logs one line at INFO when it loads - "region : AIS v3 ON (global)" or "(region + ;; section)" - naming which config decided, so the console says what is on and why. + ;; + ;; Known gaps when enabled (Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md): creating inventory ITEMS answers + ;; 501 (the viewer's own path for it is disabled and expects the server to create the asset); folder + ;; deletion is refused by the hypergrid inventory services; folder thumbnails and the favorite flag are + ;; accepted and silently dropped, this tree having no column for either. + Enabled = false + [ClientStack.LindenCaps] ;; Long list of capabilities taken from ;; http://wiki.secondlife.com/wiki/Current_Sim_Capabilities @@ -951,6 +985,36 @@ ; by the very chatty dialog that sets appearance when an avatar ; logs in or teleports into a region; values are in seconds DelayBeforeAppearanceSave = 5 + + ; Server-side baking (Docs/feature/ssb-appearance). + ; + ; ServerSideBaking turns on the whole viewer-facing path: RegionProtocols bit 0 in the RegionHandshake, the + ; UpdateAvatarAppearance cap, an AppearanceData block on appearances this simulator baked, and a bake when an + ; agent becomes a root agent. With it off, nothing about the wire changes. Turning it on makes Firestorm stop + ; baking client-side and use the simulator's bakes instead, so leave it false unless you mean it. + ; + ; TO TURN IT ON FOR THE WHOLE SIMULATOR, set this one line and name no region: + ; + ; [Appearance] + ; ServerSideBaking = true + ; + ; That is the switch. A [] section is an OPTIONAL OVERRIDE and never the way to opt in - a + ; single region opts OUT of a simulator-wide true with: + ; + ; [My Region] + ; ServerSideBaking = false + ; + ; Every region logs one line at INFO when it loads - "region : server-side baking ON (global)" or + ; "(region section)" - naming which config decided. + ; + ; With [AIS] Enabled = true, those are the two lines that turn the whole grid on. + ; + ; BakeSize is 512, 1024 or 2048; BakeQuality is the JPEG 2000 quality (0..1). Changing BakeSize invalidates + ; every stored bake, which is deliberate. The console command "appearance serverbake " exists + ; on every region whatever the flag says. + ServerSideBaking = false + BakeSize = 1024 + BakeQuality = 0.85 DelayBeforeAppearanceSend = 2 ; If true, avatar appearance information is resent to other avatars in the simulator every 60 seconds. diff --git a/Source/OpenSim.Server.RegionServer/AppData/config-include/StandaloneCommon.ini.example b/Source/OpenSim.Server.RegionServer/AppData/config-include/StandaloneCommon.ini.example index 524021f136a..257a86a24a1 100644 --- a/Source/OpenSim.Server.RegionServer/AppData/config-include/StandaloneCommon.ini.example +++ b/Source/OpenSim.Server.RegionServer/AppData/config-include/StandaloneCommon.ini.example @@ -123,6 +123,13 @@ ;LibraryName = "My World's Library" [LoginService] + + ;; Server-side baking (ADR-002, viewer contract V6). Where viewers fetch other avatars' baked textures. + ;; Must point at the AppearanceServiceConnector's port and end with "/". Leave it unset and the login + ;; response omits agent_appearance_service entirely, which is correct for a grid with no such service -- + ;; the viewer then keeps compositing locally. Set it only once the connector is running, because on a + ;; bit-0 region a viewer with no service URL never textures anyone. + ; AgentAppearanceServiceURL = "${Const|BaseURL}:${Const|PublicPort}/" WelcomeMessage = "Welcome, Avatar!" SRV_HomeURI = "${Hypergrid|HomeURI}" diff --git a/Source/OpenSim.Services.AvatarService/AppearanceService.cs b/Source/OpenSim.Services.AvatarService/AppearanceService.cs new file mode 100644 index 00000000000..32c726c530f --- /dev/null +++ b/Source/OpenSim.Services.AvatarService/AppearanceService.cs @@ -0,0 +1,133 @@ +using System; +using System.Reflection; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Server.Base; +using OpenSim.Services.Base; +using OpenSim.Services.Interfaces; +using Microsoft.Extensions.Logging; + +namespace OpenSim.Services.AvatarService; + +/// +/// The agent_appearance_service read path (Design Brief C4, ADR-002): resolve a bake channel to the asset +/// the sim stored for it and hand that asset back. +/// +/// +/// It owns no storage. The channel → asset mapping is S2's ADR-004 index in the avatar service's key/value table +/// (Bake:<channel>), and the bytes are an ordinary texture asset. Both are read through the service +/// interfaces, so this works identically whether Robust holds them locally or the deployment is a standalone. +/// +/// +/// +/// The UUID in the path is a check, not a lookup key. The viewer sends the asset id it took from the +/// avatar's TextureEntry. If the index no longer agrees — the agent re-baked and superseded that asset between +/// the appearance the viewer holds and this request — the answer is 404, so the viewer re-reads the appearance +/// and asks again. Serving whatever the index currently holds would texture the avatar with a bake the viewer +/// did not ask for, and serving the requested id blindly would let any caller pull any asset through this route. +/// +/// +public class AppearanceService : ServiceBase, IAppearanceService +{ + private static readonly ILogger m_log = LoggerProvider.CreateLogger(MethodBase.GetCurrentMethod().DeclaringType); + + public const string ConfigName = "AppearanceService"; + + private readonly IAvatarService m_AvatarService; + private readonly IAssetService m_AssetService; + + public AppearanceService(IConfigSource config) : this(config, ConfigName) + { + } + + public AppearanceService(IConfigSource config, string configName) : base(config) + { + IConfig serviceConfig = config.Configs[string.IsNullOrEmpty(configName) ? ConfigName : configName]; + if (serviceConfig is null) + throw new Exception($"No section '{configName}' in config file"); + + string avatarService = serviceConfig.GetString("AvatarService", string.Empty); + string assetService = serviceConfig.GetString("AssetService", string.Empty); + + if (string.IsNullOrWhiteSpace(avatarService)) + throw new Exception($"[{configName}] AvatarService not set; the appearance service has no bake index to read"); + if (string.IsNullOrWhiteSpace(assetService)) + throw new Exception($"[{configName}] AssetService not set; the appearance service has no assets to serve"); + + object[] args = new object[] { config }; + m_AvatarService = ServerUtils.LoadPlugin(avatarService, args); + m_AssetService = ServerUtils.LoadPlugin(assetService, args); + + if (m_AvatarService is null) + throw new Exception($"Could not load AvatarService '{avatarService}' for the appearance service"); + if (m_AssetService is null) + throw new Exception($"Could not load AssetService '{assetService}' for the appearance service"); + + m_log.LogInformation("[APPEARANCE SERVICE]: serving baked textures for {Count} channels", AppearanceChannels.Tokens.Count); + } + + /// Test seam: the service over supplied dependencies, bypassing plugin loading. + protected AppearanceService(IConfigSource config, IAvatarService avatarService, IAssetService assetService) : base(config) + { + m_AvatarService = avatarService ?? throw new ArgumentNullException(nameof(avatarService)); + m_AssetService = assetService ?? throw new ArgumentNullException(nameof(assetService)); + } + + /// + public AssetBase GetBake(UUID agentId, string channelToken, UUID expectedAssetId) + { + if (agentId.IsZero()) return null; + + string key = AppearanceChannels.BakeKeyFor(channelToken); + if (key is null) + { + m_log.LogDebug("[APPEARANCE SERVICE]: {Agent} asked for unknown channel '{Channel}'", agentId, channelToken); + return null; + } + + AvatarData avatar; + try { avatar = m_AvatarService.GetAvatar(agentId); } + catch (Exception e) + { + m_log.LogWarning("[APPEARANCE SERVICE]: avatar service threw reading the bake index for {Agent}: {Message}", agentId, e.Message); + return null; + } + + if (avatar?.Data is null || !avatar.Data.TryGetValue(key, out string stored) || !UUID.TryParse(stored, out UUID storedId) || storedId.IsZero()) + { + // No index entry: this agent has never been baked by a simulator that stores one, or the index was + // cleared. Nothing to serve, and nothing to guess at. + m_log.LogDebug("[APPEARANCE SERVICE]: no {Key} for {Agent}", key, agentId); + return null; + } + + if (storedId.NotEqual(expectedAssetId)) + { + // The viewer is holding an appearance older (or newer) than the index. Refusing sends it back to the + // appearance it should be using; answering would paint a bake it never asked for. + m_log.LogDebug("[APPEARANCE SERVICE]: {Agent} {Key} is {Stored} but {Expected} was requested; refusing", + agentId, key, storedId, expectedAssetId); + return null; + } + + AssetBase asset; + try { asset = m_AssetService.Get(storedId.ToString()); } + catch (Exception e) + { + m_log.LogWarning("[APPEARANCE SERVICE]: asset service threw fetching {Asset} for {Agent}: {Message}", storedId, agentId, e.Message); + return null; + } + + if (asset?.Data is not { Length: > 0 }) + { + // The index points at an asset that is gone. Same answer as no index at all — the sim re-bakes and + // rewrites the index the next time it sees this agent (ADR-004). + m_log.LogWarning("[APPEARANCE SERVICE]: {Key} for {Agent} points at {Asset}, which the asset service does not have", + key, agentId, storedId); + return null; + } + + return asset; + } +} diff --git a/Source/OpenSim.Services.AvatarService/AvatarService.cs b/Source/OpenSim.Services.AvatarService/AvatarService.cs index bc0bca98ac4..c2cc534f215 100644 --- a/Source/OpenSim.Services.AvatarService/AvatarService.cs +++ b/Source/OpenSim.Services.AvatarService/AvatarService.cs @@ -47,6 +47,12 @@ public AvatarService(IConfigSource config) m_log.LogDebug("[AVATAR SERVICE]: Starting avatar service"); } + /// Test seam; see . + protected AvatarService(IConfigSource config, IAvatarData database) + : base(config, database) + { + } + public AvatarAppearance GetAppearance(UUID principalID) { AvatarData avatar = GetAvatar(principalID); @@ -90,6 +96,29 @@ public bool SetAvatar(UUID principalID, AvatarData avatar) count++; // m_log.LogDebug("[AVATAR SERVICE]: SetAvatar for {0}, attachs={1}", principalID, count); + + // The delete below is load-bearing and stays: the appearance keys are of variable cardinality + // (`Wearable i:j`, `_ap_`) and ToAvatarAppearance reads them additively, so a row left behind by a + // shirt that has been taken off would put the shirt back on at the next read. + // + // What it must not take with it is another subsystem's data in the same table — server-side baking's + // ADR-004 index (Ledger Q-14). Those rows are read back first and rewritten immediately after the + // appearance keys, so an appearance save is transparent to them. Grids that have never baked pay nothing: + // with no preserved rows this is exactly the code that was here before, one delete and the same stores. + List preserved = null; + foreach (AvatarBaseData row in m_Database.Get("PrincipalID", principalID.ToString())) + { + if (row.Data is null || !row.Data.TryGetValue("Name", out string name) || !AvatarDataKeys.IsPreserved(name)) + continue; + row.Data.TryGetValue("Value", out string value); + (preserved ??= new List()).Add( + new AvatarBaseData + { + PrincipalID = principalID, + Data = new Dictionary { ["Name"] = name, ["Value"] = value ?? string.Empty }, + }); + } + m_Database.Delete("PrincipalID", principalID.ToString()); AvatarBaseData av = new AvatarBaseData(); @@ -100,7 +129,10 @@ public bool SetAvatar(UUID principalID, AvatarData avatar) av.Data["Value"] = avatar.AvatarType.ToString(); if (!m_Database.Store(av)) + { + RestorePreserved(preserved); return false; + } foreach (KeyValuePair kvp in avatar.Data) { @@ -136,13 +168,35 @@ public bool SetAvatar(UUID principalID, AvatarData avatar) if (!m_Database.Store(av)) { m_Database.Delete("PrincipalID", principalID.ToString()); + RestorePreserved(preserved); return false; } } + RestorePreserved(preserved); return true; } + /// + /// Put the module-owned rows back after the appearance rows have been written. Called on the success path and + /// on both failure paths, because a failed appearance write is no reason to destroy a bake index that is still + /// pointing at valid assets. A store that fails here is logged and not fatal: the index going missing means + /// "re-bake", which is always safe (ADR-004). + /// + private void RestorePreserved(List preserved) + { + if (preserved is null) + return; + + foreach (AvatarBaseData row in preserved) + { + if (!m_Database.Store(row)) + m_log.LogWarning( + "[AVATAR SERVICE]: could not restore preserved key {Name} for {PrincipalID} after an appearance save; it is lost and will be rebuilt", + row.Data["Name"], row.PrincipalID); + } + } + public bool ResetAvatar(UUID principalID) { return m_Database.Delete("PrincipalID", principalID.ToString()); diff --git a/Source/OpenSim.Services.AvatarService/AvatarServiceBase.cs b/Source/OpenSim.Services.AvatarService/AvatarServiceBase.cs index f37cbc33f07..5be0c35b509 100644 --- a/Source/OpenSim.Services.AvatarService/AvatarServiceBase.cs +++ b/Source/OpenSim.Services.AvatarService/AvatarServiceBase.cs @@ -35,6 +35,17 @@ public class AvatarServiceBase : ServiceBase { protected IAvatarData m_Database = null; + /// + /// Test seam: a service over a supplied data layer, bypassing plugin loading. Production always uses the + /// constructor; this exists so the delete-and-preserve behaviour of + /// SetAvatar can be exercised against a faithful in-memory row table. + /// + protected AvatarServiceBase(IConfigSource config, IAvatarData database) + : base(config) + { + m_Database = database ?? throw new ArgumentNullException(nameof(database)); + } + public AvatarServiceBase(IConfigSource config) : base(config) { diff --git a/Source/OpenSim.Services.Interfaces/AppearanceChannels.cs b/Source/OpenSim.Services.Interfaces/AppearanceChannels.cs new file mode 100644 index 00000000000..f8a3edc84df --- /dev/null +++ b/Source/OpenSim.Services.Interfaces/AppearanceChannels.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace OpenSim.Services.Interfaces; + +/// +/// The bake channel tokens the LL viewer puts in an agent_appearance_service URL, and the +/// index names they resolve to. +/// +/// +/// Established from the viewer, not assumed. LLVOAvatar::getImageURL builds +/// appearance_service_url + "texture/" + agent + "/" + texture_entry->mDefaultImageName + "/" + uuid +/// (indra/newview/llvoavatar.cpp:5912). So the channel token is neither a number nor the enum name — it is +/// the mDefaultImageName of the baked TextureEntry, which is the fourth constructor argument +/// (indra/llappearance/llavatarappearancedefines.h:162-167, +/// llavatarappearancedefines.cpp:202-215) and is given for the eleven baked entries at +/// llavatarappearancedefines.cpp:81-91: +/// +/// +/// +/// TEX_HEAD_BAKED -> "head" TEX_SKIRT_BAKED -> "skirt" +/// TEX_UPPER_BAKED -> "upper" TEX_LEFT_ARM_BAKED -> "leftarm" +/// TEX_LOWER_BAKED -> "lower" TEX_LEFT_LEG_BAKED -> "leftleg" +/// TEX_EYES_BAKED -> "eyes" TEX_AUX1_BAKED -> "aux1" +/// TEX_HAIR_BAKED -> "hair" TEX_AUX2_BAKED -> "aux2" +/// TEX_AUX3_BAKED -> "aux3" +/// +/// +/// +/// All lower case, and leftarm/leftleg carry no separator. They coincide exactly with the bake +/// library's BakeChannel names lower-cased — which is also the token in a stored bake's asset name, +/// bake:<agent>:<channel> — but that is a coincidence worth pinning rather than relying on, and a +/// test pins it. This type is the authority for the wire, and it deliberately does not reference the bake library: +/// the Robust deployment does not carry it. +/// +/// +public static class AppearanceChannels +{ + /// Token as the viewer writes it, paired with the index name Bake:<name> uses. + private static readonly (string Token, string IndexName)[] Channels = + { + ("head", "Head"), + ("upper", "Upper"), + ("lower", "Lower"), + ("eyes", "Eyes"), + ("skirt", "Skirt"), + ("hair", "Hair"), + ("leftarm", "LeftArm"), + ("leftleg", "LeftLeg"), + ("aux1", "Aux1"), + ("aux2", "Aux2"), + ("aux3", "Aux3"), + }; + + /// Every token the viewer can send, in baked-texture-index order. + public static IReadOnlyList Tokens + { + get + { + var list = new List(Channels.Length); + foreach (var c in Channels) list.Add(c.Token); + return list; + } + } + + /// + /// The avatar-service key that holds the bake for a viewer channel token, or null when the token is not one + /// of the eleven. Matching is case-insensitive so a viewer or proxy that upper-cases the path still resolves; + /// nothing else is accepted, and in particular a numeric token is not — the viewer never sends one. + /// + public static string BakeKeyFor(string channelToken) + { + if (string.IsNullOrEmpty(channelToken)) return null; + foreach (var c in Channels) + if (string.Equals(c.Token, channelToken, StringComparison.OrdinalIgnoreCase)) + return AvatarDataKeys.BakeIndexPrefix + ":" + c.IndexName; + return null; + } + + /// The index name for a token ("leftarm""LeftArm"), or null. + public static string IndexNameFor(string channelToken) + { + if (string.IsNullOrEmpty(channelToken)) return null; + foreach (var c in Channels) + if (string.Equals(c.Token, channelToken, StringComparison.OrdinalIgnoreCase)) + return c.IndexName; + return null; + } +} diff --git a/Source/OpenSim.Services.Interfaces/IAppearanceService.cs b/Source/OpenSim.Services.Interfaces/IAppearanceService.cs new file mode 100644 index 00000000000..c04d50b2930 --- /dev/null +++ b/Source/OpenSim.Services.Interfaces/IAppearanceService.cs @@ -0,0 +1,34 @@ +using OpenMetaverse; +using OpenSim.Framework; + +namespace OpenSim.Services.Interfaces; + +/// +/// The read side of server-side baking (Design Brief C4, ADR-002): given an agent, a bake channel and the asset +/// UUID the viewer believes that channel holds, hand back the stored bake. +/// +/// +/// This is what the LL viewer fetches from on a bit-0 region. It stops compositing there and asks +/// agent_appearance_service for every other avatar's bakes, so with no such service an avatar on such a +/// region never textures — which is precisely what a bit-0 region without S4 produces, and why the build plan +/// puts S4 before any flag flip. +/// +/// +public interface IAppearanceService +{ + /// + /// The stored bake for one channel, or null when there is nothing to serve. + /// + /// Whose bake. + /// + /// The channel as the viewer spells it in the URL — see , which is the + /// authority for the token set. + /// + /// + /// The asset id the viewer took from the avatar's TextureEntry. The service returns null unless the index + /// agrees with it: a viewer asking for a bake the sim has since superseded must get a 404 and re-read the + /// appearance, never a different avatar's pixels. + /// + /// The asset, or null for "nothing here" — which the connector turns into 404. + AssetBase GetBake(UUID agentId, string channelToken, UUID expectedAssetId); +} diff --git a/Source/OpenSim.Services.Interfaces/IAvatarService.cs b/Source/OpenSim.Services.Interfaces/IAvatarService.cs index 323241ddf2c..d10f0d0dab8 100644 --- a/Source/OpenSim.Services.Interfaces/IAvatarService.cs +++ b/Source/OpenSim.Services.Interfaces/IAvatarService.cs @@ -87,6 +87,54 @@ public interface IAvatarService /// Each region/client that uses avatars will have a data structure /// of this type representing the avatars. /// +/// +/// Names in the avatar service's key/value store that do not belong to the appearance record. +/// +/// +/// has to start by deleting every row for the principal, because the +/// appearance keys it writes are of variable cardinality: Wearable i:j and _ap_<point> exist +/// only while something occupies that slot or attach point, and +/// reads them additively +/// (wearables[index].Add(...), SetAttachment). Without the delete, taking a shirt off would leave +/// its Wearable 4:0 row behind and the next read would put the shirt back on. The delete is load-bearing +/// and stays. +/// +/// +/// +/// What must not be caught by it is data some other subsystem keeps in the same table. Server-side baking's +/// ADR-004 index does exactly that — Bake:<channel>, BakeHash:<channel>, +/// BakeCOFVersion, BakeSize, BakeUpdated — and before this existed, every appearance save +/// destroyed it (Ledger Q-14). A name listed here is preserved across SetAvatar; everything else is the +/// appearance record and is replaced wholesale, exactly as before. +/// +/// +/// +/// No appearance key may start with a preserved prefix. The appearance layer writes Serial, +/// AvatarHeight, VisualParams, Wearable i:j and _ap_N, plus AvatarType and the +/// legacy <Type>Item/<Type>Asset pairs; none of them begins with Bake, and a test +/// pins that. +/// +/// +public static class AvatarDataKeys +{ + /// Server-side baking's bake index (ADR-004). Covers Bake:, BakeHash:, BakeCOFVersion, BakeSize and BakeUpdated in one prefix. + public const string BakeIndexPrefix = "Bake"; + + /// Every prefix preserves. + public static readonly string[] PreservedPrefixes = { BakeIndexPrefix }; + + /// True when the named row belongs to a module rather than to the appearance record. + public static bool IsPreserved(string name) + { + if (string.IsNullOrEmpty(name)) + return false; + foreach (string prefix in PreservedPrefixes) + if (name.StartsWith(prefix, StringComparison.Ordinal)) + return true; + return false; + } +} + public class AvatarData { // This pretty much determines which name/value pairs will be @@ -148,8 +196,20 @@ public AvatarData(AvatarAppearance appearance) // Wearables Data["AvatarHeight"] = appearance.AvatarHeight.ToString(); - for (int i = 0 ; i < AvatarWearable.LEGACY_VERSION_MAX_WEARABLES ; i++) + // S11: every type the appearance actually has, not the first LEGACY_VERSION_MAX_WEARABLES (15) of them. + // The old bound stopped at type 14, so Physics (15) and Universal (16) were never written - the record + // for a live avatar wearing both held no "Wearable 15:*" or "Wearable 16:*" row at all, however + // faithfully the rest of the stack carried them. Nothing else in the tree is bounded this way: the + // wearable table is MAX_WEARABLES (17) wide (AvatarWearable.cs:75), the wire negotiates its own count and + // sends 15 and 16 in "wrbls8" (AvatarAppearance.cs:801-819), the compositor draws both, and the reader + // below takes any index (S10). This writer was the only floor. It is driven off the array's own length so + // it is right for a legacy 15-slot appearance too - AvatarAppearance's constructor and ClearWearables + // still make one of those (AvatarAppearance.cs:320-325) - and for whatever a later type count adds. + for (int i = 0 ; i < appearance.Wearables.Length ; i++) { + if (appearance.Wearables[i] is null) + continue; + for (int j = 0 ; j < appearance.Wearables[i].Count ; j++) { string fieldName = String.Format("Wearable {0}:{1}", i, j); @@ -288,6 +348,12 @@ public AvatarAppearance ToAvatarAppearance() AvatarWearable[] wearables = appearance.Wearables; int currentLength = wearables.Length; + + // S10: the key is "Wearable :" and BOTH numbers matter. Several wearables of one type + // are layered in index order, later index on top (LLTexLayerTemplate::render, lltexlayer.cpp:1659-1689), + // so a record read back in row order would silently reorder them: Data comes from a row store, which + // owes no order at all. Collect first, then apply by (type, index). + var wornRows = new List<(int Type, int Index, UUID ItemID, UUID AssetID)>(); foreach (KeyValuePair _kvp in Data) { // New style wearables @@ -296,18 +362,12 @@ public AvatarAppearance ToAvatarAppearance() string wearIndex = _kvp.Key.Substring(9); string[] wearIndices = wearIndex.Split(new char[] {':'}); int index = Convert.ToInt32(wearIndices[0]); + // A record written before the index was carried has one entry per type and no ":"; + // it reads as index 0, which is what it was. + int slot = wearIndices.Length > 1 ? Convert.ToInt32(wearIndices[1]) : 0; string[] ids = _kvp.Value.Split(new char[] {':'}); - UUID itemID = new UUID(ids[0]); - UUID assetID = new UUID(ids[1]); - if (index >= currentLength) - { - Array.Resize(ref wearables, index + 1); - for (int i = currentLength ; i < wearables.Length ; i++) - wearables[i] = new AvatarWearable(); - currentLength = wearables.Length; - } - wearables[index].Add(itemID, assetID); + wornRows.Add((index, slot, new UUID(ids[0]), new UUID(ids[1]))); continue; } // Attachments @@ -330,6 +390,20 @@ public AvatarAppearance ToAvatarAppearance() } } + wornRows.Sort((a, b) => a.Type != b.Type ? a.Type.CompareTo(b.Type) : a.Index.CompareTo(b.Index)); + foreach (var row in wornRows) + { + if (row.Type >= currentLength) + { + Array.Resize(ref wearables, row.Type + 1); + for (int i = currentLength ; i < wearables.Length ; i++) + wearables[i] = new AvatarWearable(); + currentLength = wearables.Length; + } + wearables[row.Type].Add(row.ItemID, row.AssetID); + } + appearance.Wearables = wearables; + if (appearance.Wearables[AvatarWearable.BODY].Count == 0) appearance.Wearables[AvatarWearable.BODY].Wear( AvatarWearable.DefaultWearables[ diff --git a/Source/OpenSim.Services.LLLoginService/LLLoginResponse.cs b/Source/OpenSim.Services.LLLoginService/LLLoginResponse.cs index 9caa73ff8b0..21537558b78 100644 --- a/Source/OpenSim.Services.LLLoginService/LLLoginResponse.cs +++ b/Source/OpenSim.Services.LLLoginService/LLLoginResponse.cs @@ -179,6 +179,13 @@ public class LLLoginResponse : OpenSim.Services.Interfaces.LoginResponse private string searchURL; + /// + /// Where the viewer fetches other avatars' server-side bakes: agent_appearance_service in the login + /// response (viewer contract V6). Empty means "this grid has none", and the key is then left out of the + /// response entirely rather than sent empty — see . + /// + private string agentAppearanceServiceURL = String.Empty; + // Error Flags private string errorReason; private string errorMessage; @@ -463,6 +470,7 @@ private void SetDefaultValues() profileURL = String.Empty; openIDURL = String.Empty; searchURL = String.Empty; + agentAppearanceServiceURL = String.Empty; currency = String.Empty; ClassifiedFee = "0"; @@ -538,6 +546,13 @@ public override Hashtable ToHashtable() if (searchURL != String.Empty) responseData["search"] = searchURL; + // V6: with no service the viewer logs "AgentAppearanceServiceURL not set - Baked texture requests + // will fail" and never textures anyone on a bit-0 region (llvoavatar.cpp:5901-5906). Omitting the key + // is the honest form of "we have none"; the viewer only adopts a non-empty value anyway + // (llstartup.cpp:4047-4051). + if (agentAppearanceServiceURL != String.Empty) + responseData["agent_appearance_service"] = agentAppearanceServiceURL; + if (mapTileURL != String.Empty) responseData["map-server-url"] = mapTileURL; @@ -679,6 +694,9 @@ public override OSD ToOSDMap() if (searchURL != String.Empty) map["search"] = OSD.FromString(searchURL); + if (agentAppearanceServiceURL != String.Empty) + map["agent_appearance_service"] = OSD.FromString(agentAppearanceServiceURL); + if (ClassifiedFee != String.Empty) map["classified_fee"] = OSD.FromString(ClassifiedFee); @@ -1069,6 +1087,13 @@ public string SearchURL set { searchURL = value; } } + /// See . Set by the login service from config; empty by default. + public string AgentAppearanceServiceURL + { + get { return agentAppearanceServiceURL; } + set { agentAppearanceServiceURL = value ?? String.Empty; } + } + public string Message { get { return welcomeMessage; } diff --git a/Source/OpenSim.Services.LLLoginService/LLLoginService.cs b/Source/OpenSim.Services.LLLoginService/LLLoginService.cs index 8e474612e18..ca0063a9d0e 100755 --- a/Source/OpenSim.Services.LLLoginService/LLLoginService.cs +++ b/Source/OpenSim.Services.LLLoginService/LLLoginService.cs @@ -77,6 +77,7 @@ public class LLLoginService : ILoginService protected string m_ProfileURL; protected string m_OpenIDURL; protected string m_SearchURL; + protected string m_AgentAppearanceServiceURL; protected string m_Currency; protected string m_ClassifiedFee; protected int m_MaxAgentGroups = 42; @@ -125,6 +126,10 @@ public LLLoginService(IConfigSource config, ISimulationService simService, ILibr m_ProfileURL = m_LoginServerConfig.GetString("ProfileServerURL", string.Empty); m_OpenIDURL = m_LoginServerConfig.GetString("OpenIDServerURL", string.Empty); m_SearchURL = m_LoginServerConfig.GetString("SearchURL", string.Empty); + // V6 / ADR-002. Empty by default, which means the key is omitted from the login response and the viewer + // keeps compositing locally. Lives in [LoginService] beside the other advertised URLs so grid and + // standalone configure it the same way -- both have that section. + m_AgentAppearanceServiceURL = m_LoginServerConfig.GetString("AgentAppearanceServiceURL", string.Empty); m_Currency = m_LoginServerConfig.GetString("Currency", string.Empty); m_ClassifiedFee = m_LoginServerConfig.GetString("ClassifiedFee", string.Empty); m_DestinationGuide = m_LoginServerConfig.GetString ("DestinationGuide", string.Empty); @@ -206,6 +211,16 @@ public LLLoginService(IConfigSource config, ISimulationService simService, ILibr m_MapTileURL += "/"; } + // The viewer appends "texture/..." to this with no separator (llvoavatar.cpp:5912), so a value without a + // trailing slash produces a malformed URL and every bake fetch 404s. Same normalisation MapTileURL gets. + if (!string.IsNullOrWhiteSpace(m_AgentAppearanceServiceURL)) + { + m_AgentAppearanceServiceURL = m_AgentAppearanceServiceURL.Trim(); + if (!m_AgentAppearanceServiceURL.EndsWith("/")) + m_AgentAppearanceServiceURL += "/"; + } + else m_AgentAppearanceServiceURL = string.Empty; + IConfig messagingConfig = config.Configs["Messaging"]; if (messagingConfig is not null) m_messageKey = messagingConfig.GetString("MessageKey", string.Empty); @@ -630,6 +645,9 @@ public LoginResponse Login(string firstName, string lastName, string passwd, str m_MapTileURL, m_ProfileURL, m_OpenIDURL, m_SearchURL, m_Currency, m_DSTZone, m_DestinationGuide, m_AvatarPicker, realID, m_ClassifiedFee,m_MaxAgentGroups); + // Set after construction rather than threading another argument through a 20-parameter constructor. + response.AgentAppearanceServiceURL = m_AgentAppearanceServiceURL; + m_log.LogDebug("[LLOGIN SERVICE]: All clear. Sending login response to {0} {1}", firstName, lastName); return response; diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.Tests/EventQueue/Tests/EventQueueTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.Tests/EventQueue/Tests/EventQueueTests.cs index 4ba1bf0034d..9eff11389cd 100644 --- a/Tests/OpenSim.Region.ClientStack.LindenCaps.Tests/EventQueue/Tests/EventQueueTests.cs +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.Tests/EventQueue/Tests/EventQueueTests.cs @@ -39,6 +39,9 @@ using OpenSim.Region.Framework.Scenes; using OpenSim.Region.OptionalModules.World.NPC; using OpenSim.Tests.Common; +using Nini.Config; +using Xunit; +using OpenSim.Region.ClientStack.LindenCaps; namespace OpenSim.Region.ClientStack.Linden.Tests { @@ -46,6 +49,7 @@ public class EventQueueTests : OpenSimTestCase { private TestScene m_scene; private EventQueueGetModule m_eqgMod; + private BaseHttpServer m_server; private NPCModule m_npcMod; public override void SetUp() @@ -56,11 +60,11 @@ public override void SetUp() // This is an unfortunate bit of clean up we have to do because MainServer manages things through static // variables and the VM is not restarted between tests. - MainServer.RemoveHttpServer(port); + // T1: MainServer is instance-based now; OpenSimTestCase removed the previous default server, so this one becomes DefaultServer. + MainServer.Instance.RemoveHttpServer(port); - BaseHttpServer server = new BaseHttpServer(port, false, "","",""); - MainServer.AddHttpServer(server); - MainServer.Instance = server; + m_server = new BaseHttpServer(port, false, "","",""); + MainServer.Instance.AddHttpServer(m_server); IConfigSource config = new IniConfigSource(); config.AddConfig("Startup"); @@ -86,7 +90,7 @@ public void TestAddForClient() SceneHelpers.AddScenePresence(m_scene, TestHelpers.ParseTail(0x1)); // TODO: Add more assertions for the other aspects of event queues - Assert.True(MainServer.Instance.GetPollServiceHandlerKeys().Count)); + Assert.Equal(1, m_server.GetPollServiceHandlerKeys().Count); } [Fact] @@ -101,7 +105,7 @@ public void TestRemoveForClient() m_scene.CloseAgent(spId, false); // TODO: Add more assertions for the other aspects of event queues - Assert.True(MainServer.Instance.GetPollServiceHandlerKeys().Count)); + Assert.Equal(0, m_server.GetPollServiceHandlerKeys().Count); } [Fact] @@ -127,7 +131,7 @@ public void TestEnqueueMessage() eventsResponse = m_eqgMod.GetEvents(UUID.Zero, sp.UUID); } - Assert.True((int)eventsResponse["int_response_code"])HttpStatusCode.OK)); + Assert.Equal((int)HttpStatusCode.OK, (int)eventsResponse["int_response_code"]); // Console.WriteLine("Response [{0}]", (string)eventsResponse["str_response_string"]); string data = String.Empty; @@ -146,7 +150,7 @@ public void TestEnqueueMessage() foundUpdate = true; } - Assert.True(foundUpdate)); + Assert.True(foundUpdate, string.Format("Did not find {0} in response", messageName)); } /// @@ -164,7 +168,7 @@ public void TestEnqueueMessageNoUser() Hashtable eventsResponse = m_eqgMod.GetEvents(UUID.Zero, TestHelpers.ParseTail(0x1)); - Assert.True((int)eventsResponse["int_response_code"])HttpStatusCode.NotFound)); + Assert.Equal((int)HttpStatusCode.NotFound, (int)eventsResponse["int_response_code"]); } /// @@ -188,7 +192,7 @@ UUID npcId Hashtable eventsResponse = m_eqgMod.GetEvents(UUID.Zero, npc.UUID); - Assert.True((int)eventsResponse["int_response_code"])HttpStatusCode.NotFound)); + Assert.Equal((int)HttpStatusCode.NotFound, (int)eventsResponse["int_response_code"]); } } } diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.Tests/OpenSim.Region.ClientStack.LindenCaps.Tests.csproj b/Tests/OpenSim.Region.ClientStack.LindenCaps.Tests/OpenSim.Region.ClientStack.LindenCaps.Tests.csproj index a3b8998202e..b714fab5525 100644 --- a/Tests/OpenSim.Region.ClientStack.LindenCaps.Tests/OpenSim.Region.ClientStack.LindenCaps.Tests.csproj +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.Tests/OpenSim.Region.ClientStack.LindenCaps.Tests.csproj @@ -1,11 +1,17 @@ - - enable - enable + + + + false true + false - + @@ -22,22 +28,13 @@ - - all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - diff --git a/Tests/OpenSim.Region.CoreModules.Tests/AssemblyInfo.cs b/Tests/OpenSim.Region.CoreModules.Tests/AssemblyInfo.cs new file mode 100644 index 00000000000..78d4c327c48 --- /dev/null +++ b/Tests/OpenSim.Region.CoreModules.Tests/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// These tests exercise scenes that share process-wide static state (MainServer, +// Util.FireAndForgetMethod, static caps registries), so they cannot run in parallel. +// +// Without this the project is order-dependent and flaky: a full run fails a different set of +// tests each time (9 failures at HEAD, but not the same 9), and failures appear inside +// SceneHelpers.SetupScene rather than in any assertion. The same declaration, for the same +// reason, is already in Tests/OpenSim.Region.Framework.Tests/AssemblyInfo.cs. +[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true)] diff --git a/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactoryAppearanceIntegrityTests.cs b/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactoryAppearanceIntegrityTests.cs new file mode 100644 index 00000000000..cb0b6552efa --- /dev/null +++ b/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactoryAppearanceIntegrityTests.cs @@ -0,0 +1,202 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Threading; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Services.Interfaces; +using OpenSim.Tests.Common; +using Xunit; + +namespace OpenSim.Region.CoreModules.Avatar.AvatarFactory; + +/// +/// S8. Two ways the stored appearance was destroyed by a region that was only trying to save it. +/// +/// The live loss. On 2026-09-05 a stock viewer carrying a stale inventory cache from a previous grid +/// named four item ids that exist nowhere in inventoryitems. SetAppearanceAssets logged +/// "Can't find inventory item ... setting to default" for Skin, Hair, Eyes and Shirt and then, despite the +/// message, removed them; SaveAppearance persisted the result on the next line, and because +/// AvatarService.SetAvatar deletes every row for the agent before rewriting (AvatarService.cs:93), +/// wearable slots 1-4 disappeared from the stored record entirely. The agent's Current Outfit folder still linked +/// perfectly good skin, eyes and hair items throughout. +/// +/// Why the existing suite could not catch it. AvatarFactoryNowWearingTests covers S0c, which +/// is the same failure from the other direction - there the viewer LISTED fewer slots than were worn. Every one +/// of its cases uses item ids the inventory can resolve, so the "listed but unresolvable" branch was never +/// entered. And no appearance test had ever driven a child presence. +/// +public class AvatarFactoryAppearanceIntegrityTests : OpenSimTestCase +{ + /// Records what actually reached the avatar service - the thing both defects corrupted. + private sealed class RecordingAvatarService : IAvatarService, ISharedRegionModule + { + public readonly List<(UUID User, AvatarAppearance Appearance)> Saved = new(); + + public bool SetAppearance(UUID userID, AvatarAppearance appearance) + { + lock (Saved) Saved.Add((userID, appearance)); + return true; + } + + public int Calls { get { lock (Saved) return Saved.Count; } } + + public AvatarAppearance GetAppearance(UUID userID) => null!; + public AvatarData GetAvatar(UUID userID) => null!; + public bool SetAvatar(UUID userID, AvatarData avatar) => true; + public bool ResetAvatar(UUID userID) => true; + public bool SetItems(UUID userID, string[] names, string[] values) => true; + public bool RemoveItems(UUID userID, string[] names) => true; + + public string Name => "RecordingAvatarService"; + public Type ReplaceableInterface => null!; + public void Initialise(IConfigSource source) { } + public void PostInitialise() { } + public void Close() { } + public void AddRegion(Scene scene) => scene.RegisterModuleInterface(this); + public void RemoveRegion(Scene scene) { } + public void RegionLoaded(Scene scene) { } + } + + private static readonly UUID SkinItem = new("db5a4e5f-0000-4000-8000-000000000001"); + private static readonly UUID SkinAsset = new("db5a4e5f-0000-4000-8000-0000000000a1"); + + private static (AvatarFactoryModule afm, TestScene scene, RecordingAvatarService avatars, ScenePresence sp) Setup() + { + RecordingAvatarService avatars = new RecordingAvatarService(); + AvatarFactoryModule afm = new AvatarFactoryModule(); + TestScene scene = new SceneHelpers().SetupScene(); + SceneHelpers.SetupSceneModules(scene, avatars, afm); + + // SetAppearanceAssets does nothing at all unless the agent has an inventory root + // (AvatarFactoryModule.cs:911) - without this the unresolvable branch is unreachable and these tests + // would pass against the broken code. + UserAccount user = UserAccountHelpers.CreateUserWithInventory(scene, 0x1); + ScenePresence sp = SceneHelpers.AddScenePresence(scene, user.PrincipalID); + return (afm, scene, avatars, sp); + } + + /// Wear one skin whose item id the inventory service will not resolve. + private static void WearUnresolvableSkin(ScenePresence sp) + { + AvatarWearable[] wearables = sp.Appearance.Wearables; + wearables[(int)WearableType.Skin] = new AvatarWearable(SkinItem, SkinAsset); + sp.Appearance.Wearables = wearables; + } + + // ------------------------------------------------------------------ 1. an unresolvable item keeps its slot + + /// + /// The item id cannot be resolved, so nothing can be said about the asset behind it - but the agent is still + /// wearing a skin, and the region must not decide otherwise. Before S8 this slot came out empty. + /// + [Fact] + public void An_item_the_region_cannot_resolve_keeps_its_slot() + { + (AvatarFactoryModule afm, TestScene scene, RecordingAvatarService avatars, ScenePresence sp) = Setup(); + WearUnresolvableSkin(sp); + + afm.QueueAppearanceSave(sp.UUID); + scene.EventManager.TriggerOnRemovePresence(sp.UUID); // the close flush runs SaveAppearance synchronously + + Assert.Equal(1, avatars.Calls); + AvatarAppearance saved = avatars.Saved[0].Appearance; + Assert.True(saved.Wearables[(int)WearableType.Skin].Count > 0, + "the skin slot was emptied by a failed inventory lookup; the agent is still wearing a skin"); + Assert.Equal(SkinItem, saved.Wearables[(int)WearableType.Skin][0].ItemID); + } + + /// + /// And the in-memory appearance keeps it too, which is what the next save, the next bake and every viewer in + /// range read from. + /// + [Fact] + public void The_presence_keeps_the_slot_as_well_as_the_stored_record() + { + (AvatarFactoryModule afm, TestScene scene, RecordingAvatarService _, ScenePresence sp) = Setup(); + WearUnresolvableSkin(sp); + + afm.QueueAppearanceSave(sp.UUID); + scene.EventManager.TriggerOnRemovePresence(sp.UUID); + + Assert.True(sp.Appearance.Wearables[(int)WearableType.Skin].Count > 0); + Assert.Equal(SkinItem, sp.Appearance.Wearables[(int)WearableType.Skin][0].ItemID); + } + + // ------------------------------------------------------------------ 2. a child presence never writes + + /// + /// A child presence's appearance is a copy carried for drawing; the root region owns it. Saving from one means + /// resolving another region's inventory view and writing the answer as fact. + /// + /// This drives the real timer drain rather than the close flush, because the close flush has had a child + /// guard since S0b and would pass either way. DelayBeforeAppearanceSave = 0 makes the queue drain on + /// the next 500 ms tick. + /// + [Fact] + public void A_child_presence_does_not_write_to_the_avatar_service() + { + RecordingAvatarService avatars = new RecordingAvatarService(); + AvatarFactoryModule afm = new AvatarFactoryModule(); + + IniConfigSource config = new IniConfigSource(); + config.AddConfig("Appearance").Set("DelayBeforeAppearanceSave", 0); + + TestScene scene = new SceneHelpers().SetupScene(); + SceneHelpers.SetupSceneModules(scene, config, avatars, afm); + ScenePresence sp = SceneHelpers.AddScenePresence(scene, TestHelpers.ParseTail(0x2)); + + sp.MakeChildAgent(scene.RegionInfo.RegionHandle + 1); // "the root is somewhere else now" + Assert.True(sp.IsChildAgent, "the presence must actually be a child for this test to mean anything"); + + afm.QueueAppearanceSave(sp.UUID); + + // Give the 500 ms drain timer several chances. If a write is coming, it arrives well inside this. + for (int i = 0; i < 12 && avatars.Calls == 0; i++) + Thread.Sleep(250); + + Assert.Equal(0, avatars.Calls); + } + + /// The same presence as a root does write, so the guard above is about the child state and nothing else. + [Fact] + public void A_root_presence_still_writes() + { + (AvatarFactoryModule afm, TestScene scene, RecordingAvatarService avatars, ScenePresence sp) = Setup(); + Assert.False(sp.IsChildAgent); + + afm.QueueAppearanceSave(sp.UUID); + scene.EventManager.TriggerOnRemovePresence(sp.UUID); + + Assert.Equal(1, avatars.Calls); + } +} diff --git a/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactoryNowWearingTests.cs b/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactoryNowWearingTests.cs new file mode 100644 index 00000000000..075a8b9d3d5 --- /dev/null +++ b/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactoryNowWearingTests.cs @@ -0,0 +1,163 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using OpenMetaverse; + +using OpenSim.Framework; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Tests.Common; +using Xunit; + +namespace OpenSim.Region.CoreModules.Avatar.AvatarFactory; + +/// +/// S0c (SSB Ledger R-4 / Q-3): AgentIsNowWearing must merge into the agent's existing wearables, +/// not rebuild them from an empty set. These tests drive the real event path through +/// . +/// +public class AvatarFactoryNowWearingTests : OpenSimTestCase +{ + private const int Shape = (int)WearableType.Shape; + private const int Skin = (int)WearableType.Skin; + private const int Hair = (int)WearableType.Hair; + private const int Eyes = (int)WearableType.Eyes; + + private static readonly UUID ShapeItem = TestHelpers.ParseTail(0x10); + private static readonly UUID SkinItem = TestHelpers.ParseTail(0x11); + private static readonly UUID HairItem = TestHelpers.ParseTail(0x12); + private static readonly UUID EyesItem = TestHelpers.ParseTail(0x13); + + private static (AvatarFactoryModule afm, ScenePresence sp) SetupPresenceWearingBodyParts() + { + UUID userId = TestHelpers.ParseTail(0x1); + + TestsAssetCache assetCache = new TestsAssetCache(); + AvatarFactoryModule afm = new AvatarFactoryModule(); + TestScene scene = new SceneHelpers(assetCache).SetupScene(); + SceneHelpers.SetupSceneModules(scene, afm); + ScenePresence sp = SceneHelpers.AddScenePresence(scene, userId); + + AvatarWearable[] wearables = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (int i = 0; i < wearables.Length; i++) + wearables[i] = new AvatarWearable(); + wearables[Shape].Add(ShapeItem, TestHelpers.ParseTail(0x20)); + wearables[Skin].Add(SkinItem, TestHelpers.ParseTail(0x21)); + wearables[Hair].Add(HairItem, TestHelpers.ParseTail(0x22)); + wearables[Eyes].Add(EyesItem, TestHelpers.ParseTail(0x23)); + sp.Appearance.Wearables = wearables; + + return (afm, sp); + } + + private static AvatarWearingArgs NowWearing(params (int type, UUID item)[] entries) + { + AvatarWearingArgs e = new AvatarWearingArgs(); + foreach ((int type, UUID item) in entries) + e.NowWearing.Add(new AvatarWearingArgs.Wearable(item, (byte)type)); + return e; + } + + private static UUID ItemIn(ScenePresence sp, int type) + => sp.Appearance.Wearables[type].Count == 0 ? UUID.Zero : sp.Appearance.Wearables[type][0].ItemID; + + /// (a) Partial list: slots the viewer did not mention are retained. + [Fact] + public void PartialNowWearing_RetainsUnlistedSlots() + { + TestHelpers.InMethod(); + (AvatarFactoryModule _, ScenePresence sp) = SetupPresenceWearingBodyParts(); + + ((TestClient)sp.ControllingClient).TriggerAvatarNowWearing( + NowWearing((Shape, ShapeItem), (Skin, SkinItem))); + + Assert.Equal(ShapeItem, ItemIn(sp, Shape)); + Assert.Equal(SkinItem, ItemIn(sp, Skin)); + Assert.Equal(HairItem, ItemIn(sp, Hair)); + Assert.Equal(EyesItem, ItemIn(sp, Eyes)); + } + + /// (b) A listed slot with a new item id updates; everything else is retained. + [Fact] + public void NewItemInListedSlot_UpdatesThatSlotOnly() + { + TestHelpers.InMethod(); + (AvatarFactoryModule _, ScenePresence sp) = SetupPresenceWearingBodyParts(); + UUID newShape = TestHelpers.ParseTail(0x30); + + ((TestClient)sp.ControllingClient).TriggerAvatarNowWearing( + NowWearing((Shape, newShape))); + + Assert.Equal(newShape, ItemIn(sp, Shape)); + Assert.Equal(1, sp.Appearance.Wearables[Shape].Count); + Assert.Equal(SkinItem, ItemIn(sp, Skin)); + Assert.Equal(HairItem, ItemIn(sp, Hair)); + Assert.Equal(EyesItem, ItemIn(sp, Eyes)); + } + + /// + /// (c) Pre-fix semantics for an explicit UUID.Zero entry: the old code cleared every slot and then + /// called , which ignores Zero, so a slot listed with Zero ended up + /// empty. That is preserved: "type X, item Zero" clears slot X. Unlisted slots are still retained. + /// + [Fact] + public void ZeroItemInListedSlot_ClearsThatSlot_AsPreFixCodeDid() + { + TestHelpers.InMethod(); + (AvatarFactoryModule _, ScenePresence sp) = SetupPresenceWearingBodyParts(); + + ((TestClient)sp.ControllingClient).TriggerAvatarNowWearing( + NowWearing((Hair, UUID.Zero))); + + Assert.Equal(0, sp.Appearance.Wearables[Hair].Count); + Assert.Equal(ShapeItem, ItemIn(sp, Shape)); + Assert.Equal(SkinItem, ItemIn(sp, Skin)); + Assert.Equal(EyesItem, ItemIn(sp, Eyes)); + } + + /// Unchanged list reports no change, so no avatar-service write is queued. + [Fact] + public void MergeNowWearing_ReportsUnchanged_WhenListMatchesExisting() + { + TestHelpers.InMethod(); + (AvatarFactoryModule _, ScenePresence sp) = SetupPresenceWearingBodyParts(); + AvatarWearable[] before = sp.Appearance.Wearables; + + AvatarWearable[] merged = AvatarFactoryModule.MergeNowWearing( + before, + NowWearing((Shape, ShapeItem), (Skin, SkinItem), (Hair, HairItem), (Eyes, EyesItem)).NowWearing, + out bool changed); + + Assert.False(changed); + Assert.NotSame(before, merged); + Assert.Equal(TestHelpers.ParseTail(0x20), merged[Shape].GetAsset(ShapeItem)); + + // And via the event path the stored array must be left untouched. + ((TestClient)sp.ControllingClient).TriggerAvatarNowWearing( + NowWearing((Shape, ShapeItem), (Skin, SkinItem), (Hair, HairItem), (Eyes, EyesItem))); + Assert.Same(before, sp.Appearance.Wearables); + } +} diff --git a/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactorySaveFlushTests.cs b/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactorySaveFlushTests.cs new file mode 100644 index 00000000000..e83b0246669 --- /dev/null +++ b/Tests/OpenSim.Region.CoreModules.Tests/Avatar/AvatarFactory/Tests/AvatarFactorySaveFlushTests.cs @@ -0,0 +1,249 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Services.Interfaces; +using OpenSim.Tests.Common; +using Xunit; + +namespace OpenSim.Region.CoreModules.Avatar.AvatarFactory; + +/// +/// A pending appearance change must survive the agent leaving. +/// +/// QueueAppearanceSave defers the write by m_savetime seconds and SaveAppearance +/// resolves the ScenePresence only when the timer fires; if the agent left in between the presence was gone +/// and the write was skipped silently. A detach followed by a logout inside that window left the stored appearance +/// still wearing the garment, and the viewer put it back on the next login. Observed live on 2026-09-04: a detach +/// at 14:09:35 queued a save for ~14:09:40.4 and the avatar left at ~14:09:40.0. +/// +/// Why the existing suite could not catch this. Every appearance test drives a presence that stays in +/// the scene for the whole test and asserts on sp.Appearance — the in-memory object, which was always +/// correct. Nothing asserted on what reached IAvatarService, and nothing closed a presence with a save +/// still pending. The bug lives entirely in the gap between those two, and it is a *timing* gap, so no test that +/// never ends a session could see it. +/// +public class AvatarFactorySaveFlushTests : OpenSimTestCase +{ + /// Counts what actually reached the avatar service, which is the thing the bug lost. + private sealed class CountingAvatarService : IAvatarService, ISharedRegionModule + { + public int SetAppearanceCalls; + public readonly List SavedFor = new(); + + public bool SetAppearance(UUID userID, AvatarAppearance appearance) + { + SetAppearanceCalls++; + SavedFor.Add(userID); + return true; + } + + public AvatarAppearance GetAppearance(UUID userID) => null!; + public AvatarData GetAvatar(UUID userID) => null!; + public bool SetAvatar(UUID userID, AvatarData avatar) => true; + public bool ResetAvatar(UUID userID) => true; + public bool SetItems(UUID userID, string[] names, string[] values) => true; + public bool RemoveItems(UUID userID, string[] names) => true; + + public string Name => "CountingAvatarService"; + public Type ReplaceableInterface => null!; + public void Initialise(IConfigSource source) { } + public void PostInitialise() { } + public void Close() { } + public void AddRegion(Scene scene) => scene.RegisterModuleInterface(this); + public void RemoveRegion(Scene scene) { } + public void RegionLoaded(Scene scene) { } + } + + /// Captures what was logged, so "this must never fail silently again" is an assertion. + private sealed class CapturedLog : IDisposable, ILoggerFactory + { + private readonly ILoggerFactory m_previous; + private readonly List<(LogLevel Level, string Message)> m_entries = new(); + + public CapturedLog() + { + m_previous = LoggerProvider.LoggerFactory; + LoggerProvider.LoggerFactory = this; + } + + public List Warnings + { + get + { + List found = new(); + lock (m_entries) + foreach ((LogLevel level, string message) in m_entries) + if (level == LogLevel.Warning) found.Add(message); + return found; + } + } + + public void Dispose() => LoggerProvider.LoggerFactory = m_previous; + ILogger ILoggerFactory.CreateLogger(string categoryName) => new Recorder(this); + void ILoggerFactory.AddProvider(ILoggerProvider provider) { } + + private void Record(LogLevel level, string message) + { + lock (m_entries) m_entries.Add((level, message)); + } + + private sealed class Recorder : ILogger + { + private readonly CapturedLog m_owner; + public Recorder(CapturedLog owner) { m_owner = owner; } + public IDisposable BeginScope(TState state) where TState : notnull => new Scope(); + public bool IsEnabled(LogLevel logLevel) => true; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) + => m_owner.Record(logLevel, formatter(state, exception)); + private sealed class Scope : IDisposable { public void Dispose() { } } + } + } + + private static (AvatarFactoryModule afm, TestScene scene, CountingAvatarService avatars, ScenePresence sp) Setup() + { + UUID userId = TestHelpers.ParseTail(0x1); + + CountingAvatarService avatars = new CountingAvatarService(); + AvatarFactoryModule afm = new AvatarFactoryModule(); + TestScene scene = new SceneHelpers().SetupScene(); + SceneHelpers.SetupSceneModules(scene, avatars, afm); + ScenePresence sp = SceneHelpers.AddScenePresence(scene, userId); + + return (afm, scene, avatars, sp); + } + + // ------------------------------------------------------------------ the fix + + [Fact] + public void A_queued_save_is_written_when_the_presence_closes() + { + (AvatarFactoryModule afm, TestScene scene, CountingAvatarService avatars, ScenePresence sp) = Setup(); + + afm.QueueAppearanceSave(sp.UUID); + Assert.Equal(0, avatars.SetAppearanceCalls); // still only queued: the five-second window + + scene.EventManager.TriggerOnRemovePresence(sp.UUID); + + Assert.Equal(1, avatars.SetAppearanceCalls); + Assert.Equal(sp.UUID, Assert.Single(avatars.SavedFor)); + } + + /// + /// The load-bearing one: the real close path must reach the flush, and must do it while the presence is still + /// resolvable. Scene.RemoveClient raises OnRemovePresence at Scene.cs:3866 and only + /// removes the presence in the finally block at :3898. + /// + [Fact] + public void A_queued_save_survives_a_real_client_close() + { + (AvatarFactoryModule afm, TestScene scene, CountingAvatarService avatars, ScenePresence sp) = Setup(); + UUID userId = sp.UUID; + + afm.QueueAppearanceSave(userId); + scene.RemoveClient(userId, false); + + Assert.Equal(1, avatars.SetAppearanceCalls); + Assert.Equal(userId, Assert.Single(avatars.SavedFor)); + Assert.Null(scene.GetScenePresence(userId)); // and the presence really did go away + } + + [Fact] + public void The_queued_save_is_written_exactly_once() + { + (AvatarFactoryModule afm, TestScene scene, CountingAvatarService avatars, ScenePresence sp) = Setup(); + + afm.QueueAppearanceSave(sp.UUID); + scene.EventManager.TriggerOnRemovePresence(sp.UUID); + scene.EventManager.TriggerOnRemovePresence(sp.UUID); // a second close must not write again + + Assert.Equal(1, avatars.SetAppearanceCalls); + } + + /// + /// The cost guarantee: with the queue empty the flush writes nothing. + /// + /// Entering a region queues a save of its own — SceneHelpers.AddScenePresence ends up in + /// SetAppearance, which queues one. In a running region the timer drains that within + /// m_savetime seconds and the queue is empty long before logout; in this harness the timer never runs, + /// so the entry is drained here explicitly. What is asserted after that is the case the guarantee is about: a + /// close with nothing pending. + /// + [Fact] + public void A_close_with_nothing_queued_writes_nothing() + { + (AvatarFactoryModule _, TestScene scene, CountingAvatarService avatars, ScenePresence sp) = Setup(); + + scene.EventManager.TriggerOnRemovePresence(sp.UUID); // drains the entry that entering the region made + int afterDrain = avatars.SetAppearanceCalls; + + scene.EventManager.TriggerOnRemovePresence(sp.UUID); // nothing pending now + + Assert.Equal(afterDrain, avatars.SetAppearanceCalls); + } + + [Fact] + public void An_unchanged_session_costs_no_write_through_a_real_close() + { + (AvatarFactoryModule _, TestScene scene, CountingAvatarService avatars, ScenePresence sp) = Setup(); + UUID userId = sp.UUID; + + scene.EventManager.TriggerOnRemovePresence(userId); // drain the region-entry save + int afterDrain = avatars.SetAppearanceCalls; + + scene.RemoveClient(userId, false); + + Assert.Equal(afterDrain, avatars.SetAppearanceCalls); + } + + // ------------------------------------------------------------------ the drop must never be silent again + + [Fact] + public void A_drop_warns_rather_than_losing_the_change_silently() + { + (AvatarFactoryModule afm, TestScene scene, CountingAvatarService avatars, ScenePresence _) = Setup(); + UUID ghost = TestHelpers.ParseTail(0x99); // queued, but no presence to read the appearance from + + afm.QueueAppearanceSave(ghost); + + using CapturedLog log = new CapturedLog(); + scene.EventManager.TriggerOnRemovePresence(ghost); + + Assert.Equal(0, avatars.SetAppearanceCalls); + string warning = Assert.Single(log.Warnings.FindAll(w => w.Contains("could not be flushed"))); + Assert.Contains(ghost.ToString(), warning); + Assert.Contains("lost", warning); + } +} diff --git a/Tests/OpenSim.Region.CoreModules.Tests/Avatar/Inventory/Archiver/Tests/InventoryArchiveTestCase.cs b/Tests/OpenSim.Region.CoreModules.Tests/Avatar/Inventory/Archiver/Tests/InventoryArchiveTestCase.cs index eebef28b541..e3047f62c03 100644 --- a/Tests/OpenSim.Region.CoreModules.Tests/Avatar/Inventory/Archiver/Tests/InventoryArchiveTestCase.cs +++ b/Tests/OpenSim.Region.CoreModules.Tests/Avatar/Inventory/Archiver/Tests/InventoryArchiveTestCase.cs @@ -69,6 +69,9 @@ protected UserAccount m_uaLL2 protected string m_item1Name = "Ray Gun Item"; protected string m_coaItemName = "Coalesced Item"; + // Formerly NUnit [TestFixtureSetUp]; the xunit migration (#197) dropped the attribute and nothing called it, + // so m_iarStreamBytes stayed null. xunit builds a fresh instance per test, so it now runs from SetUp(). + // (Docs/feature/repo-audit/T1-TEST-FIXTURES.md) public void FixtureSetup() { // Don't allow tests to be bamboozled by asynchronous events. Execute everything on the same thread. @@ -77,6 +80,7 @@ public void FixtureSetup() ConstructDefaultIarBytesForTestLoad(); } + // Formerly NUnit [TestFixtureTearDown]; now run from Dispose(). public void TearDown() { // We must set this back afterwards, otherwise later tests will fail since they're expecting multiple @@ -85,9 +89,16 @@ public void TearDown() Util.FireAndForgetMethod = Util.DefaultFireAndForgetMethod; } + public override void Dispose() + { + TearDown(); + base.Dispose(); + } + public override void SetUp() { base.SetUp(); + FixtureSetup(); m_iarStream = new MemoryStream(m_iarStreamBytes); } diff --git a/Tests/OpenSim.Region.CoreModules.Tests/World/LightShare/Tests/ViewerEnvironmentWLOSDTests.cs b/Tests/OpenSim.Region.CoreModules.Tests/World/LightShare/Tests/ViewerEnvironmentWLOSDTests.cs new file mode 100644 index 00000000000..40c70b11c1d --- /dev/null +++ b/Tests/OpenSim.Region.CoreModules.Tests/World/LightShare/Tests/ViewerEnvironmentWLOSDTests.cs @@ -0,0 +1,114 @@ +using System.Text; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using Xunit; + +namespace OpenSim.Region.CoreModules.World.LightShare.Tests; + +/// +/// ENV-1. given a body that is not an LLSD array. +/// +/// The defect. The method casts with osd as OSDArray and then guards on +/// if (osd != null) — the parameter, not the cast result. A non-array is still +/// non-null, so the guard passed and null was handed to DayCycle.FromWLOSD(OSDArray), which +/// dereferences array.Count on its first statement (ViewerDaycycle.cs:71) and threw +/// . +/// +/// Why that throw was load-bearing, and why this is not merely tidying. The legacy WindLight +/// setter (EnvironmentModule.SetEnvironmentSettings) parses with the auto-detect entry, holds the result +/// as a bare OSD with no type check, and calls StoreOnRegion(VEnv) — a write. The throw happened +/// to land before that write, so nothing was corrupted. Making DayCycle.FromWLOSD null-tolerant — +/// an obvious-looking hardening — would have removed the throw and let a fresh, default +/// ViewerEnvironment be stored, blanking the region's environment from a truncated request. That is the +/// trap recorded in Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.md §5. +/// +/// And note that fixing the guard alone would have armed it. With the guard corrected, a non-array +/// body makes FromWLOSD a silent no-op rather than a throw — which is the right contract for the method +/// (it returns void and its only other "cannot use this" path is to leave Cycle alone), but it +/// removes the accidental protection the setter was relying on. That is why ENV-1 changes two sites: the +/// guard here, and a type check at the setter boundary. These tests cover the first; the second is a handler +/// reachable only through a cap dispatch with a Scene, estate permissions and a ScenePresence, and there is no +/// harness for it in this project — see the session report. +/// +/// The degenerate value used below is the real one: OSDParser.DeserializeLLSDXml returns a bare +/// OSD with OSDType.Unknown for a truncated body rather than throwing or returning null +/// (AIS-AUDIT-1 §1a). The tests assert against that value, not against a hand-made stand-in. +/// +public class ViewerEnvironmentWLOSDTests +{ + /// What a truncated LLSD XML body actually parses to. Not a stand-in: this is the parser's output. + private static OSD Degenerate() + { + OSD osd = OSDParser.DeserializeLLSDXml(Encoding.UTF8.GetBytes("")); + Assert.NotNull(osd); // the trap: it is NOT null + Assert.Equal(OSDType.Unknown, osd.Type); // and it is NOT a usable type + Assert.Null(osd as OSDArray); // so the cast in FromWLOSD yields null + return osd; + } + + /// A minimally valid WindLight body: the array shape DayCycle.FromWLOSD expects. + private static OSDArray ValidWLBody() + { + var skyTracks = new OSDArray { new OSDArray { OSD.FromReal(0.0), OSD.FromString("sky0") } }; + var skyFrames = new OSDMap { ["sky0"] = new OSDMap { ["sun_angle"] = OSD.FromReal(1.0) } }; + var water = new OSDMap { ["waterFogDensity"] = OSD.FromReal(2.0) }; + return new OSDArray { new OSDMap(), skyTracks, skyFrames, water }; + } + + [Fact] + public void a_degenerate_body_is_refused_without_reaching_DayCycle() + { + var env = new ViewerEnvironment(); + + // Before ENV-1 this threw NullReferenceException from DayCycle.FromWLOSD(null). + // After ENV-1 it is a clean refusal: no exception, and nothing handed downstream. + env.FromWLOSD(Degenerate()); + + // A clean refusal, not a silent success: the environment must not claim to be a parsed legacy one. + Assert.False(env.IsLegacy, "a refused body must not mark the environment as a parsed legacy one"); + } + + [Fact] + public void an_OSDMap_body_is_refused_without_reaching_DayCycle() + { + var env = new ViewerEnvironment(); + + // An OSDMap is a perfectly valid OSD, just not the array shape this method takes - so the old + // `osd != null` guard admitted it too, and it reached DayCycle.FromWLOSD(null) exactly the same way. + env.FromWLOSD(new OSDMap { ["environment"] = new OSDMap() }); + + Assert.False(env.IsLegacy); + } + + /// + /// The property that matters most: a malformed body must not be able to wipe an environment that has + /// already been populated. This is the object-level form of "the region's environment survives". + /// + [Fact] + public void a_degenerate_body_does_not_blank_an_already_populated_environment() + { + var env = new ViewerEnvironment(); + env.FromWLOSD(ValidWLBody()); + byte[] loaded = OSDParser.SerializeLLSDXmlBytes(env.ToOSD()); + + env.FromWLOSD(Degenerate()); + byte[] after = OSDParser.SerializeLLSDXmlBytes(env.ToOSD()); + + Assert.Equal(loaded, after); // byte-identical: the refusal changed nothing + } + + /// A valid array still parses, so the fix is a refusal of bad input and not an outage. + [Fact] + public void a_valid_WindLight_array_still_parses() + { + var fresh = new ViewerEnvironment(); + byte[] before = OSDParser.SerializeLLSDXmlBytes(fresh.ToOSD()); + + var env = new ViewerEnvironment(); + env.FromWLOSD(ValidWLBody()); + byte[] after = OSDParser.SerializeLLSDXmlBytes(env.ToOSD()); + + Assert.NotEqual(before, after); // it really did take the body + } +} diff --git a/Tests/OpenSim.Region.CoreModules.Tests/World/Serialiser/Tests/SerialiserTests.cs b/Tests/OpenSim.Region.CoreModules.Tests/World/Serialiser/Tests/SerialiserTests.cs index 41a0f1f7237..c4a575e86e5 100644 --- a/Tests/OpenSim.Region.CoreModules.Tests/World/Serialiser/Tests/SerialiserTests.cs +++ b/Tests/OpenSim.Region.CoreModules.Tests/World/Serialiser/Tests/SerialiserTests.cs @@ -589,6 +589,14 @@ public class SerialiserTests : OpenSimTestCase protected Scene m_scene; protected SerialiserModule m_serialiserModule; + // Formerly NUnit [SetUp]; the xunit migration (#197) dropped the attribute and nothing called it, so every test + // here ran with m_scene null. Wired back through OpenSimTestCase.SetUp() (Docs/feature/repo-audit/T1-TEST-FIXTURES.md). + public override void SetUp() + { + base.SetUp(); + Init(); + } + private void Init() { m_serialiserModule = new SerialiserModule(); diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AppearanceServiceTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AppearanceServiceTests.cs new file mode 100644 index 00000000000..35d39567a84 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AppearanceServiceTests.cs @@ -0,0 +1,205 @@ +using Nini.Config; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSim.Services.AvatarService; +using OpenSim.Services.Interfaces; +using OpenSimNGC.Appearance.Baking; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S4 Part 1 — the agent_appearance_service read path (ADR-002), against the real +/// over a fake avatar service and a fake asset service. +/// +public class AppearanceServiceTests +{ + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + + private sealed class TestableAppearanceService : AppearanceService + { + public TestableAppearanceService(IAvatarService avatars, IAssetService assets) + : base(new IniConfigSource(), avatars, assets) { } + } + + /// An agent with a head bake stored in the ADR-004 index, and the asset behind it. + private static (TestableAppearanceService Service, FakeAvatarService Avatars, FakeAssetService Assets, UUID HeadAsset) Rig() + { + var avatars = new FakeAvatarService(); + var assets = new FakeAssetService(); + var headAsset = UUID.Random(); + + assets.Put(new AssetBase(headAsset, $"bake:{Agent}:head", (sbyte)AssetType.Texture, Agent.ToString()) + { + Data = new byte[] { 0xFF, 0x4F, 0xFF, 0x51, 1, 2, 3, 4 }, + }); + BakeIndex.Write(avatars, Agent, + new[] { new KeyValuePair(BakeChannel.Head, new StoredBake(headAsset, new string('a', 64))) }, + cofVersion: 7, bakeSize: 1024, updatedUtc: DateTime.UtcNow); + + return (new TestableAppearanceService(avatars, assets), avatars, assets, headAsset); + } + + // ------------------------------------------------------------------ the hit + + [Fact] + public void AKnownAgentAndChannelReturnsTheAssetBytes() + { + var (svc, _, assets, headAsset) = Rig(); + + var got = svc.GetBake(Agent, "head", headAsset); + + Assert.NotNull(got); + Assert.Equal(headAsset, got.FullID); + Assert.Equal(assets.GetUnchecked(headAsset.ToString()).Data, got.Data); + } + + [Fact] + public void TheChannelTokenIsMatchedCaseInsensitively() + { + var (svc, _, _, headAsset) = Rig(); + + Assert.NotNull(svc.GetBake(Agent, "head", headAsset)); + Assert.NotNull(svc.GetBake(Agent, "HEAD", headAsset)); + Assert.NotNull(svc.GetBake(Agent, "Head", headAsset)); + } + + // ------------------------------------------------------------------ the 404s + + /// + /// The rule that keeps this route from becoming a way to fetch arbitrary assets, and from painting an avatar + /// with a bake the viewer did not ask for: the UUID in the path is checked against the index, not used as the + /// lookup key. + /// + [Fact] + public void AUuidThatDoesNotMatchTheIndexIs404() + { + var (svc, _, assets, headAsset) = Rig(); + + // a real, fetchable asset that simply is not this agent's head bake + var other = UUID.Random(); + assets.Put(new AssetBase(other, "someone else", (sbyte)AssetType.Texture, UUID.Random().ToString()) { Data = new byte[] { 9, 9, 9 } }); + + Assert.Null(svc.GetBake(Agent, "head", other)); + Assert.Null(svc.GetBake(Agent, "head", UUID.Zero)); + Assert.Null(svc.GetBake(Agent, "head", UUID.Random())); + // and the one that does match still works, so the refusal is about the mismatch and nothing else + Assert.NotNull(svc.GetBake(Agent, "head", headAsset)); + } + + [Fact] + public void AnAgentWithNoIndexIs404() + { + var (svc, _, _, _) = Rig(); + var stranger = UUID.Random(); + + Assert.Null(svc.GetBake(stranger, "head", UUID.Random())); + Assert.Null(svc.GetBake(UUID.Zero, "head", UUID.Random())); + } + + [Fact] + public void AChannelTheAgentHasNoBakeForIs404() + { + var (svc, _, _, headAsset) = Rig(); + + // the rig stores only Head; every other channel has no index entry + foreach (var token in new[] { "upper", "lower", "eyes", "skirt", "hair", "leftarm", "leftleg", "aux1", "aux2", "aux3" }) + Assert.Null(svc.GetBake(Agent, token, headAsset)); + } + + [Fact] + public void AnUnknownChannelTokenIs404() + { + var (svc, _, _, headAsset) = Rig(); + + foreach (var token in new[] { "", "0", "8", "head-baked", "torso", "../head", "head/" }) + Assert.Null(svc.GetBake(Agent, token, headAsset)); + } + + /// An index entry pointing at an asset the asset service has lost answers 404, not an empty body. + [Fact] + public void AnIndexPointingAtAMissingAssetIs404() + { + var (svc, _, assets, headAsset) = Rig(); + Assert.True(assets.Remove(headAsset.ToString())); + + Assert.Null(svc.GetBake(Agent, "head", headAsset)); + } + + // ------------------------------------------------------------------ the token set + + /// + /// is transcribed from the viewer and cannot reference the bake library + /// (Robust does not ship it). This is the pin that keeps the two in step: every has + /// a token, every token maps back to a channel, and the token is the channel name lower-cased — which is also + /// what puts in a stored bake's asset name. + /// + [Fact] + public void EveryBakeChannelHasTheTokenTheViewerSends() + { + var channels = Enum.GetValues(); + Assert.Equal(channels.Length, AppearanceChannels.Tokens.Count); + + foreach (var ch in channels) + { + var token = ch.ToString().ToLowerInvariant(); + Assert.Contains(token, AppearanceChannels.Tokens); + Assert.Equal(ch.ToString(), AppearanceChannels.IndexNameFor(token)); + Assert.Equal(BakeIndex.BakeKey(ch), AppearanceChannels.BakeKeyFor(token)); + Assert.EndsWith(token, BakeOrchestrator.AssetNameFor(Agent, ch)); + } + + // the exact eleven from llavatarappearancedefines.cpp:81-91, in baked-texture-index order + Assert.Equal( + new[] { "head", "upper", "lower", "eyes", "skirt", "hair", "leftarm", "leftleg", "aux1", "aux2", "aux3" }, + AppearanceChannels.Tokens.ToArray()); + } + + /// Every channel round-trips through a real bake index, not just through the token table. + [Fact] + public void EveryChannelResolvesThroughTheStoredIndex() + { + var avatars = new FakeAvatarService(); + var assets = new FakeAssetService(); + var stored = new List>(); + + foreach (var ch in Enum.GetValues()) + { + var id = UUID.Random(); + assets.Put(new AssetBase(id, BakeOrchestrator.AssetNameFor(Agent, ch), (sbyte)AssetType.Texture, Agent.ToString()) + { + Data = new byte[] { (byte)ch, 1, 2, 3 }, + }); + stored.Add(new KeyValuePair(ch, new StoredBake(id, new string('b', 64)))); + } + Assert.True(BakeIndex.Write(avatars, Agent, stored, 1, 1024, DateTime.UtcNow)); + + var svc = new TestableAppearanceService(avatars, assets); + foreach (var kv in stored) + { + var token = kv.Key.ToString().ToLowerInvariant(); + var got = svc.GetBake(Agent, token, kv.Value.AssetId); + Assert.True(got is not null, $"{token} should resolve"); + Assert.Equal(kv.Value.AssetId, got.FullID); + Assert.Equal((byte)kv.Key, got.Data[0]); + } + } + + /// + /// The Q-14 interaction, end to end for this route: an appearance save must not take the index with it, or + /// every bake fetch starts 404ing the moment an agent changes anything. + /// + [Fact] + public void AnAppearanceSaveDoesNotBreakTheFetchRoute() + { + var (svc, avatars, _, headAsset) = Rig(); + Assert.NotNull(svc.GetBake(Agent, "head", headAsset)); + + avatars.SetAppearance(Agent, new AvatarAppearance()); + + // FakeAvatarService.SetAppearance reproduces the real delete-everything-first behaviour, so this is the + // pre-S3 outcome; the real AvatarService preserves the Bake* namespace and is tested separately. + Assert.Null(svc.GetBake(Agent, "head", headAsset)); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AppearanceVersionParamTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AppearanceVersionParamTests.cs new file mode 100644 index 00000000000..24086ec6d2b --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AppearanceVersionParamTests.cs @@ -0,0 +1,231 @@ +using OpenMetaverse; +using OpenSim.Framework; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S4b — the appearance-version parameter. The AppearanceData block S3 added is not sufficient on its own: +/// the viewer reads the version from the block and from visual parameter 11000, prefers the parameter, and +/// throws the whole message away when the two disagree. +/// +/// +/// resolve_appearance_version — both set and different → warns "inconsistent appearance_version +/// settings" and returns false (llvoavatar.cpp:9663-9690). +/// the caller then logs "bad appearance version info, discarding" and returns +/// (llvoavatar.cpp:9720-9723) — no TextureEntry applied, so no bake is ever fetched. +/// the parameter is transmitted as a byte through the parameter's own range, and id 11000 is +/// value_min="0" value_max="255", so the mapping is the identity and the byte must literally equal the +/// block's version (llvoavatar.cpp:9628-9630, :9650-9658). +/// +/// +public class AppearanceVersionParamTests +{ + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + + /// A 253-byte parameter array shaped like the one a viewer actually uploads, with slot 251 at 0. + private static byte[] Params() + { + var vp = new byte[253]; + for (var i = 0; i < vp.Length; i++) vp[i] = (byte)(i * 3 + 7); + vp[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX] = 0; // what a client-baking viewer sends + return vp; + } + + // ------------------------------------------------------------------ the index and the helper + + [Fact] + public void TheParameterIndexIsTheOneTheEnumNames() + { + Assert.Equal(251, AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX); + Assert.Equal((int)AvatarAppearance.VPElement._APPEARANCEMESSAGE_VERSION, AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX); + } + + [Fact] + public void TheHelperSetsOnlyThatByteAndNeverMutatesItsArgument() + { + var source = Params(); + var before = (byte[])source.Clone(); + + var got = AvatarAppearance.WithAppearanceVersion(source, 1); + + Assert.NotSame(source, got); + Assert.Equal(before, source); // argument untouched + Assert.Equal(1, got[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX]); + for (var i = 0; i < source.Length; i++) + if (i != AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX) + Assert.Equal(source[i], got[i]); + } + + [Fact] + public void TheHelperIsAnIdentityWhenThereIsNothingToDo() + { + Assert.Null(AvatarAppearance.WithAppearanceVersion(null, 1)); + + // already correct: same instance, so no allocation and no copy on the common warm path + var already = Params(); + already[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX] = 1; + Assert.Same(already, AvatarAppearance.WithAppearanceVersion(already, 1)); + + // too short to carry the slot: returned as-is. The viewer will not find parameter 11000 among the + // transmitted blocks either, and an absent parameter with a present field resolves to the field. + var stub = new byte[10]; + Assert.Same(stub, AvatarAppearance.WithAppearanceVersion(stub, 1)); + } + + // ------------------------------------------------------------------ the two send paths + + /// + /// The choice ScenePresence.SendAppearanceToAgentNF makes, replayed. Pinned against the source below so it + /// cannot drift. + /// + /// + /// The choice ScenePresence.SendAppearanceToAgentNF makes, replayed: 1 on a copy for an avatar this region + /// baked; the stored array itself otherwise - except for an NPC, which is never baked anywhere and says 0 on a + /// copy (SSB-NPC-1: its stored byte is the owner's, which is 1 on a server-bake region). + /// + private static byte[] ParamsForSend(byte[] stored, int cofVersion, bool isNpc = false) + => cofVersion >= 0 ? AvatarAppearance.WithAppearanceVersion(stored, 1) + : isNpc ? AvatarAppearance.WithAppearanceVersion(stored, 0) + : stored; + + [Fact] + public void OnAnSsbRegionTheParameterAndTheBlockAgree() + { + var stored = Params(); + const int cofVersion = 7; + + var sent = ParamsForSend(stored, cofVersion); + var body = AppearanceBody(sent, cofVersion); + + // the parameter, as the viewer will read it out of the VisualParam blocks + Assert.Equal(1, sent[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX]); + + // the block, as the viewer will read it out of AppearanceData + var at = 16 + 1 + 2 + 0 + 1 + sent.Length; + Assert.Equal(1, body[at]); // one AppearanceData block + Assert.Equal(1, body[at + 1]); // AppearanceVersion + Assert.Equal(cofVersion, BitConverter.ToInt32(body, at + 2)); + + // agreement is the whole point: parameter == field, so resolve_appearance_version returns true + Assert.Equal(sent[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX], body[at + 1]); + } + + [Fact] + public void OnAFlagOffRegionNothingAboutTheAppearanceChanges() + { + var stored = Params(); + var storedBefore = (byte[])stored.Clone(); + + var sent = ParamsForSend(stored, -1); + + // the very same array goes out, so the parameters cannot differ by construction + Assert.Same(stored, sent); + Assert.Equal(storedBefore, stored); + Assert.Equal(0, sent[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX]); + + // and the packet body is byte-identical to the pre-S3 form + Assert.Equal(AppearanceBodyBeforeS3(stored), AppearanceBody(sent, -1)); + } + + /// + /// A sim-baked avatar and a client-baked one on the same simulator must not interfere: the helper's copy means + /// the stored parameters are never touched, so an agent that walks from a flag-on region to a flag-off one + /// still sends its own 0. + /// + [Fact] + public void SendingOnAnSsbRegionDoesNotChangeWhatALaterFlagOffSendCarries() + { + var stored = Params(); + + var ssb = ParamsForSend(stored, 3); + Assert.Equal(1, ssb[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX]); + + var off = ParamsForSend(stored, -1); + Assert.Same(stored, off); + Assert.Equal(0, off[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX]); + Assert.Equal(AppearanceBodyBeforeS3(stored), AppearanceBody(off, -1)); + } + + /// + /// SSB-NPC-1: an NPC's stored parameters are a clone of its owner's, so on a server-bake region slot 251 holds + /// the owner's 1 - while the region never baked the NPC and sends no block. A 1 beside no block sends the viewer + /// to the appearance service under the NPC's UUID, where there is no index. The NPC's message says 0, on a + /// copy: the stored clone is untouched, and the body is the pre-S3 form for the same parameters with 0 in it. + /// + [Fact] + public void AnUnbakedNpcSaysZeroOnACopyWhateverItsOwnerStored() + { + var stored = Params(); + stored[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX] = 1; // the owner's byte, inherited by the clone + var storedBefore = (byte[])stored.Clone(); + + var sent = ParamsForSend(stored, -1, isNpc: true); + + Assert.NotSame(stored, sent); + Assert.Equal(storedBefore, stored); + Assert.Equal(0, sent[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX]); + var expected = (byte[])stored.Clone(); expected[AvatarAppearance.APPEARANCE_VERSION_PARAM_INDEX] = 0; + Assert.Equal(AppearanceBodyBeforeS3(expected), AppearanceBody(sent, -1)); + + // and a human with the same stored byte and no bake here is still sent as-is: the exception is the NPC alone + Assert.Same(stored, ParamsForSend(stored, -1)); + } + + /// The replay above is only worth anything if it matches the shipped code. + [Fact] + public void TheReplayedChoiceMatchesWhatScenePresenceActuallyDoes() + { + var root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + var src = File.ReadAllText(Path.Combine(root, "Source", "OpenSim.Region.Framework", "Scenes", "ScenePresence.cs")).Replace("\r\n", "\n"); + + Assert.Contains("byte[] visualParams;\n if (cofVersion >= 0)\n visualParams = AvatarAppearance.WithAppearanceVersion(Appearance.VisualParams, 1);\n else if (IsNPC)", src); + Assert.Contains(" visualParams = AvatarAppearance.WithAppearanceVersion(Appearance.VisualParams, 0);\n else\n visualParams = Appearance.VisualParams;", src); + Assert.Contains("avatar.ControllingClient.SendAppearance(UUID, visualParams, Appearance.Texture.GetBakesBytes(), Appearance.AvatarPreferencesHoverZ, cofVersion);", src); + // the stored array must never be written through + Assert.DoesNotContain("Appearance.VisualParams[", src); + } + + // ------------------------------------------------------------------ the packet body, as LLClientView writes it + + private static byte[] AppearanceBody(byte[] visualParams, int cofVersion) + { + var data = new byte[4096]; + int pos = 0; + Agent.ToBytes(data, pos); pos += 16; + data[pos++] = 0; + data[pos++] = 0; data[pos++] = 0; // empty TextureEntry + data[pos++] = (byte)visualParams.Length; + Buffer.BlockCopy(visualParams, 0, data, pos, visualParams.Length); pos += visualParams.Length; + if (cofVersion < 0) data[pos++] = 0; + else + { + data[pos++] = 1; + data[pos++] = 1; + Utils.IntToBytesSafepos(cofVersion, data, pos); pos += 4; + Utils.UIntToBytesSafepos(0, data, pos); pos += 4; + } + data[pos++] = 1; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + return data[..pos]; + } + + private static byte[] AppearanceBodyBeforeS3(byte[] visualParams) + { + var data = new byte[4096]; + int pos = 0; + Agent.ToBytes(data, pos); pos += 16; + data[pos++] = 0; + data[pos++] = 0; data[pos++] = 0; + data[pos++] = (byte)visualParams.Length; + Buffer.BlockCopy(visualParams, 0, data, pos, visualParams.Length); pos += visualParams.Length; + data[pos++] = 0; // "// no AppearanceData" + data[pos++] = 1; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + return data[..pos]; + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AvatarRecordWearableRangeTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AvatarRecordWearableRangeTests.cs new file mode 100644 index 00000000000..09cf18d4a8d --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/AvatarRecordWearableRangeTests.cs @@ -0,0 +1,98 @@ +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Services.Interfaces; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S11. The Avatars writer stops at wearable type 14, so Physics (15) and Universal (16) are never +/// persisted — however faithfully the rest of the stack carries them. +/// +/// +/// Observed on Ebony (1.1.258): Truly wears a Ruth2 v4 Physics Default and, since 2026-09-06 20:19 UTC, a +/// Universal. Both are linked in the COF, both are derived into the presence's wearables by S10, and both reach +/// the bake — the 20:27:33 line has LeftArm=Baked and Aux1=Baked, which only a Universal feeds. +/// The record for a7d2ff2e-dc32-44d8-aa61-3d22070a4964 has never held a Wearable 15:* or +/// Wearable 16:* row, across several deferred saves. +/// +/// +/// +/// S10 fixed the reader for types at or beyond the initial array length; this is the write-side counterpart it +/// never had. +/// +/// +public class AvatarRecordWearableRangeTests +{ + private const int Shirt = 4; // WearableType.Shirt + private const int Physics = 15; // AvatarWearable.PHYSICS / LLWearableType::WT_PHYSICS + private const int Universal = 16; // AvatarWearable.UNIVERSAL / LLWearableType::WT_UNIVERSAL + + private static AvatarWearable[] FullWidth() + { + var w = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < w.Length; i++) w[i] = new AvatarWearable(); + return w; + } + + [Fact] + public void TheRecordCarriesEveryWornTypeIncludingPhysicsAndUniversal() + { + var shirt0 = UUID.Random(); var shirt0Asset = UUID.Random(); + var shirt1 = UUID.Random(); var shirt1Asset = UUID.Random(); + var phys = UUID.Random(); var physAsset = UUID.Random(); + var uni = new UUID("32f07ed9-0000-4000-8000-000000000001"); var uniAsset = UUID.Random(); + + var wearables = FullWidth(); + wearables[Shirt].Add(shirt0, shirt0Asset); + wearables[Shirt].Add(shirt1, shirt1Asset); + wearables[Physics].Add(phys, physAsset); + wearables[Universal].Add(uni, uniAsset); + + var appearance = new AvatarAppearance { Wearables = wearables }; + + var data = new AvatarData(appearance); + + // The writer's own rows: every worn type, keyed "Wearable :". + Assert.Equal($"{shirt0}:{shirt0Asset}", data.Data["Wearable 4:0"]); + Assert.Equal($"{shirt1}:{shirt1Asset}", data.Data["Wearable 4:1"]); + Assert.True(data.Data.ContainsKey("Wearable 15:0"), "no Wearable 15:0 row: the Physics layer was not persisted"); + Assert.True(data.Data.ContainsKey("Wearable 16:0"), "no Wearable 16:0 row: the Universal layer was not persisted"); + Assert.Equal($"{phys}:{physAsset}", data.Data["Wearable 15:0"]); + Assert.Equal($"{uni}:{uniAsset}", data.Data["Wearable 16:0"]); + + // Exactly the four worn slots, and nothing invented for the empty ones. + Assert.Equal(4, data.Data.Keys.Count(k => k.StartsWith("Wearable "))); + + // And it reads back, in index order, at every type (the S10 reader). + var back = data.ToAvatarAppearance(); + + Assert.Equal(2, back.Wearables[Shirt].Count); + Assert.Equal(shirt0, back.Wearables[Shirt][0].ItemID); + Assert.Equal(shirt1, back.Wearables[Shirt][1].ItemID); + Assert.True(back.Wearables.Length > Universal, $"the read-back set is only {back.Wearables.Length} slots wide"); + Assert.Equal(1, back.Wearables[Physics].Count); + Assert.Equal(phys, back.Wearables[Physics][0].ItemID); + Assert.Equal(physAsset, back.Wearables[Physics][0].AssetID); + Assert.Equal(1, back.Wearables[Universal].Count); + Assert.Equal(uni, back.Wearables[Universal][0].ItemID); + Assert.Equal(uniAsset, back.Wearables[Universal][0].AssetID); + } + + [Fact] + public void ARecordOfTheLegacyWidthStillRoundTrips() + { + // A 15-slot appearance is what AvatarAppearance's own constructor and ClearWearables produce + // (AvatarAppearance.cs:320-325), and the writer must not walk off the end of one. + var wearables = new AvatarWearable[AvatarWearable.LEGACY_VERSION_MAX_WEARABLES]; + for (var i = 0; i < wearables.Length; i++) wearables[i] = new AvatarWearable(); + var tattoo = UUID.Random(); var tattooAsset = UUID.Random(); + wearables[14].Add(tattoo, tattooAsset); + + var data = new AvatarData(new AvatarAppearance { Wearables = wearables }); + + Assert.Equal($"{tattoo}:{tattooAsset}", data.Data["Wearable 14:0"]); + Assert.Equal(1, data.Data.Keys.Count(k => k.StartsWith("Wearable "))); + Assert.Equal(tattoo, data.ToAvatarAppearance().Wearables[14][0].ItemID); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeCostTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeCostTests.cs new file mode 100644 index 00000000000..44dca1f7a20 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeCostTests.cs @@ -0,0 +1,131 @@ +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSimNGC.Appearance.Baking; +using Xunit; +using Xunit.Abstractions; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S2 Part 2 — Ledger Q-10: where does the bake second go? The live cold bake was 2788 ms for five channels at +/// BakeSize 1024 (Truly) and 3078 ms (Aleric), with no instrumentation to say which phase owned it. This runs the +/// same two outfits through the same orchestrator at the same size with attached, cold +/// and then warm, and writes the split to Golden/last-run-cost-<set>.txt next to the golden harness's +/// own reports. +/// +/// +/// What this measures and what it does not. Decode, composite and encode are the real thing: the same +/// library code, the same textures, the same size, the same quality the module ships. The two I/O phases are not: +/// the asset service here is a , a dictionary in memory, so its fetch and store +/// figures are a floor of roughly zero rather than an estimate of what MySQL and Robust cost. That is exactly what +/// makes the arithmetic useful — the CPU phases are measured, so on the live sim asset I/O is whatever is left +/// over, and the run's own INFO line now prints both halves. +/// +/// +/// It asserts only what must not regress: a warm run composites nothing and its whole cost is a rounding error. +/// +public class BakeCostTests +{ + private readonly ITestOutputHelper _out; + public BakeCostTests(ITestOutputHelper output) { _out = output; } + + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + + /// Production settings: ADR-008's shipped size and the module's default encode quality. + private const int LiveBakeSize = 1024; + private const double LiveQuality = 0.85; + + private static string GoldenDir([CallerFilePath] string here = "") + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "..", "..", "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden")); + + public static IEnumerable Sets() + => new[] { new object[] { "truly-stock" }, new object[] { "aleric-max" } }; + + [Theory] + [MemberData(nameof(Sets))] + public void where_the_bake_second_goes(string set) + { + var fixtures = Path.Combine(GoldenDir(), set, "fixtures"); + if (!File.Exists(Path.Combine(fixtures, "avatar.json"))) + { + _out.WriteLine($"SKIPPED [{set}]: no fixtures at {fixtures}; run Golden/fetch-fixtures.sh {set}"); + return; + } + + var assets = new FakeAssetService(); + foreach (var f in Directory.GetFiles(fixtures)) + { + var ext = Path.GetExtension(f); + sbyte type = ext switch { ".bodypart" => (sbyte)AssetType.Bodypart, ".clothing" => (sbyte)AssetType.Clothing, ".j2c" => (sbyte)AssetType.Texture, _ => -1 }; + if (type < 0) continue; + var id = Path.GetFileNameWithoutExtension(f); + assets.Put(new AssetBase(new UUID(id), id, type, Agent.ToString()) { Data = File.ReadAllBytes(f) }); + } + using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(fixtures, "avatar.json"))); + var wearables = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < wearables.Length; i++) wearables[i] = new AvatarWearable(); + foreach (var w in doc.RootElement.GetProperty("wearables").EnumerateArray()) + wearables[w.GetProperty("type").GetInt32()].Add(new UUID(w.GetProperty("itemId").GetString()), new UUID(w.GetProperty("assetId").GetString())); + var vp = doc.RootElement.GetProperty("visualParams").EnumerateArray().Select(e => (byte)e.GetInt32()).ToArray(); + + var compositor = new TexLayerCompositor(); + var avatars = new FakeAvatarService(); + var appearance = new AvatarAppearance(); + + // The compositor lazily loads avatar_lad.xml and 56 mask TGAs on first use and caches resampled masks. + // On the live sim that happens once per region lifetime, not once per bake, so it is warmed away here + // rather than charged to the cold bake — and how much it is, is reported. + var warmup = System.Diagnostics.Stopwatch.StartNew(); + BakeOrchestrator.Run(Agent, BakeReason.Console, wearables, vp, new AvatarAppearance(), assets, null, + new SkiaBakeBackend(compositor) { Quality = LiveQuality }, compositor, LiveBakeSize, 0, CancellationToken.None); + warmup.Stop(); + + var cold = BakeOrchestrator.Run(Agent, BakeReason.Console, wearables, vp, appearance, assets, avatars, + new SkiaBakeBackend(compositor) { Quality = LiveQuality }, compositor, LiveBakeSize, 7, CancellationToken.None); + var warm = BakeOrchestrator.Run(Agent, BakeReason.Console, wearables, vp, appearance, assets, avatars, + new SkiaBakeBackend(compositor) { Quality = LiveQuality }, compositor, LiveBakeSize, 7, CancellationToken.None); + + var report = new StringBuilder(); + report.AppendLine($"bake cost run {DateTimeOffset.Now:O} set={set} size={LiveBakeSize} quality={LiveQuality}"); + report.AppendLine($"asset service: in-memory fake, so the fetch and store columns are a floor (~0), not a grid figure"); + report.AppendLine($"compositor first-use warm-up (avatar_lad.xml + 56 mask TGAs + a full bake), charged to neither run below: {warmup.ElapsedMilliseconds} ms"); + report.AppendLine(); + Row(report, "cold", cold); + Row(report, "warm", warm); + report.AppendLine(); + report.AppendLine($"cold: {cold.Count(ChannelStatus.Baked)} baked, {cold.Count(ChannelStatus.Reused)} reused — {cold.Timings.Summary}"); + report.AppendLine($"warm: {warm.Count(ChannelStatus.Baked)} baked, {warm.Count(ChannelStatus.Reused)} reused — {warm.Timings.Summary}"); + + var text = report.ToString(); + File.WriteAllText(Path.Combine(GoldenDir(), $"last-run-cost-{set}.txt"), text); + _out.WriteLine(text); + + // the point of the whole slice: a warm bake composites nothing + Assert.Equal(0, warm.Count(ChannelStatus.Baked)); + Assert.Equal(cold.Count(ChannelStatus.Baked), warm.Count(ChannelStatus.Reused)); + Assert.Equal(0, warm.Timings.ChannelsComposited); + Assert.Equal(0, warm.Timings.TexturesDecoded); + Assert.Equal(0, warm.Timings.AssetsStored); + Assert.True(warm.ElapsedMs < Math.Max(50, cold.ElapsedMs / 4), + $"a warm bake should be a small fraction of a cold one: {warm.ElapsedMs} ms against {cold.ElapsedMs} ms"); + } + + private static void Row(StringBuilder sb, string label, BakeOutcome o) + { + var t = o.Timings; + var other = Math.Max(0, o.ElapsedMs - (long)t.Accounted.TotalMilliseconds); + if (sb.Length > 0 && label == "cold") + sb.AppendLine("run total fetch decode composite encode store other | fetch decode composite encode store (% of total)"); + string Pct(TimeSpan v) => o.ElapsedMs == 0 ? "-" : (100.0 * v.TotalMilliseconds / o.ElapsedMs).ToString("F1", CultureInfo.InvariantCulture); + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, + "{0,-6} {1,5} {2,7:F0} {3,7:F0} {4,10:F0} {5,7:F0} {6,7:F0} {7,7} | {8,5} {9,6} {10,9} {11,6} {12,5}", + label, o.ElapsedMs, t.AssetFetch.TotalMilliseconds, t.Decode.TotalMilliseconds, t.Composite.TotalMilliseconds, + t.Encode.TotalMilliseconds, t.AssetStore.TotalMilliseconds, other, + Pct(t.AssetFetch), Pct(t.Decode), Pct(t.Composite), Pct(t.Encode), Pct(t.AssetStore))); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeIndexSurvivesAppearanceSaveTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeIndexSurvivesAppearanceSaveTests.cs new file mode 100644 index 00000000000..80b19ed61b4 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeIndexSurvivesAppearanceSaveTests.cs @@ -0,0 +1,244 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Data; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSim.Services.AvatarService; +using OpenSim.Services.Interfaces; +using OpenSimNGC.Appearance.Baking; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S3 Part 0 — Ledger Q-14. Against the real , not a fake of it: an appearance +/// save must leave the ADR-004 bake index alone, and a bake after a save must still reuse. +/// +public class BakeIndexSurvivesAppearanceSaveTests +{ + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + + /// + /// An in-memory shaped like the real Avatars table: one row per + /// (PrincipalID, Name), an upsert on that pair, Delete("PrincipalID", id) removing + /// every row for the principal. cannot be used here — it keeps + /// one object per principal and its Store overwrites the whole thing, so it loses every key but the + /// last, which is not how MySQL/PGSQL/SQLite behave (MySQLGenericTableHandler.Store is a REPLACE INTO + /// on the primary key (PrincipalID, Name)). + /// + private sealed class RowTable : IAvatarData + { + public readonly Dictionary<(UUID, string), string> Rows = new(); + public int DeleteAllCalls; + + public AvatarBaseData[] Get(string field, string val) + { + if (field != "PrincipalID" || !UUID.TryParse(val, out var id)) return Array.Empty(); + return Rows.Where(r => r.Key.Item1 == id) + .Select(r => new AvatarBaseData + { + PrincipalID = id, + Data = new Dictionary { ["Name"] = r.Key.Item2, ["Value"] = r.Value }, + }) + .ToArray(); + } + + public bool Store(AvatarBaseData data) + { + Rows[(data.PrincipalID, data.Data["Name"])] = data.Data["Value"]; + return true; + } + + public bool Delete(UUID principalID, string name) => Rows.Remove((principalID, name)); + + public bool Delete(string field, string val) + { + if (field != "PrincipalID" || !UUID.TryParse(val, out var id)) return false; + DeleteAllCalls++; + foreach (var k in Rows.Keys.Where(k => k.Item1 == id).ToList()) Rows.Remove(k); + return true; + } + + public Dictionary KeysOf(UUID id) + => Rows.Where(r => r.Key.Item1 == id).ToDictionary(r => r.Key.Item2, r => r.Value); + } + + /// The real service over the row table, through the S3 test seam. + private sealed class TestableAvatarService : AvatarService + { + public TestableAvatarService(IAvatarData db) : base(new IniConfigSource(), db) { } + } + + private static AvatarAppearance AppearanceWearing(params (WearableType Type, UUID Item, UUID Asset)[] worn) + { + var a = new AvatarAppearance(); + a.ClearWearables(); + foreach (var (type, item, asset) in worn) a.Wearables[(int)type].Add(item, asset); + a.SetAttachment(5, UUID.Random(), UUID.Random()); + return a; + } + + // ------------------------------------------------------------------ 1. the delete is still load-bearing + + /// + /// The reason SetAvatar deletes everything first, pinned so the Q-14 fix cannot be "just stop + /// deleting". Take a shirt off and its Wearable row must go with it; leave the row and + /// ToAvatarAppearance puts the shirt back on, because it reads those keys additively. + /// + [Fact] + public void AnAppearanceSaveStillDropsTheKeysTheNewAppearanceNoLongerHas() + { + var db = new RowTable(); + var svc = new TestableAvatarService(db); + var shirtItem = UUID.Random(); + + Assert.True(svc.SetAppearance(Agent, AppearanceWearing((WearableType.Shirt, shirtItem, UUID.Random())))); + Assert.Contains(db.KeysOf(Agent).Keys, k => k.StartsWith("Wearable 4:")); + + // shirt off + Assert.True(svc.SetAppearance(Agent, AppearanceWearing())); + + Assert.DoesNotContain(db.KeysOf(Agent).Keys, k => k.StartsWith("Wearable 4:")); + var shirtSlot = svc.GetAppearance(Agent).Wearables[(int)WearableType.Shirt]; + for (var j = 0; j < shirtSlot.Count; j++) + Assert.NotEqual(shirtItem, shirtSlot[j].ItemID); + } + + // ------------------------------------------------------------------ 2. the index survives + + [Fact] + public void AnAppearanceSaveLeavesTheBakeIndexIntact() + { + var db = new RowTable(); + var svc = new TestableAvatarService(db); + var head = new StoredBake(UUID.Random(), new string('a', 64)); + var upper = new StoredBake(UUID.Random(), new string('b', 64)); + + Assert.True(BakeIndex.Write(svc, Agent, + new[] + { + new KeyValuePair(BakeChannel.Head, head), + new KeyValuePair(BakeChannel.Upper, upper), + }, + cofVersion: 42, bakeSize: 1024, updatedUtc: DateTime.UtcNow)); + + var before = BakeIndex.Read(svc, Agent); + Assert.Equal(2, before.Bakes.Count); + + // an ordinary appearance save, exactly as AvatarFactoryModule.SaveAppearance does it + Assert.True(svc.SetAppearance(Agent, AppearanceWearing((WearableType.Shirt, UUID.Random(), UUID.Random())))); + + var after = BakeIndex.Read(svc, Agent); + Assert.Equal(1024, after.Size); + Assert.Equal(42, after.CofVersion); + Assert.Equal(before.UpdatedUtc, after.UpdatedUtc); + Assert.Equal(2, after.Bakes.Count); + Assert.Equal(head, after.Bakes[BakeChannel.Head]); + Assert.Equal(upper, after.Bakes[BakeChannel.Upper]); + + // and the appearance itself really was rewritten, so this is not passing by doing nothing + Assert.Equal(1, db.DeleteAllCalls); + Assert.Contains(db.KeysOf(Agent).Keys, k => k.StartsWith("Wearable 4:")); + } + + /// + /// Every appearance key the avatar service writes or reads must fall outside the preserved namespace, + /// otherwise preserving it would resurrect appearance the delete was there to remove. + /// + [Fact] + public void NoAppearanceKeyIsCaughtByThePreservedPrefix() + { + var db = new RowTable(); + var svc = new TestableAvatarService(db); + Assert.True(svc.SetAppearance(Agent, AppearanceWearing((WearableType.Shirt, UUID.Random(), UUID.Random())))); + + Assert.NotEmpty(db.KeysOf(Agent)); + Assert.All(db.KeysOf(Agent).Keys, k => Assert.False(AvatarDataKeys.IsPreserved(k), k)); + + // the legacy names ToAvatarAppearance also reads, which are never written by AvatarData(AvatarAppearance) + foreach (var legacy in new[] { "BodyItem", "BodyAsset", "SkinItem", "ShirtItem", "PantsAsset", "SkirtItem", "AvatarType", "Serial", "AvatarHeight", "VisualParams" }) + Assert.False(AvatarDataKeys.IsPreserved(legacy), legacy); + + // and every key the bake index writes must be inside it + foreach (var ch in Enum.GetValues()) + { + Assert.True(AvatarDataKeys.IsPreserved(BakeIndex.BakeKey(ch)), BakeIndex.BakeKey(ch)); + Assert.True(AvatarDataKeys.IsPreserved(BakeIndex.HashKey(ch)), BakeIndex.HashKey(ch)); + } + foreach (var k in new[] { BakeIndex.CofVersionKey, BakeIndex.SizeKey, BakeIndex.UpdatedKey }) + Assert.True(AvatarDataKeys.IsPreserved(k), k); + } + + /// A grid that has never baked must see exactly the old behaviour: one delete, nothing else. + [Fact] + public void WithNoBakeIndexTheSaveIsTheSameSingleDeleteItAlwaysWas() + { + var db = new RowTable(); + var svc = new TestableAvatarService(db); + + Assert.True(svc.SetAppearance(Agent, AppearanceWearing((WearableType.Pants, UUID.Random(), UUID.Random())))); + Assert.True(svc.SetAppearance(Agent, AppearanceWearing())); + + Assert.Equal(2, db.DeleteAllCalls); + Assert.All(db.KeysOf(Agent).Keys, k => Assert.False(AvatarDataKeys.IsPreserved(k))); + } + + // ------------------------------------------------------------------ 3. a bake after a save still reuses + + private static string FixtureDir([CallerFilePath] string here = "") + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "..", "..", "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden", "truly-stock", "fixtures")); + + private const string SkipNote = "SKIPPED: golden fixtures not fetched (Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/fixtures)"; + + /// + /// The whole point of Q-14, end to end: bake, let an appearance save run through the real service, bake again. + /// Before the fix the second run recomposited all five channels. Now it reuses all five. + /// + [Fact] + public void ABakeAfterAnAppearanceSaveStillReuses() + { + if (!File.Exists(Path.Combine(FixtureDir(), "avatar.json"))) { Console.WriteLine(SkipNote); return; } + + var dir = FixtureDir(); + var assets = new FakeAssetService(); + foreach (var f in Directory.GetFiles(dir)) + { + var ext = Path.GetExtension(f); + sbyte type = ext switch { ".bodypart" => (sbyte)AssetType.Bodypart, ".clothing" => (sbyte)AssetType.Clothing, ".j2c" => (sbyte)AssetType.Texture, _ => -1 }; + if (type < 0) continue; + var id = Path.GetFileNameWithoutExtension(f); + assets.Put(new AssetBase(new UUID(id), id, type, Agent.ToString()) { Data = File.ReadAllBytes(f) }); + } + using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(dir, "avatar.json"))); + var wearables = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < wearables.Length; i++) wearables[i] = new AvatarWearable(); + foreach (var w in doc.RootElement.GetProperty("wearables").EnumerateArray()) + wearables[w.GetProperty("type").GetInt32()].Add(new UUID(w.GetProperty("itemId").GetString()), new UUID(w.GetProperty("assetId").GetString())); + var vp = doc.RootElement.GetProperty("visualParams").EnumerateArray().Select(e => (byte)e.GetInt32()).ToArray(); + + var compositor = new TexLayerCompositor(); + var svc = new TestableAvatarService(new RowTable()); + var appearance = new AvatarAppearance(); + + BakeOutcome Bake() => BakeOrchestrator.Run(Agent, BakeReason.Console, wearables, vp, appearance, assets, svc, + new SkiaBakeBackend(compositor) { Quality = 0.5 }, compositor, 128, 7, CancellationToken.None); + + var first = Bake(); + Assert.Equal(5, first.Count(ChannelStatus.Baked)); + Assert.True(first.IndexWritten); + + // the appearance save that used to destroy the index + Assert.True(svc.SetAppearance(Agent, appearance)); + + assets.ResetOps(); // Stored accumulates across a run; the first bake's five are not the claim here + var second = Bake(); + Assert.Equal(5, second.Count(ChannelStatus.Reused)); + Assert.Equal(0, second.Count(ChannelStatus.Baked)); + Assert.Empty(assets.Stored); + foreach (var ch in new[] { BakeChannel.Head, BakeChannel.Upper, BakeChannel.Lower, BakeChannel.Eyes, BakeChannel.Hair }) + Assert.Equal(first.Channels.Single(c => c.Channel == ch).AssetId, + second.Channels.Single(c => c.Channel == ch).AssetId); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeOrchestratorTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeOrchestratorTests.cs new file mode 100644 index 00000000000..5adbafb4974 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeOrchestratorTests.cs @@ -0,0 +1,279 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSimNGC.Appearance.Baking; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// Seam tests for (S1 Part 2): no Scene, no ScenePresence, a fake asset service. +/// Tests 1 and 3 use Truly Bazar's golden fixtures (fetched, never committed) and skip when they are absent. +/// +public class BakeOrchestratorTests +{ + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + + private static string FixtureDir([CallerFilePath] string here = "") + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "..", "..", "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden", "truly-stock", "fixtures")); + + // xunit 2.9 has no dynamic skip: absent fixtures make tests 1 and 3 vacuous passes that say so on the console. + private const string SkipNote = "SKIPPED: golden fixtures not fetched (Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/fixtures)"; + + private static bool FixturesPresent => File.Exists(Path.Combine(FixtureDir(), "avatar.json")); + + /// Loads every fixture file as an asset and the avatar's wearable table + visual params. + private static (FakeAssetService assets, AvatarWearable[] wearables, byte[] visualParams) LoadFixtures() + { + var dir = FixtureDir(); + var assets = new FakeAssetService(); + foreach (var f in Directory.GetFiles(dir)) + { + var ext = Path.GetExtension(f); + sbyte type = ext switch { ".bodypart" => (sbyte)AssetType.Bodypart, ".clothing" => (sbyte)AssetType.Clothing, ".j2c" => (sbyte)AssetType.Texture, _ => -1 }; + if (type < 0) continue; + var id = Path.GetFileNameWithoutExtension(f); + assets.Put(new AssetBase(new UUID(id), id, type, Agent.ToString()) { Data = File.ReadAllBytes(f) }); + } + using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(dir, "avatar.json"))); + var wearables = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < wearables.Length; i++) wearables[i] = new AvatarWearable(); + foreach (var w in doc.RootElement.GetProperty("wearables").EnumerateArray()) + wearables[w.GetProperty("type").GetInt32()].Add(new UUID(w.GetProperty("itemId").GetString()), new UUID(w.GetProperty("assetId").GetString())); + var vp = doc.RootElement.GetProperty("visualParams").EnumerateArray().Select(e => (byte)e.GetInt32()).ToArray(); + return (assets, wearables, vp); + } + + private static FidelityReport Empty => new(Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty()); + + private static UUID[] Faces(AvatarAppearance a) + => Enumerable.Range(0, (int)AvatarAppearance.TEXTURE_COUNT).Select(i => a.Texture.FaceTextures[i]?.TextureID ?? UUID.Zero).ToArray(); + + // ------------------------------------------------------------------ 1. resolver + + [Fact] + public void Resolve_GoldenFixtures_Yields7WearablesAnd7Textures() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var (assets, wearables, vp) = LoadFixtures(); + var compositor = new TexLayerCompositor(); + + var r = BakeOrchestrator.Resolve(wearables, vp, assets, compositor, 512); + + Assert.Empty(r.Failures); + Assert.Equal(7, r.Request.Wearables.Count); + Assert.Equal(7, r.Request.Textures.Count); + Assert.Equal(512, r.Request.BakeSize); + Assert.All(r.Request.Wearables, w => Assert.StartsWith("LLWearable", w.RawText)); + // every texture the wearables reference (other than the default) was fetched, once + var referenced = r.Request.Wearables.Select(w => WearableParser.Parse(w.RawText)) + .SelectMany(p => p.Textures.Values).Where(id => !id.IsZero() && id != BakeConstants.DefaultAvatarTexture).ToHashSet(); + Assert.Equal(referenced, r.Request.Textures.Keys.ToHashSet()); + // the presence's VisualParams overlay decoded through the parameter table (or one note saying why not) + Assert.True(r.Request.VisualParams.Count > 0 || r.Notes.Count == 1, string.Join("; ", r.Notes)); + } + + // ------------------------------------------------------------------ 2. store + TE + + [Fact] + public void StoreAndApply_FiveResults_StoresFiveAssetsAndWritesExactlyThoseFaces() + { + var assets = new FakeAssetService(); + var appearance = new AvatarAppearance(); + var before = Faces(appearance); + var inputs = new BakeOrchestrator.ResolvedInputs( + new BakeRequest(Array.Empty(), new Dictionary(), new Dictionary(), 512), + Array.Empty(), Array.Empty()); + var channels = new[] { BakeChannel.Head, BakeChannel.Upper, BakeChannel.Lower, BakeChannel.Eyes, BakeChannel.Hair }; + var results = channels.Select(ch => new BakeResult(ch, Encoding.ASCII.GetBytes("j2k-" + ch), "hash-" + ch, Empty)).ToList(); + + var outcomes = BakeOrchestrator.StoreAndApply(results, inputs, Agent, assets, appearance); + + // five assets, right name / description / flags / creator + Assert.Equal(5, assets.Stored.Count); + foreach (var ch in channels) + { + var a = Assert.Single(assets.Stored, s => s.Name == $"bake:{Agent}:{ch.ToString().ToLowerInvariant()}"); + Assert.Equal("hash-" + ch, a.Description); + Assert.Equal((sbyte)AssetType.Texture, a.Type); + Assert.False(a.Temporary); + Assert.False(a.Local); + Assert.Equal(Agent.ToString(), a.Metadata.CreatorID); + Assert.Equal(Encoding.ASCII.GetBytes("j2k-" + ch), a.Data); + } + // outcomes: those five Baked with the stored id and hash, the other six Skipped + Assert.Equal(11, outcomes.Count); + foreach (var o in outcomes) + { + if (channels.Contains(o.Channel)) + { + Assert.Equal(ChannelStatus.Baked, o.Status); + Assert.Equal("hash-" + o.Channel, o.InputHash); + Assert.Contains(assets.Stored, s => s.FullID == o.AssetId); + } + else Assert.Equal(ChannelStatus.Skipped, o.Status); + } + // exactly faces 8, 9, 10, 11, 20 written; 19 (skirt), 40-44 and every other face untouched + var written = new[] { 8, 9, 10, 11, 20 }; + var after = Faces(appearance); + for (var i = 0; i < before.Length; i++) + { + if (written.Contains(i)) + { + var ch = channels[Array.IndexOf(written, i)]; + Assert.Equal(outcomes.Single(o => o.Channel == ch).AssetId, after[i]); + Assert.NotEqual(before[i], after[i]); + } + else Assert.Equal(before[i], after[i]); + } + Assert.Equal(before[19], after[19]); + } + + // ------------------------------------------------------------------ 3. missing texture + + [Fact] + public void Run_MissingEyesTexture_FailsEyesAndBakesTheRest() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var (assets, wearables, vp) = LoadFixtures(); + var compositor = new TexLayerCompositor(); + // the eyes wearable's Eyes-slot texture goes missing. (Not the hair texture: in this fixture set the hair + // and the shoes share one texture id, so its absence correctly fails Hair *and* Lower.) + var eyesAsset = assets.Get(wearables[(int)WearableType.Eyes][0].AssetID.ToString()); + var eyes = WearableParser.Parse(Encoding.UTF8.GetString(eyesAsset.Data)); + var eyesTex = eyes.Textures[TextureSlot.EyesIris]; + Assert.True(assets.Remove(eyesTex.ToString()), "fixture set should contain the eyes texture"); + var appearance = new AvatarAppearance(); + var before = Faces(appearance); + + var outcome = BakeOrchestrator.Run(Agent, BakeReason.Console, wearables, vp, appearance, assets, + new SkiaBakeBackend(compositor) { Quality = 0.5 }, compositor, 128, CancellationToken.None); + + var byCh = outcome.Channels.ToDictionary(c => c.Channel); + Assert.Equal(ChannelStatus.Failed, byCh[BakeChannel.Eyes].Status); + Assert.Contains(eyesTex.ToString(), byCh[BakeChannel.Eyes].Reason); + Assert.Equal(UUID.Zero, byCh[BakeChannel.Eyes].AssetId); + var after = Faces(appearance); + Assert.Equal(before[11], after[11]); + foreach (var ch in new[] { BakeChannel.Head, BakeChannel.Upper, BakeChannel.Lower, BakeChannel.Hair }) + { + Assert.True(byCh[ch].Status == ChannelStatus.Baked, $"{ch}: {byCh[ch].Status} {byCh[ch].Reason}"); + Assert.NotEqual(UUID.Zero, byCh[ch].AssetId); + Assert.NotEmpty(byCh[ch].InputHash); + Assert.Equal(byCh[ch].AssetId, after[BakeOrchestrator.FaceOf(ch)]); + var stored = assets.Get(byCh[ch].AssetId.ToString()); + Assert.NotNull(stored); + Assert.Equal(byCh[ch].InputHash, stored.Description); + Assert.True(stored.Data.Length > 0); + } + Assert.Equal(4, outcome.Count(ChannelStatus.Baked)); + Assert.Equal(1, outcome.Count(ChannelStatus.Failed)); + Assert.Equal(6, outcome.Count(ChannelStatus.Skipped)); + Assert.Equal(4, assets.Stored.Count); + } + + // ------------------------------------------------------------------ 4. worn but assetless (S1d) + + /// + /// A worn slot with no asset behind it is a real worn wearable, not an empty slot: the viewer counts + /// wearables, not textures (LLTexLayerTemplate::updateWearableCache, lltexlayer.cpp:1615-1638), so it still + /// contributes its layers' morph masks. Resolve must hand it to the library as an empty WearableInput of + /// its type rather than dropping it (Ledger Q-12; MORPH-MASK-PASS.md 2.4). + /// + [Fact] + public void Resolve_WornButAssetlessSlot_ReachesTheLibraryAsAWearableInput() + { + var assets = new FakeAssetService(); + var wearables = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < wearables.Length; i++) wearables[i] = new AvatarWearable(); + // a shirt slot that is worn (a real item) with no asset, exactly as Aleric Fenwood wears his + wearables[(int)WearableType.Shirt].Add(new UUID("77c41e39-38f9-f75a-0000-585989bf0000"), UUID.Zero); + + var r = BakeOrchestrator.Resolve(wearables, null, assets, new TexLayerCompositor(), 512); + + var shirt = Assert.Single(r.Request.Wearables); + Assert.Equal((int)WearableType.Shirt, shirt.WearableType); + Assert.Equal(UUID.Zero, shirt.AssetId); + Assert.Equal("", shirt.RawText); + Assert.Empty(r.Failures); // a missing asset is not a failure: there is nothing to fetch + Assert.Empty(r.Request.Textures); + Assert.Contains(r.Notes, n => n.Contains("worn with no asset")); + } + + /// + /// S1c decision 3, answered end to end. RED through S1d; green from S1e, which fixed it. + /// + /// An assetless Skirt slot makes the library produce a Skirt channel (SkiaBakeBackend.ChannelsFor adds it + /// whenever a Skirt wearable is worn, textures or not). Every layer of that channel is then skipped — + /// skirt_fabric has no texture, skirt_fabric_alpha is a mask layer with nothing to mask, skirt_tattoo needs a + /// Universal — so nothing is drawn. But the bake is not empty: the layer set's alpha starts opaque and only + /// the (skipped) mask layer would have carved the skirt out of it, so what gets encoded is a 96.5% opaque + /// near-black image, which the orchestrator then stores and writes into face 19. + /// + /// On any avatar whose Skirt slot is occupied by a default item with no asset, a server bake would therefore + /// paint a solid dark skirt over whatever face 19 held. The pre-existing trigger is a real Skirt wearable + /// carrying no skirt texture; S1d widened it to assetless slots, which are common. + /// + /// S1e closes it: the library reports BakeResult.NothingDrawn for a channel where every layer was skipped, + /// and StoreAndApply neither stores nor applies such a bake — the outcome is Skipped, "nothing drawn for + /// this channel", and the face keeps what it had. + /// + [Fact] + public void Run_AssetlessSkirtSlot_MustNotOverwriteFace19WithAnUndrawnBake() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var (assets, wearables, vp) = LoadFixtures(); + wearables[(int)WearableType.Skirt].Add(UUID.Random(), UUID.Zero); // worn, no asset + var appearance = new AvatarAppearance(); + var before = Faces(appearance); + var compositor = new TexLayerCompositor(); + + var outcome = BakeOrchestrator.Run(Agent, BakeReason.Console, wearables, vp, appearance, assets, + new SkiaBakeBackend(compositor) { Quality = 0.5 }, compositor, 128, CancellationToken.None); + + var skirt = outcome.Channels.Single(c => c.Channel == BakeChannel.Skirt); + var after = Faces(appearance); + var stored = skirt.AssetId.IsZero() ? null : assets.Get(skirt.AssetId.ToString()); + var img = stored is null ? null : J2kCodec.Decode(stored.Data); + var opaque = img is null ? 0 : img.A.Count(a => a > 128) * 100.0 / img.A.Length; + var diag = $"skirt={skirt.Status} face19 {before[19]} -> {after[19]} opaque={opaque:F1}% " + + $"layers=[{string.Join(" | ", skirt.Fidelity.Notes)}]"; + + // nothing was drawn into this channel: every layer skipped + Assert.All(skirt.Fidelity.Notes, n => Assert.Contains("skipped", n)); + // therefore the face must be left as it was + Assert.True(before[19] == after[19], "an undrawn channel must not overwrite its face. " + diag); + } + + /// + /// The other side of the S1e rule, so it cannot be satisfied by discarding transparent bakes instead: a + /// channel that DID draw is stored and applied even when every pixel it drew is transparent. Truly Bazar's + /// bald hair is exactly this — a 4x4 fully transparent hair texture — and its bake is the correct bake. + /// + [Fact] + public void Run_DrawnButFullyTransparentChannel_IsStillStoredAndApplied() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var (assets, wearables, vp) = LoadFixtures(); + var appearance = new AvatarAppearance(); + var before = Faces(appearance); + var compositor = new TexLayerCompositor(); + + var outcome = BakeOrchestrator.Run(Agent, BakeReason.Console, wearables, vp, appearance, assets, + new SkiaBakeBackend(compositor) { Quality = 0.5 }, compositor, 128, CancellationToken.None); + + var hair = outcome.Channels.Single(c => c.Channel == BakeChannel.Hair); + var after = Faces(appearance); + Assert.Equal(ChannelStatus.Baked, hair.Status); + var stored = assets.Get(hair.AssetId.ToString()); + Assert.NotNull(stored); + Assert.Equal(hair.AssetId, after[BakeOrchestrator.FaceOf(BakeChannel.Hair)]); + Assert.NotEqual(before[BakeOrchestrator.FaceOf(BakeChannel.Hair)], after[BakeOrchestrator.FaceOf(BakeChannel.Hair)]); + // it really is all-transparent: this is the case a pixel-based rule would have wrongly discarded + var img = J2kCodec.Decode(stored!.Data); + Assert.True(img.A.All(a => a <= 2), $"Truly's hair bake should be fully transparent; max alpha {img.A.Max()}"); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeReuseTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeReuseTests.cs new file mode 100644 index 00000000000..7859863d6ba --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeReuseTests.cs @@ -0,0 +1,399 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSimNGC.Appearance.Baking; +using Xunit; +using Xunit.Abstractions; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S2 Part 1: persistence in the avatar service's key/value table and the input-hash skip (ADR-004). No Scene, no +/// SceneHelpers — a fake asset service, a fake avatar service, and Truly Bazar's golden fixtures as the outfit. +/// Every test skips (vacuously, with a console line) when the fixtures have not been fetched. +/// +public class BakeReuseTests +{ + private readonly ITestOutputHelper _out; + public BakeReuseTests(ITestOutputHelper output) { _out = output; } + + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + private const int Size = 128; // small and fast; the fidelity of the pixels is the golden harness's job + private const int CofVersion = 42; + + private static string FixtureDir([CallerFilePath] string here = "") + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "..", "..", "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden", "truly-stock", "fixtures")); + + private const string SkipNote = "SKIPPED: golden fixtures not fetched (Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/fixtures)"; + private static bool FixturesPresent => File.Exists(Path.Combine(FixtureDir(), "avatar.json")); + + /// A backend that counts the calls it gets and the channels each one asked for. + private sealed class CountingBackend : IBakeBackend + { + private readonly IBakeBackend m_inner; + public CountingBackend(IBakeBackend inner) { m_inner = inner; } + public int Calls; + public readonly List Composited = new(); + public Task> BakeAsync(BakeRequest r, CancellationToken ct) + { + Calls++; + var task = m_inner.BakeAsync(r, ct); + Composited.AddRange(task.GetAwaiter().GetResult().Select(x => x.Channel)); + return task; + } + } + + private sealed class Rig + { + public required FakeAssetService Assets; + public required FakeAvatarService Avatars; + public required AvatarWearable[] Wearables; + public required byte[] VisualParams; + public required TexLayerCompositor Compositor; + public AvatarAppearance Appearance = new(); + + public (BakeOutcome Outcome, CountingBackend Backend) Bake(int size = Size, int cof = CofVersion) + { + var backend = new CountingBackend(new SkiaBakeBackend(Compositor) { Quality = 0.5 }); + var outcome = BakeOrchestrator.Run(Agent, BakeReason.Console, Wearables, VisualParams, Appearance, + Assets, Avatars, backend, Compositor, size, cof, CancellationToken.None); + return (outcome, backend); + } + + public UUID Face(BakeChannel ch) => Appearance.Texture.FaceTextures[BakeOrchestrator.FaceOf(ch)]?.TextureID ?? UUID.Zero; + public Dictionary Keys => Avatars.Records.TryGetValue(Agent, out var d) ? d : new Dictionary(); + } + + private static Rig Load() + { + var dir = FixtureDir(); + var assets = new FakeAssetService(); + foreach (var f in Directory.GetFiles(dir)) + { + var ext = Path.GetExtension(f); + sbyte type = ext switch { ".bodypart" => (sbyte)AssetType.Bodypart, ".clothing" => (sbyte)AssetType.Clothing, ".j2c" => (sbyte)AssetType.Texture, _ => -1 }; + if (type < 0) continue; + var id = Path.GetFileNameWithoutExtension(f); + assets.Put(new AssetBase(new UUID(id), id, type, Agent.ToString()) { Data = File.ReadAllBytes(f) }); + } + using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(dir, "avatar.json"))); + var wearables = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < wearables.Length; i++) wearables[i] = new AvatarWearable(); + foreach (var w in doc.RootElement.GetProperty("wearables").EnumerateArray()) + wearables[w.GetProperty("type").GetInt32()].Add(new UUID(w.GetProperty("itemId").GetString()), new UUID(w.GetProperty("assetId").GetString())); + var vp = doc.RootElement.GetProperty("visualParams").EnumerateArray().Select(e => (byte)e.GetInt32()).ToArray(); + return new Rig { Assets = assets, Avatars = new FakeAvatarService(), Wearables = wearables, VisualParams = vp, Compositor = new TexLayerCompositor() }; + } + + /// The channels a bake of Truly's stock outfit produces. + private static readonly BakeChannel[] Live = { BakeChannel.Head, BakeChannel.Upper, BakeChannel.Lower, BakeChannel.Eyes, BakeChannel.Hair }; + + /// + /// Replace one worn wearable with a different asset that differs only in one stored parameter value. Same + /// wearable type, same textures — the point is that the input hash of the channels that type feeds changes + /// and no other channel's does. + /// + private static UUID MutateWearable(Rig rig, WearableType type) + { + var slot = rig.Wearables[(int)type]; + var oldId = slot[0].AssetID; + var text = Encoding.UTF8.GetString(rig.Assets.Assets[oldId.ToString()].Data); + var parsed = WearableParser.Parse(text); + // the LLWearable body writes one " " line per parameter, unindented (see any .clothing fixture) + var pid = parsed.Params.Keys.OrderBy(k => k).First(); + var idx = text.IndexOf($"\n{pid} ", StringComparison.Ordinal); + Assert.True(idx >= 0, $"could not find parameter {pid} in the {type} wearable to change"); + var end = text.IndexOf('\n', idx + 1); + var mutated = text[..idx] + $"\n{pid} 0.123456" + text[end..]; + Assert.NotEqual(text, mutated); + Assert.Equal(0.123456f, WearableParser.Parse(mutated).Params[pid], 5); + + var newId = UUID.Random(); + rig.Assets.Put(new AssetBase(newId, newId.ToString(), (sbyte)AssetType.Clothing, Agent.ToString()) { Data = Encoding.UTF8.GetBytes(mutated) }); + rig.Wearables[(int)type] = new AvatarWearable(); + rig.Wearables[(int)type].Add(slot[0].ItemID, newId); + return newId; + } + + // ------------------------------------------------------------------ 1. the index is written + + [Fact] + public void FirstBake_WritesTheAdr004KeysThroughTheOrdinaryAvatarService() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + + var (outcome, _) = rig.Bake(); + + Assert.Equal(5, outcome.Count(ChannelStatus.Baked)); + Assert.True(outcome.IndexWritten); + Assert.Equal(1, rig.Avatars.SetItemsCalls); // one batched SetItems, not one call per key + + var keys = rig.Keys; + foreach (var ch in Live) + { + var stored = outcome.Channels.Single(c => c.Channel == ch); + Assert.Equal(stored.AssetId.ToString(), keys[$"Bake:{ch}"]); + Assert.Equal(stored.InputHash, keys[$"BakeHash:{ch}"]); + Assert.Equal(stored.AssetId, rig.Face(ch)); + } + Assert.Equal(CofVersion.ToString(), keys["BakeCOFVersion"]); + Assert.Equal(Size.ToString(), keys["BakeSize"]); + Assert.True(DateTime.TryParse(keys["BakeUpdated"], null, System.Globalization.DateTimeStyles.RoundtripKind, out var updated)); + Assert.True((DateTime.UtcNow - updated.ToUniversalTime()).TotalMinutes < 5, $"BakeUpdated {keys["BakeUpdated"]} should be now, in UTC"); + // no key for a channel the outfit never produced + Assert.DoesNotContain("Bake:Skirt", keys.Keys); + // every key fits the Avatars table's Name varchar(32) + Assert.All(keys.Keys, k => Assert.True(k.Length <= 32, k)); + + // it round-trips through the same reader the bake path uses + var index = BakeIndex.Read(rig.Avatars, Agent); + Assert.Equal(Size, index.Size); + Assert.Equal(CofVersion, index.CofVersion); + Assert.Equal(5, index.Bakes.Count); + } + + // ------------------------------------------------------------------ 2. unchanged inputs -> all Reused, zero composites + + [Fact] + public void UnchangedInputs_ReuseEveryChannelAndCompositeNothing() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + var (first, _) = rig.Bake(); + var firstIds = Live.ToDictionary(ch => ch, ch => first.Channels.Single(c => c.Channel == ch).AssetId); + rig.Assets.ResetOps(); + + var (second, backend) = rig.Bake(); + + Assert.Equal(5, second.Count(ChannelStatus.Reused)); + Assert.Equal(0, second.Count(ChannelStatus.Baked)); + Assert.Equal(0, second.Count(ChannelStatus.Failed)); + // the backend was never even called: with nothing to bake there is no request to make + Assert.Equal(0, backend.Calls); + Assert.Empty(backend.Composited); + // nothing was stored and nothing was deleted + Assert.Empty(rig.Assets.Stored); + Assert.DoesNotContain(rig.Assets.Ops, o => o.StartsWith("delete ")); + Assert.Empty(second.Superseded); + // not one texture was fetched — only the seven wearable assets, which the hash needs + var fetched = rig.Assets.Fetched.ToList(); + var wearableIds = rig.Wearables.SelectMany(w => Enumerable.Range(0, w.Count).Select(i => w[i].AssetID)) + .Where(id => !id.IsZero()).Select(id => id.ToString()).ToHashSet(); + Assert.All(fetched, f => Assert.Contains(f, wearableIds)); + // the faces still point at the same assets and the index still resolves + foreach (var ch in Live) + { + Assert.Equal(firstIds[ch], second.Channels.Single(c => c.Channel == ch).AssetId); + Assert.Equal(firstIds[ch], rig.Face(ch)); + Assert.NotNull(rig.Assets.GetUnchecked(firstIds[ch].ToString())); + } + _out.WriteLine($"second run: {second.Count(ChannelStatus.Reused)} reused, {backend.Calls} backend calls, " + + $"{fetched.Count} asset fetches (all wearables), {rig.Assets.Stored.Count} stores"); + } + + // ------------------------------------------------------------------ 3. one wearable changed -> only its channels re-bake + + /// + /// Truly wears an Undershirt. avatar_lad.xml gives the upper_undershirt local texture to the + /// upper_body layer set and to no other, so maps that slot + /// to and Upper is the only channel the type feeds. Changing the + /// undershirt must therefore re-bake Upper and reuse Head, Lower, Eyes and Hair — the whole point of hashing + /// per channel rather than per outfit. + /// + [Fact] + public void OneWearableChanged_RebakesOnlyTheChannelsThatWearableTypeFeeds() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + var (first, _) = rig.Bake(); + var firstIds = Live.ToDictionary(ch => ch, ch => first.Channels.Single(c => c.Channel == ch).AssetId); + + var expected = BakeOrchestrator.ChannelsFedBy(WearableKind.Undershirt, rig.Compositor).ToHashSet(); + Assert.Equal(new HashSet { BakeChannel.Upper }, expected); // stated, not merely derived + + MutateWearable(rig, WearableType.Undershirt); + rig.Assets.ResetOps(); + + var (second, backend) = rig.Bake(); + + Assert.Equal(1, backend.Calls); + Assert.Equal(new[] { BakeChannel.Upper }, backend.Composited); + Assert.Equal(ChannelStatus.Baked, second.Channels.Single(c => c.Channel == BakeChannel.Upper).Status); + foreach (var ch in Live.Where(c => c != BakeChannel.Upper)) + { + Assert.Equal(ChannelStatus.Reused, second.Channels.Single(c => c.Channel == ch).Status); + Assert.Equal(firstIds[ch], rig.Face(ch)); + } + Assert.NotEqual(firstIds[BakeChannel.Upper], rig.Face(BakeChannel.Upper)); + Assert.Single(rig.Assets.Stored); + // and the index now carries the new Upper and the old everything-else + var keys = rig.Keys; + Assert.Equal(rig.Face(BakeChannel.Upper).ToString(), keys["Bake:Upper"]); + Assert.Equal(firstIds[BakeChannel.Head].ToString(), keys["Bake:Head"]); + } + + // ------------------------------------------------------------------ 4. a stored hash whose asset is gone + + [Fact] + public void StoredHashWhoseAssetHasVanished_IsNotTrustedAndTheChannelRebakes() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + var (first, _) = rig.Bake(); + var headId = first.Channels.Single(c => c.Channel == BakeChannel.Head).AssetId; + + // the asset goes; the index still claims it, hash and all + Assert.True(rig.Assets.Remove(headId.ToString())); + Assert.Equal(headId.ToString(), rig.Keys["Bake:Head"]); + rig.Assets.ResetOps(); + + var (second, backend) = rig.Bake(); + + Assert.Equal(new[] { BakeChannel.Head }, backend.Composited); + var head = second.Channels.Single(c => c.Channel == BakeChannel.Head); + Assert.Equal(ChannelStatus.Baked, head.Status); + Assert.NotEqual(headId, head.AssetId); + Assert.Equal(head.AssetId, rig.Face(BakeChannel.Head)); + Assert.Equal(4, second.Count(ChannelStatus.Reused)); + Assert.Contains(second.Notes, n => n.Contains("vanished")); + // superseding an asset that is already gone deletes nothing + Assert.Empty(second.Superseded); + } + + // ------------------------------------------------------------------ 5. a BakeSize change invalidates everything + + [Fact] + public void BakeSizeChange_RebakesEveryChannel() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + var (first, _) = rig.Bake(size: 128); + var firstIds = Live.ToDictionary(ch => ch, ch => first.Channels.Single(c => c.Channel == ch).AssetId); + rig.Assets.ResetOps(); + + var (second, backend) = rig.Bake(size: 256); + + Assert.Equal(5, second.Count(ChannelStatus.Baked)); + Assert.Equal(0, second.Count(ChannelStatus.Reused)); + Assert.Equal(Live.OrderBy(c => c), backend.Composited.OrderBy(c => c)); + Assert.Equal("256", rig.Keys["BakeSize"]); + foreach (var ch in Live) + { + Assert.NotEqual(firstIds[ch], rig.Face(ch)); + // the hash itself changed, not only the BakeSize key: size is inside BakeHash + Assert.NotEqual(first.Channels.Single(c => c.Channel == ch).InputHash, rig.Keys[$"BakeHash:{ch}"]); + } + // every one of the five old assets was superseded + Assert.Equal(5, second.Superseded.Count); + Assert.All(firstIds.Values, id => Assert.Contains(id, second.Superseded)); + } + + // ------------------------------------------------------------------ 6. supersede ordering + + [Fact] + public void Supersede_DeletesThePreviousAssetAndOnlyAfterTheNewOneIsStored() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + var (first, _) = rig.Bake(); + var oldUpper = first.Channels.Single(c => c.Channel == BakeChannel.Upper).AssetId; + MutateWearable(rig, WearableType.Undershirt); + rig.Assets.ResetOps(); + + var (second, _) = rig.Bake(); + + var newUpper = second.Channels.Single(c => c.Channel == BakeChannel.Upper).AssetId; + Assert.NotEqual(oldUpper, newUpper); + Assert.Equal(new[] { oldUpper }, second.Superseded); + Assert.Null(rig.Assets.GetUnchecked(oldUpper.ToString())); + Assert.NotNull(rig.Assets.GetUnchecked(newUpper.ToString())); + + // the order on the asset service: the new bake is stored, then the old one is deleted. Never the reverse, + // or a failing store would leave the avatar with a face pointing at nothing. + var store = rig.Assets.Ops.IndexOf("store " + newUpper); + var delete = rig.Assets.Ops.IndexOf("delete " + oldUpper); + Assert.True(store >= 0 && delete >= 0, string.Join(" | ", rig.Assets.Ops)); + Assert.True(store < delete, $"store must precede delete: {string.Join(" | ", rig.Assets.Ops)}"); + + // and nothing a face still points at was touched: the four reused channels keep their assets + foreach (var ch in Live.Where(c => c != BakeChannel.Upper)) + Assert.NotNull(rig.Assets.GetUnchecked(rig.Face(ch).ToString())); + } + + /// + /// The supersede rule is "never delete something a face still points at", and it is a rule about the faces, + /// not about bookkeeping: an index that names an asset which some other channel's face is using must not have + /// that asset deleted out from under it. + /// + [Fact] + public void Supersede_RefusesToDeleteAnAssetAnotherFaceStillPointsAt() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + var (first, _) = rig.Bake(); + var oldUpper = first.Channels.Single(c => c.Channel == BakeChannel.Upper).AssetId; + + // contrive the collision: some other channel's face is pointing at Upper's old bake + rig.Appearance.Texture.CreateFace((uint)BakeOrchestrator.FaceOf(BakeChannel.Skirt)).TextureID = oldUpper; + MutateWearable(rig, WearableType.Undershirt); + rig.Assets.ResetOps(); + + var (second, _) = rig.Bake(); + + Assert.Equal(ChannelStatus.Baked, second.Channels.Single(c => c.Channel == BakeChannel.Upper).Status); + Assert.Empty(second.Superseded); + Assert.NotNull(rig.Assets.GetUnchecked(oldUpper.ToString())); + Assert.DoesNotContain(rig.Assets.Ops, o => o == "delete " + oldUpper); + } + + // ------------------------------------------------------------------ 7. the hazard this index lives with + + /// + /// Pins the behaviour behind 's hazard note. AvatarService.SetAvatar deletes + /// every row for the agent before rewriting the appearance-derived keys (AvatarService.cs:93), so any + /// appearance save destroys the bake index. The consequence must be a re-bake and never a broken face: the + /// index can be wholly absent, never half-right. + /// + [Fact] + public void AnAppearanceSaveWipesTheIndex_AndTheNextBakeSimplyRebakes() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + var (first, _) = rig.Bake(); + Assert.Equal(5, first.Count(ChannelStatus.Baked)); + + rig.Avatars.SetAppearance(Agent, new AvatarAppearance()); + + Assert.Empty(BakeIndex.Read(rig.Avatars, Agent).Bakes); + var (second, backend) = rig.Bake(); + Assert.Equal(5, second.Count(ChannelStatus.Baked)); + Assert.Equal(0, second.Count(ChannelStatus.Reused)); + Assert.Equal(5, backend.Composited.Count); + // the old assets are orphaned, not deleted: with the index gone, supersede has nothing to go on + Assert.Empty(second.Superseded); + foreach (var ch in Live) + Assert.NotNull(rig.Assets.GetUnchecked(first.Channels.Single(c => c.Channel == ch).AssetId.ToString())); + } + + // ------------------------------------------------------------------ 8. no avatar service at all + + [Fact] + public void WithNoAvatarService_EverythingBakesAndNothingIsPersisted() + { + if (!FixturesPresent) { Console.WriteLine(SkipNote); return; } + var rig = Load(); + var appearance = new AvatarAppearance(); + + var outcome = BakeOrchestrator.Run(Agent, BakeReason.Console, rig.Wearables, rig.VisualParams, appearance, + rig.Assets, null, new SkiaBakeBackend(rig.Compositor) { Quality = 0.5 }, rig.Compositor, Size, 0, CancellationToken.None); + + Assert.Equal(5, outcome.Count(ChannelStatus.Baked)); + Assert.Equal(0, outcome.Count(ChannelStatus.Reused)); + Assert.False(outcome.IndexWritten); + Assert.Empty(rig.Avatars.Records); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BodyPartLossTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BodyPartLossTests.cs new file mode 100644 index 00000000000..6d9b2ad12e4 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BodyPartLossTests.cs @@ -0,0 +1,155 @@ +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S8. A bake made from a wearable set that has lost a body part is not a bake of a different outfit - it is a +/// bake of nothing, and storing it is destructive, because a new bake supersedes (deletes) the asset it replaces +/// (ADR-004). Baking again cannot undo it. +/// +/// The live loss. 2026-09-05, 18:06:51: four unresolvable item ids had just emptied wearable slots +/// 1-4, and the reason=CofChanged bake that followed reported "no Skin worn / no Eyes worn / no Hair worn", +/// stored 4 channels and superseded 4 - the good ones. The agent's Current Outfit folder still linked valid skin, +/// eyes and hair items the whole time. +/// +/// The guard is deliberately narrow. It fires only on the four body-part slots, and only on +/// present-to-absent: a resident cannot take off their skin, so that transition is always a failure upstream of +/// the bake. Everything a resident can actually do - change clothes, strip to underwear, swap a shape - either +/// leaves the body parts populated or replaces them, and goes through untouched. +/// +public class BodyPartLossTests +{ + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + + private static ServerSideBakingRegion Region() => new(enabled: true, handshake: new CofHandshake()); + + /// A wearable set with something in each named slot and nothing anywhere else. + private static AvatarWearable[] Worn(params WearableType[] slots) + { + var w = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < w.Length; i++) w[i] = new AvatarWearable(); + foreach (var slot in slots) w[(int)slot] = new AvatarWearable(UUID.Random(), UUID.Random()); + return w; + } + + private static readonly WearableType[] AllBodyParts = + { WearableType.Shape, WearableType.Skin, WearableType.Hair, WearableType.Eyes }; + + // ---------------- the refusal ---------------- + + [Theory] + [InlineData(WearableType.Skin)] + [InlineData(WearableType.Shape)] + [InlineData(WearableType.Hair)] + [InlineData(WearableType.Eyes)] + public void Losing_a_body_part_since_the_last_bake_refuses_the_next_one(WearableType lost) + { + var region = Region(); + region.RecordGoodBodyParts(Agent, Worn(AllBodyParts)); + + var remaining = new System.Collections.Generic.List(AllBodyParts); + remaining.Remove(lost); + + var refusal = region.RefusalForBodyPartLoss(Agent, Worn(remaining.ToArray())); + + Assert.NotNull(refusal); + Assert.Contains(lost.ToString(), refusal); + Assert.Contains("nothing is baked and nothing is superseded", refusal); + } + + /// The live case exactly: skin, hair and eyes gone at once, shape still there. + [Fact] + public void The_2026_09_05_loss_is_refused() + { + var region = Region(); + region.RecordGoodBodyParts(Agent, Worn(WearableType.Shape, WearableType.Skin, WearableType.Hair, WearableType.Eyes, WearableType.Shirt)); + + var refusal = region.RefusalForBodyPartLoss(Agent, Worn(WearableType.Shape)); + + Assert.NotNull(refusal); + Assert.Contains("Skin", refusal); + Assert.Contains("Hair", refusal); + Assert.Contains("Eyes", refusal); + } + + // ---------------- what must still go through ---------------- + + [Fact] + public void The_first_bake_of_a_session_always_proceeds() + { + // Nothing to compare with. Refusing here would leave a new arrival unbaked forever. + Assert.Null(Region().RefusalForBodyPartLoss(Agent, Worn(AllBodyParts))); + } + + [Fact] + public void An_ordinary_outfit_change_proceeds() + { + var region = Region(); + region.RecordGoodBodyParts(Agent, Worn(WearableType.Shape, WearableType.Skin, WearableType.Hair, WearableType.Eyes, WearableType.Shirt, WearableType.Pants)); + + // took the shirt and pants off; every body part still worn + Assert.Null(region.RefusalForBodyPartLoss(Agent, Worn(AllBodyParts))); + } + + [Fact] + public void Gaining_a_body_part_proceeds() + { + var region = Region(); + region.RecordGoodBodyParts(Agent, Worn(WearableType.Shape, WearableType.Skin)); + Assert.Null(region.RefusalForBodyPartLoss(Agent, Worn(AllBodyParts))); + } + + [Fact] + public void Replacing_a_body_part_with_a_different_item_proceeds() + { + var region = Region(); + region.RecordGoodBodyParts(Agent, Worn(AllBodyParts)); + Assert.Null(region.RefusalForBodyPartLoss(Agent, Worn(AllBodyParts))); // fresh random ids, same slots + } + + // ---------------- the baseline is only ever a good set ---------------- + + /// + /// A refusal must not become the baseline. If it did, the retry a few seconds later would see no loss, bake + /// from the empty set and supersede the good bakes anyway - the guard would delay the damage, not prevent it. + /// + [Fact] + public void A_refused_set_does_not_become_the_new_baseline() + { + var region = Region(); + region.RecordGoodBodyParts(Agent, Worn(AllBodyParts)); + var damaged = Worn(WearableType.Shape); + + Assert.NotNull(region.RefusalForBodyPartLoss(Agent, damaged)); + Assert.NotNull(region.RefusalForBodyPartLoss(Agent, damaged)); // and again, and again + Assert.NotNull(region.RefusalForBodyPartLoss(Agent, damaged)); + } + + [Fact] + public void Forgetting_an_agent_clears_the_baseline() + { + var region = Region(); + region.RecordGoodBodyParts(Agent, Worn(AllBodyParts)); + region.Forget(Agent); + Assert.Null(region.RefusalForBodyPartLoss(Agent, Worn(WearableType.Shape))); + } + + // ---------------- the slot set itself ---------------- + + [Fact] + public void The_guarded_slots_are_the_four_body_parts() + => Assert.Equal( + new[] { (int)WearableType.Shape, (int)WearableType.Skin, (int)WearableType.Hair, (int)WearableType.Eyes }, + ServerSideBakingRegion.BodyPartSlots); + + [Fact] + public void Presence_is_read_per_slot_from_the_wearable_array() + { + Assert.Equal( + new[] { (int)WearableType.Skin, (int)WearableType.Eyes }, + ServerSideBakingRegion.BodyPartsPresent(Worn(WearableType.Skin, WearableType.Eyes, WearableType.Shirt))); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BomAuxChannelTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BomAuxChannelTests.cs new file mode 100644 index 00000000000..e01cf10389c --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BomAuxChannelTests.cs @@ -0,0 +1,272 @@ +using System.Text; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSim.Services.Interfaces; +using OpenSimNGC.Appearance.Baking; +using Xunit; +using Xunit.Abstractions; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S5 Part 2 — the five Bakes-on-Mesh aux channels (leftarm, leftleg, aux1, aux2, aux3). The library has produced +/// them since S0b and nothing has ever exercised them, because no test avatar wears a Universal wearable and +/// neither reference set has one (Ledger Q-11). This drives them with a synthetic Universal instead. +/// +/// +/// A synthetic fixture is the honest limit of what can be tested without content. See +/// for what it does not cover. +/// +/// +public class BomAuxChannelTests +{ + private readonly ITestOutputHelper _out; + public BomAuxChannelTests(ITestOutputHelper output) { _out = output; } + + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + + /// The five aux channels and the Universal texture slot each is switched on by. + private static readonly (BakeChannel Channel, TextureSlot Slot, int Face)[] Aux = + { + (BakeChannel.LeftArm, TextureSlot.LeftArmTattoo, 40), + (BakeChannel.LeftLeg, TextureSlot.LeftLegTattoo, 41), + (BakeChannel.Aux1, TextureSlot.Aux1Tattoo, 42), + (BakeChannel.Aux2, TextureSlot.Aux2Tattoo, 43), + (BakeChannel.Aux3, TextureSlot.Aux3Tattoo, 44), + }; + + /// A small solid-colour JPEG 2000, so the compositor has something real to draw. + private static byte[] Texture(byte r, byte g, byte b) + { + var img = new RgbaPlanes(32, 32, hasAlpha: true); + for (var i = 0; i < img.R.Length; i++) { img.R[i] = r; img.G[i] = g; img.B[i] = b; img.A[i] = 255; } + return J2kCodec.Encode(img); + } + + /// An LLWearable body of the given type carrying the given texture slots. + private static string WearableText(WearableKind kind, string name, IReadOnlyDictionary textures) + { + var sb = new StringBuilder(); + sb.Append("LLWearable version 22\n").Append(name).Append("\n\n"); + sb.Append("\tpermissions 0\n\t{\n\t\tbase_mask\t7fffffff\n\t\towner_mask\t7fffffff\n\t\tgroup_mask\t00000000\n") + .Append("\t\teveryone_mask\t00000000\n\t\tnext_owner_mask\t00082000\n\t\tcreator_id\t11111111-1111-0000-0000-000100bba000\n") + .Append("\t\towner_id\t11111111-1111-0000-0000-000100bba000\n\t\tlast_owner_id\t00000000-0000-0000-0000-000000000000\n") + .Append("\t\tgroup_id\t00000000-0000-0000-0000-000000000000\n\t}\n"); + sb.Append("\tsale_info\t0\n\t{\n\t\tsale_type\tnot\n\t\tsale_price\t10\n\t}\n"); + sb.Append("type ").Append((int)kind).Append('\n'); + sb.Append("parameters 0\n"); + sb.Append("textures ").Append(textures.Count).Append('\n'); + foreach (var (slot, id) in textures) sb.Append((int)slot).Append(' ').Append(id).Append('\n'); + return sb.ToString(); + } + + /// + /// An agent wearing a Shape, a Skin and one Universal that paints the named aux slots. The Shape and Skin are + /// there because without a body the classic channels produce nothing interesting; the Universal is the point. + /// + private static (FakeAssetService Assets, AvatarWearable[] Wearables, Dictionary Painted) + Outfit(params TextureSlot[] slots) + { + var assets = new FakeAssetService(); + var wearables = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < wearables.Length; i++) wearables[i] = new AvatarWearable(); + + void Wear(WearableKind kind, WearableType type, string name, Dictionary tex) + { + var assetId = UUID.Random(); + assets.Put(new AssetBase(assetId, name, (sbyte)(kind is WearableKind.Shape or WearableKind.Skin ? AssetType.Bodypart : AssetType.Clothing), Agent.ToString()) + { + Data = Encoding.UTF8.GetBytes(WearableText(kind, name, tex)), + }); + wearables[(int)type].Add(UUID.Random(), assetId); + } + + Wear(WearableKind.Shape, WearableType.Shape, "Shape", new Dictionary()); + + var skinTex = UUID.Random(); + assets.Put(new AssetBase(skinTex, "skin", (sbyte)AssetType.Texture, Agent.ToString()) { Data = Texture(200, 170, 140) }); + Wear(WearableKind.Skin, WearableType.Skin, "Skin", new Dictionary + { + [TextureSlot.HeadBodypaint] = skinTex, [TextureSlot.UpperBodypaint] = skinTex, [TextureSlot.LowerBodypaint] = skinTex, + }); + + var painted = new Dictionary(); + foreach (var s in slots) + { + var id = UUID.Random(); + assets.Put(new AssetBase(id, "aux " + s, (sbyte)AssetType.Texture, Agent.ToString()) { Data = Texture(20, 200, 60) }); + painted[s] = id; + } + Wear(WearableKind.Universal, WearableType.Universal, "Universal", painted); + + return (assets, wearables, painted); + } + + // ------------------------------------------------------------------ the library + + /// An aux channel appears only when a Universal actually paints its slot. + [Fact] + public void TheLibraryProducesAnAuxChannelOnlyForASlotThatIsPainted() + { + var compositor = new TexLayerCompositor(); + + foreach (var (channel, slot, _) in Aux) + { + var (assets, wearables, _) = Outfit(slot); + var inputs = BakeOrchestrator.Resolve(wearables, null, assets, compositor, 64); + var results = new SkiaBakeBackend(compositor) { Quality = 0.5 }.Bake(inputs.Request); + var produced = results.Select(r => r.Channel).ToHashSet(); + + Assert.Contains(channel, produced); + foreach (var (other, _, _) in Aux) + if (other != channel) Assert.DoesNotContain(other, produced); + } + } + + [Fact] + public void AllFiveAuxChannelsCompositeAndEncodeTogether() + { + var compositor = new TexLayerCompositor(); + var (assets, wearables, painted) = Outfit(Aux.Select(a => a.Slot).ToArray()); + Assert.Equal(5, painted.Count); + + var inputs = BakeOrchestrator.Resolve(wearables, null, assets, compositor, 64); + var results = new SkiaBakeBackend(compositor) { Quality = 0.5 }.Bake(inputs.Request); + + foreach (var (channel, _, _) in Aux) + { + var r = Assert.Single(results, x => x.Channel == channel); + Assert.False(r.NothingDrawn, $"{channel} drew nothing"); + Assert.NotEmpty(r.J2kBytes); + Assert.Equal(64, J2kCodec.Decode(r.J2kBytes).W); + Assert.Equal(64, r.InputHash.Length); + + // the Universal's texture actually reached the canvas, not just the aux_base fill: the fixture paints + // (20,200,60) and the base layer is a flat (128,128,128), so a green-dominant pixel can only be the + // tattoo. Without this the test would pass on a channel that drew nothing but its base. + var img = J2kCodec.Decode(r.J2kBytes); + var green = 0; + for (var i = 0; i < img.R.Length; i++) + if (img.G[i] > 150 && img.R[i] < 100 && img.B[i] < 120) green++; + Assert.True(green > 0, $"{channel}: the Universal's texture never reached the canvas"); + + _out.WriteLine($"{channel,-8} {r.J2kBytes.Length,7} bytes {100.0 * green / img.R.Length,5:F1}% tattoo hash {r.InputHash[..12]}"); + } + } + + // ------------------------------------------------------------------ the orchestrator: store and apply + + [Fact] + public void TheOrchestratorStoresEachAuxChannelAndWritesFaces40To44() + { + var compositor = new TexLayerCompositor(); + var (assets, wearables, _) = Outfit(Aux.Select(a => a.Slot).ToArray()); + var avatars = new FakeAvatarService(); + var appearance = new AvatarAppearance(); + + var outcome = BakeOrchestrator.Run(Agent, BakeReason.Login, wearables, null, appearance, assets, avatars, + new SkiaBakeBackend(compositor) { Quality = 0.5 }, compositor, 64, 4, CancellationToken.None); + + foreach (var (channel, _, face) in Aux) + { + var c = outcome.Channels.Single(x => x.Channel == channel); + Assert.True(c.Status == ChannelStatus.Baked, $"{channel}: {c.Status} {c.Reason}"); + + // the face index the brief names + Assert.Equal(face, BakeOrchestrator.FaceOf(channel)); + Assert.Equal(c.AssetId, appearance.Texture.FaceTextures[face].TextureID); + + // stored as a real asset with the ADR-004 marker + var stored = assets.GetUnchecked(c.AssetId.ToString()); + Assert.NotNull(stored); + Assert.Equal(BakeOrchestrator.AssetNameFor(Agent, channel), stored.Name); + Assert.Equal(c.InputHash, stored.Description); + } + + // and the index carries all five alongside the classic ones + var index = BakeIndex.Read(avatars, Agent); + foreach (var (channel, _, _) in Aux) + { + Assert.True(index.TryGet(channel, out var bake), $"no index entry for {channel}"); + Assert.Equal(outcome.Channels.Single(x => x.Channel == channel).AssetId, bake.AssetId); + } + } + + /// The whole point of the aux channels: the Robust route serves them under the viewer's own token. + [Fact] + public void TheRobustRouteServesEveryAuxChannel() + { + var compositor = new TexLayerCompositor(); + var (assets, wearables, _) = Outfit(Aux.Select(a => a.Slot).ToArray()); + var avatars = new FakeAvatarService(); + var appearance = new AvatarAppearance(); + + var outcome = BakeOrchestrator.Run(Agent, BakeReason.Login, wearables, null, appearance, assets, avatars, + new SkiaBakeBackend(compositor) { Quality = 0.5 }, compositor, 64, 4, CancellationToken.None); + + var svc = new AuxAppearanceService(avatars, assets); + + foreach (var (channel, _, _) in Aux) + { + var c = outcome.Channels.Single(x => x.Channel == channel); + var token = channel.ToString().ToLowerInvariant(); + + // the token the viewer sends resolves to this channel (S4's map) + Assert.Equal(BakeIndex.BakeKey(channel), AppearanceChannels.BakeKeyFor(token)); + + var got = svc.GetBake(Agent, token, c.AssetId); + Assert.True(got is not null, $"{token} should be served"); + Assert.Equal(c.AssetId, got.FullID); + Assert.Equal(assets.GetUnchecked(c.AssetId.ToString()).Data, got.Data); + + // and a stale UUID is still refused on these channels + Assert.Null(svc.GetBake(Agent, token, UUID.Random())); + } + } + + private sealed class AuxAppearanceService : OpenSim.Services.AvatarService.AppearanceService + { + public AuxAppearanceService(IAvatarService a, IAssetService b) : base(new Nini.Config.IniConfigSource(), a, b) { } + } + + // ------------------------------------------------------------------ the recorded gap + + /// + /// What this does not establish, recorded so the gap is not mistaken for coverage: + /// + /// + /// Fidelity. There is no reference bake for any aux channel — neither golden set has a + /// Universal, so nothing compares these pixels against the LL compositor's. The classic channels are + /// diffed against references at 1024; these are only asserted to composite, encode and round-trip. + /// Real Universal content. The fixture paints flat 32x32 colours through slots the layer sets + /// name. It does not exercise a real Universal's parameters, colour drivers, alpha masks, or the + /// interaction between a Universal and the classic channels it can also paint (head/upper/lower/skirt/ + /// hair/eyes universal tattoo slots are untouched here). + /// The viewer. No LL or Firestorm client has ever rendered a sim-baked aux channel from this + /// simulator. Faces 40-44 are asserted from AvatarAppearance.BAKE_INDICES, not observed in-world, + /// and whether a viewer requests them at all depends on it having a mesh body bound to those slots. + /// Multi-Universal outfits. One Universal is worn. Two Universals painting the same aux slot + /// is the layered case the classic channels get wrong most often, and it is untested. + /// + /// + /// This test asserts only the two facts that make the gap precise: the reference sets carry no Universal, and + /// the aux channels therefore have no golden thresholds. + /// + [Fact] + public void WhatIsStillUntestedForWantOfRealContent() + { + var goldenDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", + "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden")); + + foreach (var manifest in Directory.GetFiles(goldenDir, "manifest.json", SearchOption.AllDirectories)) + { + var text = File.ReadAllText(manifest); + foreach (var (channel, _, _) in Aux) + Assert.DoesNotContain($"\"{channel.ToString().ToLowerInvariant()}\":", text); + // each set records these as not set, which is the fact this leans on + Assert.Contains("notSet", text); + } + _out.WriteLine("no reference set carries a Universal, so no aux channel has a fidelity threshold"); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/ChangeTriggerTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/ChangeTriggerTests.cs new file mode 100644 index 00000000000..d2b78c57e67 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/ChangeTriggerTests.cs @@ -0,0 +1,215 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSimNGC.Appearance.Baking; +using Xunit; +using Xunit.Abstractions; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S5 Part 1 — the change trigger. A rebake happens when the region has finished applying an outfit change +/// and persisted it, never on the arrival of the change (Ledger Q-16, Design Brief §4.6). +/// +/// +/// Both signals a change produces reach that point. The legacy route queues an appearance save in +/// Client_OnAvatarNowWearing (AvatarFactoryModule.cs:1292); the cap route queues one too rather +/// than baking on arrival. SaveAppearance then raises OnAvatarAppearanceChange immediately after +/// SetAppearanceAssets and AvatarService.SetAppearance, which is the one moment the wearables have +/// resolved asset ids and the result is stored. +/// +/// +public class ChangeTriggerTests +{ + private readonly ITestOutputHelper _out; + public ChangeTriggerTests(ITestOutputHelper output) { _out = output; } + + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + // An arbitrary fixed instant. Deliberately not a timestamp from any log: the spacings in these tests are + // constructed, not observed (Ledger Q-6). + private static readonly DateTime T0 = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + private static ServerSideBakingRegion On(TimeSpan? debounce = null) + => new(true, new CofHandshake()) { ChangeDebounce = debounce ?? TimeSpan.FromSeconds(2) }; + + private static ServerSideBakingRegion Off() + => new(false, new CofHandshake()); + + // ------------------------------------------------------------------ one change, one bake + + [Fact] + public void AChangeOnAnSsbRegionTriggersExactlyOneBake() + { + var region = On(); + + Assert.True(region.TryClaimChangeBake(Agent, T0)); + + // the same change producing a second signal claims nothing more + Assert.False(region.TryClaimChangeBake(Agent, T0)); + } + + /// + /// A single outfit change produces more than one signal, and a slam produces several. The exact spread is + /// unmeasured (Ledger Q-6), so the window is sized against the 5 s save delay rather than against it; + /// everything inside the window collapses into the one bake that was already claimed. The spacings below are + /// illustrative, not observations. + /// + [Fact] + public void ABurstOfSignalsCoalescesToOne() + { + var region = On(TimeSpan.FromSeconds(2)); + var claims = 0; + + // a pair close together, plus the extra signals a slam adds, all inside one second + foreach (var ms in new[] { 0, 310, 420, 655, 980 }) + if (region.TryClaimChangeBake(Agent, T0.AddMilliseconds(ms))) claims++; + + Assert.Equal(1, claims); + _out.WriteLine("5 signals spanning 980 ms -> 1 bake"); + } + + [Fact] + public void ASeparateChangeAfterTheWindowBakesAgain() + { + var region = On(TimeSpan.FromSeconds(2)); + + Assert.True(region.TryClaimChangeBake(Agent, T0)); + Assert.False(region.TryClaimChangeBake(Agent, T0.AddSeconds(1.9))); + Assert.True(region.TryClaimChangeBake(Agent, T0.AddSeconds(2.1))); + + // and the window is shorter than DelayBeforeAppearanceSave (5 s), so a change that completed its own + // save cycle is never suppressed + Assert.True(On().ChangeDebounce < TimeSpan.FromSeconds(5)); + } + + [Fact] + public void TheWindowIsPerAgent() + { + var region = On(); + var other = UUID.Random(); + + Assert.True(region.TryClaimChangeBake(Agent, T0)); + Assert.True(region.TryClaimChangeBake(other, T0)); + Assert.False(region.TryClaimChangeBake(Agent, T0)); + Assert.False(region.TryClaimChangeBake(other, T0)); + } + + [Fact] + public void AnAgentThatLeavesForgetsItsWindow() + { + var region = On(); + + Assert.True(region.TryClaimChangeBake(Agent, T0)); + Assert.False(region.TryClaimChangeBake(Agent, T0)); + + region.Forget(Agent); + Assert.True(region.TryClaimChangeBake(Agent, T0)); + } + + // ------------------------------------------------------------------ flag-off regions + + [Fact] + public void AFlagOffRegionTriggersNoBakeAtAll() + { + var region = Off(); + + foreach (var ms in new[] { 0, 310, 5000, 60000 }) + Assert.False(region.TryClaimChangeBake(Agent, T0.AddMilliseconds(ms))); + + Assert.False(region.ServerSideBakingEnabled); + } + + // ------------------------------------------------------------------ the send happens even with nothing recomputed + + private static string FixtureDir([CallerFilePath] string here = "") + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "..", "..", "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden", "truly-stock", "fixtures")); + + private const string SkipNote = "SKIPPED: golden fixtures not fetched"; + + /// + /// The case the trigger exists for and the one most easily got wrong: an outfit change whose channels all + /// hash the same as the stored bakes. Nothing is recomputed, nothing is stored — and the appearance must + /// still go out, because the viewer is waiting for an AvatarAppearance it can accept and will not + /// re-request one. BakeAsync sends when Baked + Reused > 0, so Reused alone is enough; this asserts + /// that condition against a real all-reused outcome from the orchestrator. + /// + [Fact] + public void AChangeWhoseHashesAllMatchStillSendsTheAppearance() + { + if (!File.Exists(Path.Combine(FixtureDir(), "avatar.json"))) { Console.WriteLine(SkipNote); return; } + + var dir = FixtureDir(); + var assets = new FakeAssetService(); + foreach (var f in Directory.GetFiles(dir)) + { + var ext = Path.GetExtension(f); + sbyte type = ext switch { ".bodypart" => (sbyte)AssetType.Bodypart, ".clothing" => (sbyte)AssetType.Clothing, ".j2c" => (sbyte)AssetType.Texture, _ => -1 }; + if (type < 0) continue; + var id = Path.GetFileNameWithoutExtension(f); + assets.Put(new AssetBase(new UUID(id), id, type, Agent.ToString()) { Data = File.ReadAllBytes(f) }); + } + using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(dir, "avatar.json"))); + var wearables = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < wearables.Length; i++) wearables[i] = new AvatarWearable(); + foreach (var w in doc.RootElement.GetProperty("wearables").EnumerateArray()) + wearables[w.GetProperty("type").GetInt32()].Add(new UUID(w.GetProperty("itemId").GetString()), new UUID(w.GetProperty("assetId").GetString())); + var vp = doc.RootElement.GetProperty("visualParams").EnumerateArray().Select(e => (byte)e.GetInt32()).ToArray(); + + var compositor = new TexLayerCompositor(); + var avatars = new FakeAvatarService(); + var appearance = new AvatarAppearance(); + BakeOutcome Bake(BakeReason r) => BakeOrchestrator.Run(Agent, r, wearables, vp, appearance, assets, avatars, + new SkiaBakeBackend(compositor) { Quality = 0.5 }, compositor, 128, 9, CancellationToken.None); + + var first = Bake(BakeReason.Login); + Assert.Equal(5, first.Count(ChannelStatus.Baked)); + + assets.ResetOps(); + var change = Bake(BakeReason.CofChanged); + + // nothing recomputed + Assert.Equal(0, change.Count(ChannelStatus.Baked)); + Assert.Equal(5, change.Count(ChannelStatus.Reused)); + Assert.Empty(assets.Stored); + + // ...and the appearance still goes out: this is the condition BakeAsync sends on + Assert.True(change.Count(ChannelStatus.Baked) + change.Count(ChannelStatus.Reused) > 0); + Assert.Equal(BakeReason.CofChanged, change.Reason); + // every face still points at a real stored bake, so the message the viewer gets is usable + foreach (var c in change.Channels.Where(c => c.Status == ChannelStatus.Reused)) + Assert.NotNull(assets.GetUnchecked(c.AssetId.ToString())); + } + + // ------------------------------------------------------------------ the shipped wiring + + /// + /// The trigger point and both routes into it, pinned against the source so this cannot rot into a test of a + /// helper nothing calls. + /// + [Fact] + public void TheShippedWiringMatchesWhatIsTestedHere() + { + var root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + string Read(params string[] parts) => File.ReadAllText(Path.Combine(new[] { root }.Concat(parts).ToArray())).Replace("\r\n", "\n"); + + var factory = Read("Source", "OpenSim.Region.CoreModules", "Avatar", "AvatarFactory", "AvatarFactoryModule.cs"); + // the completion event is raised, and after the persist rather than before it + var persist = factory.IndexOf("m_scene.AvatarService.SetAppearance(id, sp.Appearance);", StringComparison.Ordinal); + var trigger = factory.IndexOf("m_scene.EventManager.TriggerAvatarAppearanceChanged(sp);", StringComparison.Ordinal); + Assert.True(persist > 0 && trigger > persist, "the trigger must be raised after the appearance is persisted"); + Assert.DoesNotContain("//m_scene.EventManager.TriggerAvatarAppearanceChanged(sp);", factory); + // the legacy route still queues a save + Assert.Contains("QueueAppearanceSave(client.AgentId);", factory); + + var module = Read("Source", "OpenSim.Region.OptionalModules", "Avatar", "ServerSideBaking", "ServerSideBakingModule.cs"); + Assert.Contains("scene.EventManager.OnAvatarAppearanceChange += OnAvatarAppearanceChanged;", module); + Assert.Contains("scene.EventManager.OnAvatarAppearanceChange -= OnAvatarAppearanceChanged;", module); + Assert.Contains("BakeAsync(sp, BakeReason.CofChanged, CancellationToken.None)", module); + Assert.Contains("region.TryClaimChangeBake(sp.UUID, DateTime.UtcNow)", module); + // the cap route joins the same path instead of baking on arrival + Assert.Contains("scene.AvatarFactory?.QueueAppearanceSave(agentID);", module); + Assert.DoesNotContain("BakeAsync(sp, BakeReason.Cap,", module); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/CofVersionIdentityTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/CofVersionIdentityTests.cs new file mode 100644 index 00000000000..5ad4540cbe9 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/CofVersionIdentityTests.cs @@ -0,0 +1,144 @@ +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSim.Services.Interfaces; +using Xunit; +using Xunit.Abstractions; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S3 Part 1 — the question AIS going live forced: is the cof_version the viewer sends to +/// UpdateAvatarAppearance the same number AIS reports as the Current Outfit folder's version? +/// +/// +/// It is, and this proves it rather than asserting it. Both sides read +/// of the same folder from the same : AIS through +/// AisMutation.ReportVersion (which the viewer stores as its cof_version and posts back) and +/// AisEnvelope.Category's version; the bake through +/// . One field, one writer — the data +/// layer's folder-version bump. +/// +/// +public class CofVersionIdentityTests +{ + private readonly ITestOutputHelper _out; + public CofVersionIdentityTests(ITestOutputHelper output) { _out = output; } + + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + private static readonly UUID CofId = new("11111111-2222-3333-4444-555555555555"); + + /// Just enough inventory service to hand back one Current Outfit folder at a chosen version. + private sealed class OneFolderInventory : IInventoryService + { + private readonly InventoryFolderBase m_cof; + public int GetFolderForTypeCalls; + + public OneFolderInventory(int version) + { + m_cof = new InventoryFolderBase(CofId, "Current Outfit", Agent, (short)FolderType.CurrentOutfit, UUID.Random(), (ushort)version); + } + + public InventoryFolderBase Folder => m_cof; + + public InventoryFolderBase GetFolderForType(UUID userID, FolderType type) + { + GetFolderForTypeCalls++; + return type == FolderType.CurrentOutfit && userID == Agent ? m_cof : null; + } + + public InventoryFolderBase GetFolder(UUID userID, UUID folderID) => folderID == CofId ? m_cof : null; + + // nothing else is exercised + public bool CreateUserInventory(UUID user) => false; + public List GetInventorySkeleton(UUID userId) => new(); + public InventoryFolderBase GetRootFolder(UUID userID) => null; + public InventoryCollection GetFolderContent(UUID userID, UUID folderID) => null; + public InventoryCollection[] GetMultipleFoldersContent(UUID principalID, UUID[] folderIDs) => Array.Empty(); + public List GetFolderItems(UUID userID, UUID folderID) => new(); + public bool AddFolder(InventoryFolderBase folder) => false; + public bool UpdateFolder(InventoryFolderBase folder) => false; + public bool MoveFolder(InventoryFolderBase folder) => false; + public bool DeleteFolders(UUID userID, List folderIDs) => false; + public bool DeleteFolders(UUID userID, List folderIDs, bool onlyIfTrash) => false; + public bool PurgeFolder(InventoryFolderBase folder) => false; + public bool AddItem(InventoryItemBase item) => false; + public bool UpdateItem(InventoryItemBase item) => false; + public bool MoveItems(UUID ownerID, List items) => false; + public bool DeleteItems(UUID userID, List itemIDs) => false; + public InventoryItemBase GetItem(UUID userID, UUID itemID) => null; + public InventoryItemBase[] GetMultipleItems(UUID userID, UUID[] ids) => Array.Empty(); + public List GetActiveGestures(UUID userId) => new(); + public int GetAssetPermissions(UUID userID, UUID assetID) => 0; + public bool HasInventoryForUser(UUID userID) => true; + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(7)] + [InlineData(4242)] + [InlineData(65535)] + public void TheBakeReadsTheSameNumberAisReports(int version) + { + var inventory = new OneFolderInventory(version); + + // what the bake stores as BakeCOFVersion and compares in the cap handshake + int bakeSideValue = ServerSideBakingModule.CofVersionOf(inventory, Agent); + + // what AIS puts in _updated_category_versions, which is what the viewer keeps and posts back as cof_version + var envelope = new OSDMap(); + AisMutation.ReportVersion(envelope, inventory.GetFolder(Agent, CofId)); + int aisSideValue = ((OSDMap)envelope[AisMutation.UpdatedCategoryVersions])[CofId.ToString()].AsInteger(); + + // and what AIS puts in a category envelope's "version" + int aisCategoryValue = AisEnvelope.Category(inventory.Folder, Agent)["version"].AsInteger(); + + Assert.Equal(version, bakeSideValue); + Assert.Equal(version, aisSideValue); + Assert.Equal(version, aisCategoryValue); + _out.WriteLine($"folder.Version={version} bake={bakeSideValue} AIS _updated_category_versions={aisSideValue} AIS category.version={aisCategoryValue}"); + } + + [Fact] + public void TheBakeReadsTheCurrentOutfitFolderSpecificallyAndFreshEveryTime() + { + var inventory = new OneFolderInventory(9); + + Assert.Equal(9, ServerSideBakingModule.CofVersionOf(inventory, Agent)); + Assert.Equal(9, ServerSideBakingModule.CofVersionOf(inventory, Agent)); + + // ADR-006: read fresh, never cached — two reads, two service calls + Assert.Equal(2, inventory.GetFolderForTypeCalls); + + // a different agent has no folder here and must not inherit this one's version + Assert.Equal(0, ServerSideBakingModule.CofVersionOf(inventory, UUID.Random())); + // and no inventory service at all is 0, not a throw + Assert.Equal(0, ServerSideBakingModule.CofVersionOf(null, Agent)); + } + + /// + /// The one place the identity can break, recorded rather than hidden: + /// is a ushort (InventoryFolderBase.cs:67) while the cap's cof_version is an S32 and the + /// database column is wider. That is AIS ledger A-Q13. Both sides read the same truncated field, so they stay + /// equal to each other — but past 65535 both disagree with the database, and the wrap makes an older outfit + /// compare equal to a newer one. + /// + [Fact] + public void TheIdentityHoldsThroughTheUshortWrapBecauseBothSidesTruncateAlike() + { + const int past = 65536 + 7; + var inventory = new OneFolderInventory(past); // the ctor casts to ushort exactly as the data layer does + + int bakeSideValue = ServerSideBakingModule.CofVersionOf(inventory, Agent); + var envelope = new OSDMap(); + AisMutation.ReportVersion(envelope, inventory.GetFolder(Agent, CofId)); + int aisSideValue = ((OSDMap)envelope[AisMutation.UpdatedCategoryVersions])[CofId.ToString()].AsInteger(); + + Assert.Equal(bakeSideValue, aisSideValue); // still one number + Assert.Equal(7, bakeSideValue); // but not the database's, which is A-Q13 + Assert.NotEqual(past, bakeSideValue); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/CofWearableOrderTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/CofWearableOrderTests.cs new file mode 100644 index 00000000000..44a11bb7393 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/CofWearableOrderTests.cs @@ -0,0 +1,246 @@ +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using OpenSim.Services.Interfaces; +using OpenSimNGC.Appearance.Baking; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S10. Firestorm's "Add" puts a second wearable of one type in the COF. The LL viewer orders same-type wearables +/// by the COF link item's description — "@" + (type * 100 + index), written by +/// LLAppearanceMgr::getWearableOrderingDescUpdates (llappearancemgr.cpp:3676-3702) from +/// build_order_string (:3637-3642) — and layers them in that index order, later index on top +/// (LLTexLayerTemplate::render, lltexlayer.cpp:1659-1689, over the cache built 0..n-1 at :1615-1638). +/// +/// +/// Observed on Ebony (1.1.246, 2026-09-06 10:09:52 UTC): both "Shirt" and "Shirt2" were linked in the COF, a +/// CofChanged bake fired and reused 6/6, and the Avatars record for the agent held only +/// Wearable 4:0. The second shirt never reached , so the +/// composite had nothing to layer. +/// +/// +public class CofWearableOrderTests +{ + private static AvatarWearable[] Empty() + { + var w = new AvatarWearable[AvatarWearable.MAX_WEARABLES]; + for (var i = 0; i < w.Length; i++) w[i] = new AvatarWearable(); + return w; + } + + private static AvatarWearable[] With(params (int Type, UUID Item, UUID Asset)[] worn) + { + var w = Empty(); + foreach (var (type, item, asset) in worn) w[type].Add(item, asset); + return w; + } + + private static CofWearableLink Link(string desc, int type, UUID item, UUID asset) + => new(UUID.Random(), desc, item, type, asset); + + private const int Shirt = 4; // WearableType.Shirt / LLWearableType::WT_SHIRT + private const int Pants = 5; + + // ------------------------------------------------------------------ 1. the derivation + + [Fact] + public void TwoShirtLinks_DeriveInAtOrder_NotInFolderOrder() + { + var shirt0 = UUID.Random(); var asset0 = UUID.Random(); + var shirt1 = UUID.Random(); var asset1 = UUID.Random(); + + // Handed over in the wrong order on purpose: the folder listing has no order of its own, the description does. + var links = new[] + { + Link("@401", Shirt, shirt1, asset1), + Link("@400", Shirt, shirt0, asset0), + }; + + var derived = CofWearables.Derive(Empty(), links, out var changed); + + Assert.True(changed); + Assert.Equal(2, derived[Shirt].Count); + Assert.Equal(shirt0, derived[Shirt][0].ItemID); + Assert.Equal(asset0, derived[Shirt][0].AssetID); + Assert.Equal(shirt1, derived[Shirt][1].ItemID); + Assert.Equal(asset1, derived[Shirt][1].AssetID); + } + + [Fact] + public void OrderStringIsTypeTimesHundredPlusIndex() + { + // build_order_string(WT_SHIRT, 1) == "@401" (llappearancemgr.cpp:3637-3642), and within one type the + // key orders exactly as the index does. + Assert.Equal(400, CofWearables.OrderKey("@400")); + Assert.Equal(401, CofWearables.OrderKey("@401")); + Assert.Equal(0, CofWearables.OrderOf("@400", Shirt)); + Assert.Equal(1, CofWearables.OrderOf("@401", Shirt)); + Assert.Equal(0, CofWearables.OrderOf("@500", Pants)); + // WearablesOrderComparator sinks a description that carries no ordering info (llappearancemgr.cpp:3657-3668). + Assert.True(CofWearables.OrderKey("") < 0); + Assert.True(CofWearables.OrderKey("Broken link") < 0); + Assert.True(CofWearables.OrderOf("@500", Shirt) < 0); + } + + [Fact] + public void ADescriptionlessLinkSinksBelowTheOrderedOnes() + { + var ordered = UUID.Random(); var unordered = UUID.Random(); + var links = new[] + { + Link("", Shirt, unordered, UUID.Random()), + Link("@400", Shirt, ordered, UUID.Random()), + }; + + var derived = CofWearables.Derive(Empty(), links, out _); + + Assert.Equal(ordered, derived[Shirt][0].ItemID); + Assert.Equal(unordered, derived[Shirt][1].ItemID); + } + + // ------------------------------------------------------------------ 2. the S8 rule must survive + + [Fact] + public void ATypeNoLinkResolvesForKeepsWhatTheAgentAlreadyWears() + { + // S8: an item this region cannot resolve is a statement about the inventory lookup, not about what the + // avatar is wearing (AvatarFactoryModule.cs:975-989). An unresolvable link cannot be attributed to a + // wearable type at all, so its type must look untouched and keep its contents rather than go empty. + var shirt = UUID.Random(); var shirtAsset = UUID.Random(); + var pants = UUID.Random(); var pantsAsset = UUID.Random(); + var existing = With((Shirt, shirt, shirtAsset), (Pants, pants, pantsAsset)); + + // Only the shirt link resolved this time; the pants link's target could not be read. + var derived = CofWearables.Derive(existing, new[] { Link("@400", Shirt, shirt, shirtAsset) }, out var changed); + + Assert.False(changed); + Assert.Equal(shirt, derived[Shirt][0].ItemID); + Assert.Equal(1, derived[Pants].Count); + Assert.Equal(pants, derived[Pants][0].ItemID); + } + + [Fact] + public void NoLinksAtAllChangesNothing() + { + var shirt = UUID.Random(); + var existing = With((Shirt, shirt, UUID.Random())); + + var derived = CofWearables.Derive(existing, Array.Empty(), out var changed); + + Assert.False(changed); + Assert.Equal(shirt, derived[Shirt][0].ItemID); + } + + // ------------------------------------------------------------------ 3. persistence keeps the second slot + + [Fact] + public void TheAvatarsRecordRoundTripsBothShirtsInIndexOrder() + { + var shirt0 = UUID.Random(); var asset0 = UUID.Random(); + var shirt1 = UUID.Random(); var asset1 = UUID.Random(); + var appearance = new AvatarAppearance { Wearables = With((Shirt, shirt0, asset0)) }; + appearance.Wearables[Shirt].Add(shirt1, asset1); + + var data = new AvatarData(appearance); + + // The writer already names the slot "Wearable :" (IAvatarService.cs:199-208). + Assert.Equal($"{shirt0}:{asset0}", data.Data["Wearable 4:0"]); + Assert.Equal($"{shirt1}:{asset1}", data.Data["Wearable 4:1"]); + + // Shuffle the rows: a row store hands them back in whatever order it likes, and the index in the key is + // the only thing that says which shirt is on top. + var shuffled = new AvatarData { AvatarType = data.AvatarType, Data = new Dictionary() }; + foreach (var kvp in data.Data.Reverse()) shuffled.Data[kvp.Key] = kvp.Value; + + var back = shuffled.ToAvatarAppearance(); + + Assert.Equal(2, back.Wearables[Shirt].Count); + Assert.Equal(shirt0, back.Wearables[Shirt][0].ItemID); + Assert.Equal(shirt1, back.Wearables[Shirt][1].ItemID); + } + + // ------------------------------------------------------------------ 4. the pixel + + private static RgbaPlanes Flat(byte r, byte g, byte b) + { + var p = new RgbaPlanes(64, 64, hasAlpha: false); + Array.Fill(p.R, r); Array.Fill(p.G, g); Array.Fill(p.B, b); Array.Fill(p.A, (byte)255); + return p; + } + + /// A shirt worn at full coverage, so the whole upper-body region is its colour. + private static readonly Dictionary FullShirt = new() + { + [800] = 1f, [801] = 1f, [802] = 1f, [781] = 1f, [803] = 1f, [804] = 1f, [805] = 1f, + }; + + /// The worn list the bake composites, built from the derived wearables in the order they carry. + private static List Worn(AvatarWearable[] wearables, IReadOnlyDictionary shirtTextures) + { + var worn = new List + { + new() { Kind = WearableKind.Shape, Label = "Shape", Params = new Dictionary { [80] = 0f } }, + new() + { + Kind = WearableKind.Skin, Label = "Skin", Params = new Dictionary { [111] = 0.5f }, + TextureIds = new Dictionary { [TextureSlot.UpperBodypaint] = UUID.Random() }, + Textures = new Dictionary { [TextureSlot.UpperBodypaint] = Flat(200, 150, 120) }, + }, + }; + var slot = wearables[Shirt]; + for (var j = 0; j < slot.Count; j++) + { + worn.Add(new WornWearable + { + Kind = WearableKind.Shirt, Label = $"Shirt:{j}", Params = FullShirt, + TextureIds = new Dictionary { [TextureSlot.UpperShirt] = UUID.Random() }, + Textures = new Dictionary { [TextureSlot.UpperShirt] = shirtTextures[slot[j].AssetID] }, + }); + } + return worn; + } + + private static (int Red, int Blue) Count(RgbaPlanes img) + { + int red = 0, blue = 0; + for (var i = 0; i < img.R.Length; i++) + { + // Saturated red / saturated blue, not an exact colour: the shirt layer is tinted by the wearable's + // own colour parameters, and the skin underneath it (200,150,120) is neither. + if (img.R[i] > 150 && img.R[i] > img.G[i] + 80 && img.R[i] > img.B[i] + 80) red++; + else if (img.B[i] > 150 && img.B[i] > img.G[i] + 80 && img.B[i] > img.R[i] + 80) blue++; + } + return (red, blue); + } + + [Fact] + public void TheHigherIndexedShirtIsCompositedOverTheLowerOne() + { + const int size = 256; + var red = UUID.Random(); var blue = UUID.Random(); + var textures = new Dictionary { [red] = Flat(255, 0, 0), [blue] = Flat(0, 0, 255) }; + var compositor = new TexLayerCompositor(); + + // "@400" red, "@401" blue: blue is on top, and nothing red survives it. + var blueOnTop = CofWearables.Derive(Empty(), new[] + { + Link("@400", Shirt, UUID.Random(), red), + Link("@401", Shirt, UUID.Random(), blue), + }, out _); + var (r1, b1) = Count(compositor.Bake(BakeChannel.Upper, Worn(blueOnTop, textures), size).Image); + Assert.True(b1 > 1000, $"the @401 shirt must cover the upper body; {b1} blue pixels"); + Assert.Equal(0, r1); + + // Swap the two order strings and the bake swaps with them. + var redOnTop = CofWearables.Derive(Empty(), new[] + { + Link("@401", Shirt, UUID.Random(), red), + Link("@400", Shirt, UUID.Random(), blue), + }, out _); + var (r2, b2) = Count(compositor.Bake(BakeChannel.Upper, Worn(redOnTop, textures), size).Image); + Assert.True(r2 > 1000, $"the @401 shirt must cover the upper body; {r2} red pixels"); + Assert.Equal(0, b2); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/FakeAssetService.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/FakeAssetService.cs new file mode 100644 index 00000000000..02b30c4af29 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/FakeAssetService.cs @@ -0,0 +1,38 @@ +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Services.Interfaces; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// An in-memory IAssetService: whatever was Put or Stored, keyed by id string. records every +/// Get / Store / Delete in order, which is how the supersede tests prove the old asset went only after the new +/// one had landed. +/// +public sealed class FakeAssetService : IAssetService +{ + public readonly Dictionary Assets = new(StringComparer.OrdinalIgnoreCase); + public readonly List Stored = new(); + public readonly List Ops = new(); + + public void Put(AssetBase asset) => Assets[asset.ID] = asset; + public bool Remove(string id) => Assets.Remove(id); + + /// Ids fetched with Get since the last , in order. + public IEnumerable Fetched => Ops.Where(o => o.StartsWith("get ")).Select(o => o[4..]); + + public void ResetOps() { Ops.Clear(); Stored.Clear(); } + + public AssetBase Get(string id) { Ops.Add("get " + id); return Assets.TryGetValue(id, out var a) ? a : null; } + public AssetBase Get(string id, string foreignAssetService, bool storeOnLocalGrid) => Get(id); + public AssetBase GetUnchecked(string id) => Assets.TryGetValue(id, out var a) ? a : null; + public AssetMetadata GetMetadata(string id) => Assets.TryGetValue(id, out var a) ? a.Metadata : null; + public byte[] GetData(string id) => Get(id)?.Data; + public AssetBase GetCached(string id) => Get(id); + public bool Get(string id, object sender, AssetRetrieved handler) { handler(id, sender, Get(id)); return true; } + public void Get(string id, string foreignAssetService, bool storeOnLocalGrid, SimpleAssetRetrieved callBack) => callBack(Get(id)); + public bool[] AssetsExist(string[] ids) => ids.Select(i => Assets.ContainsKey(i)).ToArray(); + public string Store(AssetBase asset) { Ops.Add("store " + asset.ID); Assets[asset.ID] = asset; Stored.Add(asset); return asset.ID; } + public bool UpdateContent(string id, byte[] data) { if (!Assets.TryGetValue(id, out var a)) return false; a.Data = data; return true; } + public bool Delete(string id) { Ops.Add("delete " + id); return Assets.Remove(id); } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/FakeAvatarService.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/FakeAvatarService.cs new file mode 100644 index 00000000000..64748a0534d --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/FakeAvatarService.cs @@ -0,0 +1,57 @@ +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Services.Interfaces; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// An in-memory IAvatarService over the same key/value shape as the Avatars table: one string map per principal. +/// Only the three calls the bake index uses do anything — GetAvatar, SetItems, RemoveItems — which is the point: +/// if a test passes against this, the production path needs no service change either. +/// +/// +/// reproduces the real service's behaviour faithfully, delete-everything-first +/// included (AvatarService.cs:93), because that is the hazard the bake index lives with. +/// +/// +public sealed class FakeAvatarService : IAvatarService +{ + public readonly Dictionary> Records = new(); + public int SetItemsCalls; + + private Dictionary Record(UUID id) + { + if (!Records.TryGetValue(id, out var d)) Records[id] = d = new Dictionary(); + return d; + } + + public AvatarData GetAvatar(UUID userID) + => new() { AvatarType = 1, Data = new Dictionary(Record(userID)) }; + + public bool SetAvatar(UUID userID, AvatarData avatar) + { + Records[userID] = new Dictionary(avatar.Data); // AvatarService.SetAvatar deletes every row first + return true; + } + + public AvatarAppearance GetAppearance(UUID userID) => GetAvatar(userID).ToAvatarAppearance(); + public bool SetAppearance(UUID userID, AvatarAppearance appearance) => SetAvatar(userID, new AvatarData(appearance)); + + public bool ResetAvatar(UUID userID) => Records.Remove(userID); + + public bool SetItems(UUID userID, string[] names, string[] values) + { + if (names.Length != values.Length) return false; + SetItemsCalls++; + var d = Record(userID); + for (var i = 0; i < names.Length; i++) d[names[i]] = values[i]; + return true; + } + + public bool RemoveItems(UUID userID, string[] names) + { + var d = Record(userID); + foreach (var n in names) d.Remove(n); + return true; + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/LoginResponseAdvertisementTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/LoginResponseAdvertisementTests.cs new file mode 100644 index 00000000000..04540ba4c48 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/LoginResponseAdvertisementTests.cs @@ -0,0 +1,145 @@ +using System.Collections; +using Microsoft.Extensions.Logging.Abstractions; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Services.LLLoginService; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S4 Part 2 — viewer contract V6. The login response advertises agent_appearance_service when the grid +/// has one and omits the key entirely when it does not. +/// +/// +/// The distinction matters because of what the viewer does with it: llstartup.cpp:4047-4051 adopts the +/// value only when non-empty, and LLVOAvatar::getImageURL warns +/// "AgentAppearanceServiceURL not set - Baked texture requests will fail" and returns "" when it is unset +/// (llvoavatar.cpp:5901-5906). On a bit-0 region that means the avatar never textures — which is exactly +/// what Ebony produced when S3's flag was flipped before this service existed. +/// +/// +public class LoginResponseAdvertisementTests +{ + private const string Key = "agent_appearance_service"; + + static LoginResponseAdvertisementTests() + { + // LLLoginResponse.ToHashtable wraps its whole body in try/catch and returns the *failure* response on any + // exception (LLLoginResponse.cs:483, :593-597). It logs inside that body, and with no logger factory + // installed the log call throws, so every assertion here would silently be made against the failure + // hashtable instead of the real one. Installing a null factory is what makes this test test anything. + LoggerProvider.LoggerFactory = NullLoggerFactory.Instance; + } + + /// + /// A fresh response per encoding. ToHashtable and ToOSDMap both mutate the instance they are + /// called on — they append to loginFlags, globalTextures and uiConfig — and both swallow + /// any exception and return the *failure* response instead (LLLoginResponse.cs:593-597, :712-717). Calling + /// them one after the other on one object therefore makes the second one fail silently, which is not + /// something production ever does: the login service builds a response and serialises it once. + /// + /// + /// A response the serialisers can actually render. The parameterless constructor leaves + /// inventoryLibRoot null (LLLoginResponse.cs:141 — every other ArrayList is initialised, that one is + /// not), and ToOSDMap passes it straight to ArrayListToOSDArray (:664), so the LLSD form of a + /// default-constructed response always throws and silently degrades to the failure map. Production never hits + /// it because the real constructor fills it in. Setting it here is the fixture, not a workaround for anything + /// S4 introduced. + /// + private static LLLoginResponse Fresh(string url) + { + var r = new LLLoginResponse { InventoryLibRoot = new ArrayList() }; + if (url is not null) r.AgentAppearanceServiceURL = url; + return r; + } + + private static Hashtable Xml(string url) + { + var r = Fresh(url); + var h = r.ToHashtable(); + Assert.True(h.ContainsKey("session_id"), "the response should have been built, not the failure form"); + return h; + } + + private static OSDMap Llsd(string url) + { + var r = Fresh(url); + var m = (OSDMap)r.ToOSDMap(); + Assert.True(m.ContainsKey("session_id"), "the response should have been built, not the failure form"); + return m; + } + + private static (Hashtable Xml, OSDMap Llsd) Build(string url) => (Xml(url), Llsd(url)); + + [Fact] + public void WhenConfiguredTheResponseCarriesTheUrlInBothEncodings() + { + var (xml, llsd) = Build("http://legiongrid.ddns.net:8002/"); + + Assert.True(xml.ContainsKey(Key)); + Assert.Equal("http://legiongrid.ddns.net:8002/", xml[Key]); + Assert.True(llsd.ContainsKey(Key)); + Assert.Equal("http://legiongrid.ddns.net:8002/", llsd[Key].AsString()); + } + + [Fact] + public void WhenNotConfiguredTheKeyIsAbsentEntirelyRatherThanEmpty() + { + var (xml, llsd) = Build(null); + + Assert.False(xml.ContainsKey(Key)); + Assert.False(llsd.ContainsKey(Key)); + } + + [Fact] + public void AnEmptyOrNullValueIsAlsoAnAbsentKey() + { + foreach (var value in new[] { "", null }) + { + var (xml, llsd) = Build(value); + Assert.False(xml.ContainsKey(Key), $"value '{value}'"); + Assert.False(llsd.ContainsKey(Key), $"value '{value}'"); + } + } + + /// + /// The response is otherwise untouched: advertising this must not perturb anything a viewer already reads. + /// Compared key set by key set, configured against not. + /// + [Fact] + public void NothingElseInTheResponseChanges() + { + var (withXml, withLlsd) = Build("http://example.invalid:8002/"); + var (withoutXml, withoutLlsd) = Build(null); + + var added = withXml.Keys.Cast().Except(withoutXml.Keys.Cast()).ToArray(); + Assert.Equal(new[] { Key }, added); + Assert.Empty(withoutXml.Keys.Cast().Except(withXml.Keys.Cast())); + + var addedLlsd = withLlsd.Keys.Except(withoutLlsd.Keys).ToArray(); + Assert.Equal(new[] { Key }, addedLlsd); + Assert.Empty(withoutLlsd.Keys.Except(withLlsd.Keys)); + } + + /// + /// The trailing slash is not cosmetic. The viewer builds the URL as + /// appearance_service_url + "texture/" + ... with no separator (llvoavatar.cpp:5912), so a + /// configured value without one yields ...:8002texture/<agent>/... and every fetch fails. The + /// login service normalises it exactly as it does MapTileURL; this pins the arithmetic the + /// normalisation exists to protect. + /// + [Fact] + public void TheViewerConcatenatesWithoutASeparatorSoTheValueMustEndInSlash() + { + const string agent = "a7d2ff2e-dc32-44d8-aa61-3d22070a4964"; + const string asset = "11111111-2222-3333-4444-555555555555"; + + var good = "http://host:8002/" + "texture/" + agent + "/" + "head" + "/" + asset; + Assert.Equal($"http://host:8002/texture/{agent}/head/{asset}", good); + + var bad = "http://host:8002" + "texture/" + agent + "/" + "head" + "/" + asset; + Assert.Equal($"http://host:8002texture/{agent}/head/{asset}", bad); + Assert.DoesNotContain("/texture/", bad); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/OneSwitchConfigTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/OneSwitchConfigTests.cs new file mode 100644 index 00000000000..fb8a76ae1a1 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/OneSwitchConfigTests.cs @@ -0,0 +1,178 @@ +using Nini.Config; +using OpenSim.Region.ClientStack.LindenCaps.AIS; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S12. The operator contract: **two lines in the global config turn AIS v3 and server-side baking on for the +/// whole simulator, and no region is ever named.** A [<Region Name>] section is an optional override +/// — the way a single region opts *out* — and never the way to opt in. +/// +/// +/// The four cases are asserted for both modules together, because "one switch for the grid" is a property of the +/// pair, not of either alone: an operator who sets both globals and finds one lane on and the other off has been +/// let down whichever module is at fault. +/// +/// +public class OneSwitchConfigTests +{ + private const string Ebony = "Ebony"; + private const string Elm = "Elm"; + private const string Transylvania = "Transylvania"; + + /// A config source with the given sections and keys. + private static IConfigSource Config(params (string Section, string Key, string Value)[] entries) + { + var source = new IniConfigSource(); + foreach (var (section, key, value) in entries) + (source.Configs[section] ?? source.AddConfig(section)).Set(key, value); + return source; + } + + /// The global default each module reads in Initialise, from the same source a region sees. + private static bool AisGlobal(IConfigSource source) + => source.Configs[AISv3Module.ConfigSection]?.GetBoolean("Enabled", false) ?? false; + + private static bool SsbGlobal(IConfigSource source) + => source.Configs[ServerSideBakingModule.ConfigSection]?.GetBoolean("ServerSideBaking", false) ?? false; + + /// Both lanes for one region, resolved exactly as the modules resolve them. + private static (bool Ais, bool Ssb) On(IConfigSource source, string region) + => (AISv3Module.ResolveEnabled(AisGlobal(source), source, region), + ServerSideBakingRegion.ResolveEnabled(SsbGlobal(source), source, region)); + + /// The two lines an operator adds, and nothing else. No region is named anywhere. + private static IConfigSource GlobalOnly() => Config( + (AISv3Module.ConfigSection, "Enabled", "true"), + (ServerSideBakingModule.ConfigSection, "ServerSideBaking", "true")); + + // ------------------------------------------------------------------ (a) the whole point + + [Fact] + public void a_global_true_and_no_region_section_turns_every_region_on() + { + var source = GlobalOnly(); + + foreach (var region in new[] { Ebony, Elm, Transylvania }) + { + var (ais, ssb) = On(source, region); + Assert.True(ais, $"AIS must be on for {region} from the global switch alone"); + Assert.True(ssb, $"server-side baking must be on for {region} from the global switch alone"); + } + } + + [Fact] + public void the_two_global_lines_are_the_documented_keys_and_sections() + { + // If either name drifts, the line an operator was told to add stops working and nothing says so. These are + // the exact section and key names written into OpenSimDefaults.ini and the design brief's config contract. + Assert.Equal("AIS", AISv3Module.ConfigSection); + Assert.Equal("Appearance", ServerSideBakingModule.ConfigSection); + + var source = GlobalOnly(); + Assert.True(AisGlobal(source), "[AIS] Enabled = true"); + Assert.True(SsbGlobal(source), "[Appearance] ServerSideBaking = true"); + } + + // ------------------------------------------------------------------ (b) opt out + + [Fact] + public void b_a_region_section_of_false_opts_that_region_out_and_leaves_the_rest_on() + { + var source = GlobalOnly(); + source.AddConfig(Elm).Set("AIS_Enabled", "false"); + source.Configs[Elm].Set("ServerSideBaking", "false"); + + var elm = On(source, Elm); + Assert.False(elm.Ais, "Elm opted out of AIS"); + Assert.False(elm.Ssb, "Elm opted out of server-side baking"); + + foreach (var region in new[] { Ebony, Transylvania }) + { + var (ais, ssb) = On(source, region); + Assert.True(ais, $"{region} is untouched by Elm's opt-out"); + Assert.True(ssb, $"{region} is untouched by Elm's opt-out"); + } + } + + [Fact] + public void a_region_section_that_says_nothing_about_these_keys_does_not_opt_out() + { + // Regions commonly have a section for other settings. Its mere existence must not turn the lanes off - + // the region value is an override only when the KEY is there. + var source = GlobalOnly(); + source.AddConfig(Ebony).Set("SomeOtherSetting", "42"); + + var (ais, ssb) = On(source, Ebony); + Assert.True(ais); + Assert.True(ssb); + } + + // ------------------------------------------------------------------ (c) today's behaviour, kept + + [Fact] + public void c_global_false_and_a_region_section_of_true_turns_that_region_on() + { + // This is how Ebony runs today (config/OpenSim.ini:162-164) and it must keep working: the single-region + // trial is what every flip so far has depended on. + var source = Config( + (Ebony, "AIS_Enabled", "true"), + (Ebony, "ServerSideBaking", "true")); + + var ebony = On(source, Ebony); + Assert.True(ebony.Ais); + Assert.True(ebony.Ssb); + + var elm = On(source, Elm); + Assert.False(elm.Ais, "a region with no section stays off when the global is off"); + Assert.False(elm.Ssb); + } + + // ------------------------------------------------------------------ (d) nothing set + + [Fact] + public void d_nothing_set_anywhere_is_off() + { + var source = Config(); + + foreach (var region in new[] { Ebony, Elm, Transylvania }) + { + var (ais, ssb) = On(source, region); + Assert.False(ais, $"AIS must default off for {region}"); + Assert.False(ssb, $"server-side baking must default off for {region}"); + } + } + + [Fact] + public void a_null_config_source_or_an_unnamed_region_falls_back_to_the_global() + { + Assert.True(AISv3Module.ResolveEnabled(true, null, Ebony)); + Assert.False(AISv3Module.ResolveEnabled(false, null, Ebony)); + Assert.True(ServerSideBakingRegion.ResolveEnabled(true, null, Ebony)); + Assert.False(ServerSideBakingRegion.ResolveEnabled(false, null, Ebony)); + + var source = GlobalOnly(); + Assert.True(AISv3Module.ResolveEnabled(AisGlobal(source), source, null)); + Assert.True(ServerSideBakingRegion.ResolveEnabled(SsbGlobal(source), source, "")); + } + + // ------------------------------------------------------------------ which source decided + + [Fact] + public void the_deciding_source_is_reported_so_the_flip_can_be_verified_from_the_log() + { + var source = GlobalOnly(); + source.AddConfig(Elm).Set("ServerSideBaking", "false"); + source.AddConfig(Transylvania).Set("SomeOtherSetting", "42"); + + Assert.Equal("global", ServerSideBakingRegion.EnabledSource(source, Ebony)); + Assert.Equal("region section", ServerSideBakingRegion.EnabledSource(source, Elm)); + Assert.Equal("global", ServerSideBakingRegion.EnabledSource(source, Transylvania)); + + source.AddConfig(Ebony).Set("AIS_Enabled", "true"); + Assert.Equal("region section", AISv3Module.EnabledSource(source, Ebony)); + Assert.Equal("global", AISv3Module.EnabledSource(source, Elm)); + } +} diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests.csproj b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests.csproj new file mode 100644 index 00000000000..baad049b05a --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests.csproj @@ -0,0 +1,40 @@ + + + + + net10.0 + disable + enable + true + false + false + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/WireTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/WireTests.cs new file mode 100644 index 00000000000..69a0820c613 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/WireTests.cs @@ -0,0 +1,381 @@ +using System.Reflection; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Region.OptionalModules.Avatar.ServerSideBaking; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// S3 Part 1 — the wire. The §4.3 COF handshake in all four of its branches, the per-region flag, and the one +/// thing ADR-001 will not tolerate: that a flag-off region's AvatarAppearance packet is byte-for-byte the +/// packet it was before server-side baking existed. +/// +public class WireTests +{ + private static readonly UUID Agent = new("a7d2ff2e-dc32-44d8-aa61-3d22070a4964"); + private static readonly DateTime T0 = new(2026, 9, 4, 21, 0, 0, DateTimeKind.Utc); + + private static int NeverCalled() => throw new InvalidOperationException("the folder must not be re-read on this branch"); + + // ------------------------------------------------------------------ the four handshake branches + + [Fact] + public void Handshake_Equal_Bakes() + { + var h = new CofHandshake(); + var d = h.Decide(Agent, clientVersion: 7, serverVersion: 7, NeverCalled, T0); + + Assert.Equal(CofVerdict.Bake, d.Verdict); + Assert.True(d.Success); + Assert.Equal(7, d.Version); + Assert.Equal(0, h.MismatchesFor(Agent)); + } + + [Fact] + public void Handshake_ClientBehind_IsStaleAndQuotesTheServersVersion() + { + var h = new CofHandshake(); + var d = h.Decide(Agent, clientVersion: 5, serverVersion: 9, NeverCalled, T0); + + Assert.Equal(CofVerdict.Stale, d.Verdict); + Assert.False(d.Success); + Assert.Equal(9, d.Version); // this is the `expected` the viewer re-requests with + Assert.Equal(1, h.MismatchesFor(Agent)); + } + + /// + /// The viewer is ahead because it changed the COF by a path this sim has not seen — with AIS live, that is + /// the normal case. The folder is re-read once, and if the write has landed by then it is a plain match. + /// + [Fact] + public void Handshake_ClientAhead_RereadsOnceAndBakesWhenItCatchesUp() + { + var h = new CofHandshake(); + var rereads = 0; + int Reread() { rereads++; return 11; } + + var d = h.Decide(Agent, clientVersion: 11, serverVersion: 9, Reread, T0); + + Assert.Equal(1, rereads); + Assert.Equal(CofVerdict.Bake, d.Verdict); + Assert.Equal(11, d.Version); + Assert.Equal(0, h.MismatchesFor(Agent)); + } + + /// + /// Still ahead after the re-read: the sim cannot honestly claim the viewer's number, so it quotes the freshly + /// read one and lets the viewer come back. Exactly one re-read, never a loop of them. + /// + [Fact] + public void Handshake_ClientStillAheadAfterTheReread_IsStaleWithTheFreshVersion() + { + var h = new CofHandshake(); + var rereads = 0; + int Reread() { rereads++; return 10; } + + var d = h.Decide(Agent, clientVersion: 12, serverVersion: 9, Reread, T0); + + Assert.Equal(1, rereads); + Assert.Equal(CofVerdict.Stale, d.Verdict); + Assert.Equal(10, d.Version); // the re-read value, not the stale 9 + Assert.Equal(1, h.MismatchesFor(Agent)); + } + + [Fact] + public void Handshake_ARereadThatThrows_DoesNotEscapeAndStillAnswers() + { + var h = new CofHandshake(); + var d = h.Decide(Agent, clientVersion: 12, serverVersion: 9, () => throw new TimeoutException("inventory service"), T0); + + Assert.Equal(CofVerdict.Stale, d.Verdict); + Assert.Equal(9, d.Version); + } + + // ------------------------------------------------------------------ anti-livelock (Ledger R-2) + + [Fact] + public void Handshake_TooManyMismatchesInTheWindow_BakesAnyway() + { + var h = new CofHandshake { MaxMismatches = 3, Window = TimeSpan.FromSeconds(30) }; + + Assert.Equal(CofVerdict.Stale, h.Decide(Agent, 5, 9, NeverCalled, T0).Verdict); + Assert.Equal(CofVerdict.Stale, h.Decide(Agent, 5, 9, NeverCalled, T0.AddSeconds(1)).Verdict); + + var third = h.Decide(Agent, 5, 9, NeverCalled, T0.AddSeconds(2)); + Assert.Equal(CofVerdict.LivelockBake, third.Verdict); + Assert.True(third.Success); + Assert.Equal(9, third.Version); // baked at the server's version, per §4.3 + Assert.Contains("3 mismatches", third.Reason); + + // and it starts over afterwards, so the next disagreement is refused normally again + Assert.Equal(0, h.MismatchesFor(Agent)); + Assert.Equal(CofVerdict.Stale, h.Decide(Agent, 5, 9, NeverCalled, T0.AddSeconds(3)).Verdict); + } + + [Fact] + public void Handshake_MismatchesOutsideTheWindowDoNotAccumulate() + { + var h = new CofHandshake { MaxMismatches = 3, Window = TimeSpan.FromSeconds(30) }; + + Assert.Equal(CofVerdict.Stale, h.Decide(Agent, 5, 9, NeverCalled, T0).Verdict); + Assert.Equal(CofVerdict.Stale, h.Decide(Agent, 5, 9, NeverCalled, T0.AddSeconds(10)).Verdict); + // past the window: the count restarts rather than tripping the rule + Assert.Equal(CofVerdict.Stale, h.Decide(Agent, 5, 9, NeverCalled, T0.AddSeconds(41)).Verdict); + Assert.Equal(1, h.MismatchesFor(Agent)); + Assert.Equal(CofVerdict.Stale, h.Decide(Agent, 5, 9, NeverCalled, T0.AddSeconds(42)).Verdict); + } + + [Fact] + public void Handshake_ASuccessfulBakeClearsTheAgentsCounter() + { + var h = new CofHandshake { MaxMismatches = 3 }; + h.Decide(Agent, 5, 9, NeverCalled, T0); + h.Decide(Agent, 5, 9, NeverCalled, T0.AddSeconds(1)); + Assert.Equal(2, h.MismatchesFor(Agent)); + + Assert.Equal(CofVerdict.Bake, h.Decide(Agent, 9, 9, NeverCalled, T0.AddSeconds(2)).Verdict); + Assert.Equal(0, h.MismatchesFor(Agent)); + } + + [Fact] + public void Handshake_CountersArePerAgent() + { + var h = new CofHandshake { MaxMismatches = 2 }; + var other = UUID.Random(); + + Assert.Equal(CofVerdict.Stale, h.Decide(Agent, 5, 9, NeverCalled, T0).Verdict); + Assert.Equal(CofVerdict.Stale, h.Decide(other, 5, 9, NeverCalled, T0).Verdict); + Assert.Equal(1, h.MismatchesFor(Agent)); + Assert.Equal(1, h.MismatchesFor(other)); + } + + // ------------------------------------------------------------------ the per-region flag + + private static IConfigSource Config(params (string Section, string Key, string Value)[] entries) + { + var src = new IniConfigSource(); + foreach (var (section, key, value) in entries) + (src.Configs[section] ?? src.AddConfig(section)).Set(key, value); + return src; + } + + [Theory] + // simulatorDefault, region section, region value -> expected + [InlineData(false, null, null, false)] // nothing anywhere: off, which is what every shipped ini says + [InlineData(true, null, null, true)] // simulator-wide on, no region section + [InlineData(false, "Ebony", "true", true)] // the case S3 ships for: one region opts in + [InlineData(true, "Ebony", "false", false)] // and a region can opt out of a simulator-wide on + [InlineData(false, "Elm", "true", false)] // a section for a different region does not apply + public void FlagResolvesPerRegionLikeAisEnabledDoes(bool simulatorDefault, string section, string value, bool expected) + { + var config = section is null ? Config() : Config((section, "ServerSideBaking", value)); + Assert.Equal(expected, ServerSideBakingRegion.ResolveEnabled(simulatorDefault, config, "Ebony")); + } + + [Fact] + public void FlagFallsBackToTheSimulatorDefaultWhenThereIsNoConfigOrNoRegionName() + { + Assert.True(ServerSideBakingRegion.ResolveEnabled(true, null, "Ebony")); + Assert.False(ServerSideBakingRegion.ResolveEnabled(false, null, "Ebony")); + Assert.True(ServerSideBakingRegion.ResolveEnabled(true, Config(("Ebony", "ServerSideBaking", "false")), null)); + } + + // ------------------------------------------------------------------ what the flag gates + + [Fact] + public void AFlagOffRegionNeverReportsABakedCofVersion() + { + var off = new ServerSideBakingRegion(false, new CofHandshake()); + Assert.False(off.ServerSideBakingEnabled); + Assert.Equal(-1, off.BakedCofVersion(Agent)); + + // a console bake on a flag-off region still writes faces and sends, but must not reach the wire + off.RecordBake(Agent, 42); + Assert.Equal(-1, off.BakedCofVersion(Agent)); + } + + [Fact] + public void AFlagOnRegionReportsTheVersionItBakedAtAndForgetsOnClose() + { + var on = new ServerSideBakingRegion(true, new CofHandshake()); + Assert.Equal(-1, on.BakedCofVersion(Agent)); + + on.RecordBake(Agent, 42); + Assert.Equal(42, on.BakedCofVersion(Agent)); + + on.RecordBake(Agent, 43); + Assert.Equal(43, on.BakedCofVersion(Agent)); + + // a negative version is "no bake", never recorded + on.RecordBake(Agent, -1); + Assert.Equal(43, on.BakedCofVersion(Agent)); + + on.Forget(Agent); + Assert.Equal(-1, on.BakedCofVersion(Agent)); + Assert.Equal(0, on.Handshake.MismatchesFor(Agent)); + } + + // ------------------------------------------------------------------ ADR-001: the flag-off packet is unchanged + + /// + /// The AvatarAppearance packet body as LLClientView.SendAppearance writes it, replayed here field for + /// field so the two forms can be compared without a UDP server. The layout under test is the one thing S3 + /// changed in a hot path: the AppearanceData block, which was a hard-coded count of 0 and is now a count of 0 + /// or one 9-byte block. + /// + private static byte[] AppearanceBody(byte[] textureEntry, byte[] visualParams, float hover, int cofVersion) + { + var data = new byte[4096]; + int pos = 0; + Agent.ToBytes(data, pos); pos += 16; + data[pos++] = 0; + + int len = textureEntry.Length; + if (len == 0) { data[pos++] = 0; data[pos++] = 0; } + else + { + data[pos++] = (byte)len; + data[pos++] = (byte)(len >> 8); + Buffer.BlockCopy(textureEntry, 0, data, pos, len); pos += len; + } + + len = visualParams.Length; + data[pos++] = (byte)len; + if (len > 0) Buffer.BlockCopy(visualParams, 0, data, pos, len); + pos += len; + + if (cofVersion < 0) + { + data[pos++] = 0; + } + else + { + data[pos++] = 1; + data[pos++] = 1; + Utils.IntToBytesSafepos(cofVersion, data, pos); pos += 4; + Utils.UIntToBytesSafepos(0, data, pos); pos += 4; + } + + data[pos++] = 1; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + Utils.FloatToBytesSafepos(hover, data, pos); pos += 4; + return data[..pos]; + } + + /// + /// The pre-S3 form, transcribed from the code as it stood at 95c3eefbbe: a single zero byte where the + /// AppearanceData count goes, and nothing else different. + /// + private static byte[] AppearanceBodyBeforeS3(byte[] textureEntry, byte[] visualParams, float hover) + { + var data = new byte[4096]; + int pos = 0; + Agent.ToBytes(data, pos); pos += 16; + data[pos++] = 0; + + int len = textureEntry.Length; + if (len == 0) { data[pos++] = 0; data[pos++] = 0; } + else + { + data[pos++] = (byte)len; + data[pos++] = (byte)(len >> 8); + Buffer.BlockCopy(textureEntry, 0, data, pos, len); pos += len; + } + + len = visualParams.Length; + data[pos++] = (byte)len; + if (len > 0) Buffer.BlockCopy(visualParams, 0, data, pos, len); + pos += len; + + data[pos++] = 0; // "// no AppearanceData" + data[pos++] = 1; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + Utils.FloatToBytesSafepos(0, data, pos); pos += 4; + Utils.FloatToBytesSafepos(hover, data, pos); pos += 4; + return data[..pos]; + } + + [Theory] + [InlineData(0, 0)] + [InlineData(45, 218)] + [InlineData(300, 0)] // a TextureEntry over 255 bytes, to exercise the two-byte length + public void OnAFlagOffRegionTheAppearancePacketIsByteIdenticalToTheOldOne(int teLength, int vpLength) + { + var te = new byte[teLength]; + for (var i = 0; i < te.Length; i++) te[i] = (byte)(i * 7 + 1); + var vp = new byte[vpLength]; + for (var i = 0; i < vp.Length; i++) vp[i] = (byte)(255 - i); + + // -1 is what SendAppearanceToAgentNF passes when the region has no baking module, or the flag is off, or + // this sim has not baked the avatar + Assert.Equal(AppearanceBodyBeforeS3(te, vp, 1.25f), AppearanceBody(te, vp, 1.25f, -1)); + } + + [Fact] + public void OnAFlagOnRegionTheAppearanceCarriesExactlyOneNineByteAppearanceDataBlock() + { + var te = new byte[45]; + var vp = new byte[218]; + + var without = AppearanceBody(te, vp, 0f, -1); + var with = AppearanceBody(te, vp, 0f, 7); + + // count byte plus AppearanceVersion(1) + CofVersion(4) + Flags(4) + Assert.Equal(without.Length + 9, with.Length); + + var at = 16 + 1 + 2 + te.Length + 1 + vp.Length; + Assert.Equal(0, without[at]); + Assert.Equal(1, with[at]); // one block + Assert.Equal(1, with[at + 1]); // AppearanceVersion = 1 (V5) + Assert.Equal(7, BitConverter.ToInt32(with, at + 2)); + Assert.Equal(0u, BitConverter.ToUInt32(with, at + 6)); + // everything before the block is untouched + Assert.Equal(without[..at], with[..at]); + } + + /// + /// The two forms above are only worth anything if they match the shipped code. This reads + /// LLClientView.SendAppearance's source and asserts the branch is there, so a change to the packet + /// writer that is not mirrored here fails rather than passing silently. + /// + [Fact] + public void TheReplayedLayoutMatchesWhatLLClientViewActuallyWrites() + { + var root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + var file = Path.Combine(root, "Source", "OpenSim.Region.ClientStack.LindenUDP", "LLClientView.cs"); + Assert.True(File.Exists(file), file); + var src = File.ReadAllText(file); + + var i = src.IndexOf("public void SendAppearance(UUID targetID, byte[] visualParams, byte[] textureEntry, float hover, int cofVersion)", StringComparison.Ordinal); + Assert.True(i > 0, "the AppearanceData-bearing SendAppearance overload is gone or renamed"); + var body = src[i..(i + 3000)]; + + Assert.Contains("if (cofVersion < 0)", body); + Assert.Contains("data[pos++] = 1;", body); + Assert.Contains("Utils.IntToBytesSafepos(cofVersion, data, pos); pos += 4;", body); + Assert.Contains("Utils.UIntToBytesSafepos(0, data, pos); pos += 4;", body); + + // and the four-argument overload still exists and still means "no AppearanceData" + Assert.Contains("public void SendAppearance(UUID targetID, byte[] visualParams, byte[] textureEntry, float hover)\n => SendAppearance(targetID, visualParams, textureEntry, hover, -1);", + src.Replace("\r\n", "\n"), StringComparison.Ordinal); + } + + /// RegionProtocols keeps bit 63 and gains bit 0 only behind the flag. + [Fact] + public void RegionProtocolsSetsBitZeroOnlyWhenTheFlagIsOn() + { + var root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + var src = File.ReadAllText(Path.Combine(root, "Source", "OpenSim.Region.ClientStack.LindenUDP", "LLClientView.cs")).Replace("\r\n", "\n"); + + Assert.Contains("ulong regionProtocols = 1UL << 63;", src); + Assert.Contains("if (m_scene.RequestModuleInterface() is { ServerSideBakingEnabled: true })\n regionProtocols |= 1UL;", src); + Assert.Contains("zc.AddUInt64(regionProtocols);", src); + Assert.DoesNotContain("zc.AddUInt64(1UL << 63);", src); + + // the arithmetic itself, so the constants cannot rot + Assert.Equal(0x8000000000000000UL, 1UL << 63); + Assert.Equal(0x8000000000000001UL, (1UL << 63) | 1UL); + } +} diff --git a/Tests/OpenSim.Tests.Common/Mock/TestClient.cs b/Tests/OpenSim.Tests.Common/Mock/TestClient.cs index 369043263db..e3435ea0272 100644 --- a/Tests/OpenSim.Tests.Common/Mock/TestClient.cs +++ b/Tests/OpenSim.Tests.Common/Mock/TestClient.cs @@ -566,6 +566,11 @@ public virtual void SendWearables(AvatarWearable[] wearables, int serial) { } + public virtual void SendAppearance(UUID agentID, byte[] visualParams, byte[] textureEntry, float hover, int cofVersion) + { + } + + public virtual void SendAppearance(UUID agentID, byte[] visualParams, byte[] textureEntry, float hover) { } @@ -851,6 +856,14 @@ public virtual void SendPlayAttachedSound(UUID soundID, UUID objectID, UUID owne { } + /// + /// Test seam: raise as if an AgentIsNowWearing packet had arrived. + /// + public void TriggerAvatarNowWearing(AvatarWearingArgs e) + { + OnAvatarNowWearing?.Invoke(this, e); + } + public void SendTriggeredSound(UUID soundID, UUID ownerID, UUID objectID, UUID parentID, ulong handle, Vector3 position, float gain) { } diff --git a/Tests/OpenSim.Tests.Common/OpenSimTestCase.cs b/Tests/OpenSim.Tests.Common/OpenSimTestCase.cs index b6a9f301019..24aeeca18d6 100644 --- a/Tests/OpenSim.Tests.Common/OpenSimTestCase.cs +++ b/Tests/OpenSim.Tests.Common/OpenSimTestCase.cs @@ -29,7 +29,18 @@ namespace OpenSim.Tests.Common; -public class OpenSimTestCase : IDisposable +/// +/// Base class for every xunit test class in the tree. xunit constructs a fresh instance per test; NUnit's +/// per-test [SetUp] is reproduced through , which xunit calls after +/// the constructor (subclass constructor bodies included) and before the test method, exactly where NUnit ran +/// [SetUp]. Subclasses override and call base.SetUp(). Teardown stays on +/// : xunit 2 calls DisposeAsync and then Dispose, so DisposeAsync is a no-op. +/// +/// History: the xunit migration (#197) turned the subclasses' [SetUp] methods into overrides of SetUp() and +/// nothing invoked them, so every test relying on fields assigned in SetUp ran against null +/// (Docs/feature/repo-audit/T1-TEST-FIXTURES.md). +/// +public class OpenSimTestCase : IDisposable, Xunit.IAsyncLifetime { protected OpenSimTestCase() { @@ -48,13 +59,23 @@ protected OpenSimTestCase() } /// - /// For subclasses that override SetUp() - provides per-test setup functionality. + /// Per-test setup, run before every test method (the NUnit [SetUp] equivalent). Override and call base.SetUp(). /// public virtual void SetUp() { // Override in subclasses for per-test setup } + /// xunit's post-construction hook: runs before the test method. + public virtual Task InitializeAsync() + { + SetUp(); + return Task.CompletedTask; + } + + /// xunit calls this before ; teardown lives in Dispose so nothing runs twice. + public virtual Task DisposeAsync() => Task.CompletedTask; + public virtual void Dispose() { // Do "global" teardown here; Called after every test method. diff --git a/Tranquillity.sln b/Tranquillity.sln index f6072f25c5d..9b0c8f8565d 100644 --- a/Tranquillity.sln +++ b/Tranquillity.sln @@ -175,6 +175,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSim.Tests", "Tests\Open EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSim.TrustedHypergrid.Tests", "Tests\OpenSim.TrustedHypergrid.Tests\OpenSim.TrustedHypergrid.Tests.csproj", "{3714BB49-4D61-41E4-97F4-3AC9EA3B1BD1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSimNGC.Appearance.Baking", "Source\OpenSimNGC.Appearance.Baking\OpenSimNGC.Appearance.Baking.csproj", "{CE171E0C-2161-4434-BF4D-B2C562CD04CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSimNGC.Appearance.Baking.Tests", "Source\OpenSimNGC.Appearance.Baking.Tests\OpenSimNGC.Appearance.Baking.Tests.csproj", "{3798E75C-4DC7-4FD7-BB47-931C3086647F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSim.Region.ClientStack.LindenCaps.AIS.Tests", "Tests\OpenSim.Region.ClientStack.LindenCaps.AIS.Tests\OpenSim.Region.ClientStack.LindenCaps.AIS.Tests.csproj", "{55932F56-34A3-424D-B9E3-AC204231776F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSim.Region.OptionalModules.ServerSideBaking.Tests", "Tests\OpenSim.Region.OptionalModules.ServerSideBaking.Tests\OpenSim.Region.OptionalModules.ServerSideBaking.Tests.csproj", "{7A38CB4E-9986-4817-851C-F25D6EA74CD3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSim.Region.ClientStack.LindenCaps.Tests", "Tests\OpenSim.Region.ClientStack.LindenCaps.Tests\OpenSim.Region.ClientStack.LindenCaps.Tests.csproj", "{D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1181,6 +1191,66 @@ Global {3714BB49-4D61-41E4-97F4-3AC9EA3B1BD1}.Release|x64.Build.0 = Release|Any CPU {3714BB49-4D61-41E4-97F4-3AC9EA3B1BD1}.Release|x86.ActiveCfg = Release|Any CPU {3714BB49-4D61-41E4-97F4-3AC9EA3B1BD1}.Release|x86.Build.0 = Release|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Debug|x64.Build.0 = Debug|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Debug|x86.Build.0 = Debug|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Release|Any CPU.Build.0 = Release|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Release|x64.ActiveCfg = Release|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Release|x64.Build.0 = Release|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Release|x86.ActiveCfg = Release|Any CPU + {CE171E0C-2161-4434-BF4D-B2C562CD04CA}.Release|x86.Build.0 = Release|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Debug|x64.ActiveCfg = Debug|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Debug|x64.Build.0 = Debug|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Debug|x86.ActiveCfg = Debug|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Debug|x86.Build.0 = Debug|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Release|Any CPU.Build.0 = Release|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Release|x64.ActiveCfg = Release|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Release|x64.Build.0 = Release|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Release|x86.ActiveCfg = Release|Any CPU + {3798E75C-4DC7-4FD7-BB47-931C3086647F}.Release|x86.Build.0 = Release|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Debug|x64.ActiveCfg = Debug|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Debug|x64.Build.0 = Debug|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Debug|x86.ActiveCfg = Debug|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Debug|x86.Build.0 = Debug|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Release|Any CPU.Build.0 = Release|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Release|x64.ActiveCfg = Release|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Release|x64.Build.0 = Release|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Release|x86.ActiveCfg = Release|Any CPU + {55932F56-34A3-424D-B9E3-AC204231776F}.Release|x86.Build.0 = Release|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Debug|x64.ActiveCfg = Debug|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Debug|x64.Build.0 = Debug|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Debug|x86.Build.0 = Debug|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Release|Any CPU.Build.0 = Release|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Release|x64.ActiveCfg = Release|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Release|x64.Build.0 = Release|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Release|x86.ActiveCfg = Release|Any CPU + {7A38CB4E-9986-4817-851C-F25D6EA74CD3}.Release|x86.Build.0 = Release|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Debug|x64.Build.0 = Debug|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Debug|x86.Build.0 = Debug|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Release|Any CPU.Build.0 = Release|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Release|x64.ActiveCfg = Release|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Release|x64.Build.0 = Release|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Release|x86.ActiveCfg = Release|Any CPU + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1269,6 +1339,11 @@ Global {739DA041-246C-4525-A518-E6906D56F232} = {7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0} {5F82E524-3295-4ADD-AABF-0630E8B23EFE} = {7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0} {3714BB49-4D61-41E4-97F4-3AC9EA3B1BD1} = {7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0} + {CE171E0C-2161-4434-BF4D-B2C562CD04CA} = {E886CEB5-B01D-411F-A7A0-72AA1B71761D} + {3798E75C-4DC7-4FD7-BB47-931C3086647F} = {7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0} + {55932F56-34A3-424D-B9E3-AC204231776F} = {7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0} + {7A38CB4E-9986-4817-851C-F25D6EA74CD3} = {7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0} + {D294F8E7-1D95-4AB4-BDEE-FCF3190A54CA} = {7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A40AD54B-8BC7-41E4-B241-19B59EA65D2A} From 680c9cc9123aab8626907cea333a653005ab705c Mon Sep 17 00:00:00 2001 From: JohnLegionH Date: Sun, 20 Sep 2026 12:46:53 -0500 Subject: [PATCH 4/8] docs: Track L design set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design record behind the AIS v3 and server-side-baking lanes: the SSB recon and its addendum, the design brief, the ADR set, the SSB and combined Track L build plans, the S0a verification pass, the S1b fidelity surface, and the fact sheet written for review. Also the two repo-audit notes the lanes depend on: S8-WEARABLE-RESOLUTION (where the appearance save actually runs) and T1-TEST-FIXTURES (the NUnit lifecycle hooks orphaned by the xunit migration, and the LindenCaps.Tests restoration). The Legion-only operational records — the two lane ledgers, KnownDefects, the commit inventory and the R1 Robust reconciliation — are deliberately not carried; they record a specific grid's deployments, not this work. Folded from feature/ais-v3, 36 commits over these paths: c1fc7fff3e test(fixtures): NUnit lifecycle hooks orphaned by the xunit migration — CoreModules.Tests 35→5 failing d43f8cb362 test(caps): LindenCaps.Tests restored to the solution 2720a482ac docs(ssb): recon addendum, design brief, ADR set, build plans (SSB + Track L combined), ledger, Mike fact sheet 653466861c docs(ssb): recon addendum, design brief, ADR set, build plans (SSB + Track L combined), ledger, Mike fact sheet 5ed0d1ec3c docs(ssb): S0a verification pass c90de5e280 docs(ssb): ledger — Q-2/Q-3/Q-4 resolved, S0a/S0c logged 6f52f8bd81 docs(ssb): ledger P-1 (LL compositor is the authority), Q-7 resolved, Q-8/Q-9 opened, S0b logged; harness wording: reference bakes 73b842c1c3 docs(ssb): ledger — Q-8 resolved in S0d, session logged 0176eed007 docs(ssb): 5th component is the morph mask — rename, cite packing order bbc065bc5f test(ssb): golden RGB/A thresholds; docs: ADR-007 masks, packing-order citation 2b6d9bbbc2 docs(ssb): S1 done — first live server bake verified; Q-9 closed, Q-10/Q-11 opened; build plan actuals 2f85db334f docs(ssb): S1b fidelity surface, bake-size comparison, Q-11 status 2a916c73d0 docs(ssb): Q-12 resolved, S1c logged ab7e06c163 docs(ssb): ADR-008 default is 1024, superseding D-7 by measurement e39cfe4437 docs(ssb): Q-13 resolved, S1c/S1d/S1e logged 2697af65b0 docs(ais): step 7 is not viewer-reachable; Robust deploy verified live 3455deff98 test(ssb): close S1 DoD — golden diff at the shipped bake size 86880b7316 feat(ssb): persist bakes and skip recompute on unchanged inputs 162bfadcc3 perf(ssb): instrument the bake phases and answer Q-10 95c3eefbbe docs(ssb): the AIS/COF seam for S5; S2 live measurement 1ea8efa063 docs(ssb): S3 recorded — Q-14 closed, Q-16 ruled, Q-6 now measurable 26dc949379 docs(ssb): S4 done; the S3 flag flip and why the cloud was correct db64e6ff45 docs(ssb): S5 recorded — Q-6 answered, trigger and aux channels e2060d349f docs(ssb): §4.3 states the cap's new contract; S5 trigger breadth recorded b13f15add3 docs(ssb): withdraw the unsourced Q-6 "310 ms" measurement bcba73d4ca docs(ssb): S6 recon — Q-5 answered, LibreMetaverse surfaces everything needed 78ffbd392f docs(ssb): S5 live verify — ten COF mutations, ten bakes, paired ba7f0af96a docs(ssb): the gateway's own bake is overwritten on a bit-0 region b85b7b32fe docs(ssb): ADR-009 accepted, S6(b) ledger row 05c4e0847e docs(ssb): S9 ledger row, the trigger table, checklist step 17 77050c5372 docs(ssb): S10 Part 5 — trigger table, Q-11 live results, step 18 b0b3c6b671 fix(appearance): persist wearable types 15 and 16, and record S11 faf28cb71c docs(ssb): Q-11 — the verbatim bake line, and gloves/skirt were seen live 508ebc1b34 fix(ais): a refused wearable save is a 403, and library textures are legal bb4bcd03dc feat(config): S12 — one switch for the grid, and say which config decided dc0b7c03f1 fix(ssb): SSB-NPC-1 - an NPC's appearance must not claim a server bake it does not have --- .../repo-audit/S8-WEARABLE-RESOLUTION.md | 77 +++ Docs/feature/repo-audit/T1-TEST-FIXTURES.md | 108 +++++ .../ssb-appearance/ADR-SET-ssb-appearance.md | 120 +++++ .../BUILD-PLAN-ssb-appearance.md | 104 ++++ .../BUILD-PLAN-track-L-combined.md | 61 +++ .../DESIGN-BRIEF-ssb-appearance.md | 456 ++++++++++++++++++ .../FACT-SHEET-ais-ssb-for-mike.md | 40 ++ .../RECON-ssb-appearance-addendum.md | 80 +++ .../ssb-appearance/RECON-ssb-appearance.md | 263 ++++++++++ .../ssb-appearance/S0a-VERIFICATION.md | 76 +++ Docs/feature/ssb-appearance/S1b-FIDELITY.md | 215 +++++++++ 11 files changed, 1600 insertions(+) create mode 100644 Docs/feature/repo-audit/S8-WEARABLE-RESOLUTION.md create mode 100644 Docs/feature/repo-audit/T1-TEST-FIXTURES.md create mode 100644 Docs/feature/ssb-appearance/ADR-SET-ssb-appearance.md create mode 100644 Docs/feature/ssb-appearance/BUILD-PLAN-ssb-appearance.md create mode 100644 Docs/feature/ssb-appearance/BUILD-PLAN-track-L-combined.md create mode 100644 Docs/feature/ssb-appearance/DESIGN-BRIEF-ssb-appearance.md create mode 100644 Docs/feature/ssb-appearance/FACT-SHEET-ais-ssb-for-mike.md create mode 100644 Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md create mode 100644 Docs/feature/ssb-appearance/RECON-ssb-appearance.md create mode 100644 Docs/feature/ssb-appearance/S0a-VERIFICATION.md create mode 100644 Docs/feature/ssb-appearance/S1b-FIDELITY.md diff --git a/Docs/feature/repo-audit/S8-WEARABLE-RESOLUTION.md b/Docs/feature/repo-audit/S8-WEARABLE-RESOLUTION.md new file mode 100644 index 00000000000..477238f3623 --- /dev/null +++ b/Docs/feature/repo-audit/S8-WEARABLE-RESOLUTION.md @@ -0,0 +1,77 @@ +# S8 — what S0c did not cover: the listed-but-unresolvable wearable + +2026-09-05. A note against the appearance-integrity work, because the two defects look like one and the +distinction is what made the second one survive the first fix. + +## The two shapes of the same loss + +A wearable slot can be emptied by the region in two different ways, and they enter the code at different points. + +| | S0c (Ledger R-4 / Q-3) | S8 | +|---|---|---| +| Input | `AgentIsNowWearing` names **fewer slots** than the agent is wearing | the slot **is** named, and its item id will not resolve | +| Where | `Client_OnAvatarNowWearing` (`AvatarFactoryModule.cs:1262-1300`) | `SetAppearanceAssets` (`:940-947`) | +| Old behaviour | built a fresh `AvatarAppearance` and filled only the listed slots, so unlisted ones were dropped | logged "setting to default" and **removed** the wearable | +| Fix | merge the list into the existing wearables instead of replacing | keep the wearable that is in the slot | +| Fixed in | S0c | S8, `483c2d7a13` | + +Both end in the same place: `SaveAppearance` persists the reduced set, and `AvatarService.SetAvatar` deletes +every row for the agent before rewriting (`AvatarService.cs:93`), so the slot leaves the stored record +altogether. Both are self-reinforcing — the next login reads back the damaged record. + +## Why S0c's fix could not reach this one + +S0c changed how the *viewer's list* is applied. It never touched asset resolution, which happens later and from +a different input: whatever is in `sp.Appearance.Wearables` by the time the save timer drains. An outfit that +survived `MergeNowWearing` intact could still lose four slots a few hundred milliseconds later because the +inventory service did not recognise their item ids. + +The live case shows the gap clearly. Truly's `AgentIsNowWearing` was not the problem and her Current Outfit +folder (`71c3c184`) linked valid skin, eyes and hair throughout. What failed was `GetItem` for four ids that a +stale May-2 viewer cache from the previous grid still believed in — items that exist nowhere in +`inventoryitems`. S0c's merge was working exactly as designed and the slots went anyway. + +## Why no test caught it + +`AvatarFactoryNowWearingTests` covers S0c thoroughly, and every one of its cases uses item ids the inventory can +resolve. It therefore never enters the `else` branch at `:940`. Worse, a test that *does* wear an unresolvable +item still passes against the broken code unless the agent has an inventory root, because `SetAppearanceAssets` +skips its entire loop otherwise (`:911`) — so the naive version of the S8 test was green before the fix and had +to be rebuilt on `UserAccountHelpers.CreateUserWithInventory` to go red. + +That is the transferable lesson: a test for a resolution failure has to make resolution actually run. + +## Which presence actually ran it — and why that matters for the fix + +Corrected here, because the S8 session got it wrong twice and the second reading was no better sourced than the +first. + +**The AVFACTORY warnings carry no region tag.** `SetAppearanceAssets` logs agent, slot and item and nothing +else, so no line in that block says where it ran. The original brief placed the 18:06:46 warnings on +Transylvania's child presence; the S8 report then argued they must have been on a Transylvania presence that had +just become root, because the bake five seconds later cannot happen on a child. Both readings were inferences +about a region the log never named. + +**The timing settles it.** Truly logged in at **18:06:40**, on **Ebony**. `DelayBeforeAppearanceSave` is 5 s. +The login queues a save through the baked-texture cache check (`ScenePresence.cs:2291-2294`), it drains at +~18:06:45-46, and that is the save whose warnings are in the log. The presence is **Ebony's root** — the region +Truly actually logged into. Transylvania's child presence did not write anything. + +**So fix #1 is the cause and fix #2 is defence in depth.** Keeping an unresolvable wearable +(`SetAppearanceAssets`) is what stops this loss; the child-presence guard closes a hole that is real and +unguarded but was not the path taken here. Ordering the two that way matters if either is ever reverted: without +fix #1 the loss recurs on a root presence, where no child guard can reach it. + +## Upstream + +Both S8 defects are inherited unchanged from OpenSim-NGC develop `a68d59f232`: the removal at +`AvatarFactoryModule.cs:871-878` and the unguarded `SaveAppearance` at `:811-828`. Neither is a fork +regression, and neither has been reported upstream from here. + +## Still open + +`SetAppearanceAssets` leaves an unresolved wearable with whatever asset id it already carried. For an item from +another grid that asset id may be meaningless here, so the slot is *populated but unrenderable* rather than +*missing*. That is strictly better than the old behaviour — the record survives, and a viewer that re-reads its +inventory repairs it — but it is not the same as correct, and nothing currently reconciles such a slot on +login. diff --git a/Docs/feature/repo-audit/T1-TEST-FIXTURES.md b/Docs/feature/repo-audit/T1-TEST-FIXTURES.md new file mode 100644 index 00000000000..56f0265eb74 --- /dev/null +++ b/Docs/feature/repo-audit/T1-TEST-FIXTURES.md @@ -0,0 +1,108 @@ +# T1 — test-infrastructure defects at the deployed tree + +Tree: `D:\tranq-fix`, branch `fix/test-fixtures`, HEAD `db7c746248` (deployed). Runs are serial +(`-- xUnit.ParallelizeTestCollections=false`) so order-flaky tests do not blur the count. + +## Part 1 — `OpenSim.Region.CoreModules.Tests`: 35 failures at clean HEAD + +### Failing tests before the fix (35 of 79) + +| Class | Tests | First exception line | +|---|---|---| +| `Asset.Tests.FlotsamAssetCacheTests` | TestCacheAsset, TestClearCache, TestExpireAsset | `NullReferenceException` at `FlotsamAssetCacheTests.cs:70` (`m_cache` null) | +| `World.Serialiser.Tests.SerialiserTests` | TestSerializeXml, TestSerializeXml2, TestDeserializeXml2 | `NullReferenceException` at `SerialiserTests.cs:714` (`m_scene` null) | +| `World.Land.Tests.PrimCountModuleTests` | TestInitialCounts, TestAddOwnerObject, TestAddGroupObject, TestAddOthersObject, TestCopyOwnerObject, TestMoveOwnerObject, TestRemoveOwnerObject, TestRemoveGroupObject, TestRemoveOthersObject, TestTaint | `NullReferenceException` at `PrimCountModuleTests.cs:85` (`m_lo` null) | +| `World.Media.Moap.Tests.MoapTests` | TestSetMediaUrl, TestClearMediaUrl | `NullReferenceException` in `SceneHelpers.AddSceneObject` (`SceneHelpers.cs:625`, scene argument null) | +| `Framework.InventoryAccess.Tests.InventoryAccessModuleTests` | TestRezObject | `NullReferenceException` at `InventoryAccessModuleTests.cs:141` (`m_scene` null) | +| `Avatar.Inventory.Transfer.Tests.InventoryTransferModuleTests` | TestAcceptGivenItem, TestRejectGivenItem, TestAcceptGivenFolder, TestRejectGivenFolder | `NullReferenceException` in `UserAccountHelpers.CreateUserWithInventory` (`UserAccountHelpers.cs:154`, scene argument null) | +| `Avatar.Inventory.Archiver.Tests.InventoryArchiveSaveTests` | TestOrder, TestSaveItemToIar, TestSaveItemToIarNoAssets, TestSaveRootFolderToIar, TestSaveNonRootFolderToIar | `ArgumentNullException (buffer)` at `InventoryArchiveSaveTests.cs:67` (`m_iarStreamBytes` null) / `NullReferenceException` in `CreateUserWithInventory` | +| `Avatar.Inventory.Archiver.Tests.InventoryArchiveLoadTests` | TestLoadIarCreatorAccountPresent, TestLoadCoalesecedItem, TestLoadIarV0_1AbsentCreator | `NullReferenceException` in `CreateUserWithInventory` (`m_scene` null) | +| `Avatar.Inventory.Archiver.Tests.InventoryArchiveLoadPathTests` | TestLoadIarPathStartsWithSlash, TestLoadIarToInventoryPaths | `NullReferenceException` in `InventoryArchiveReadRequest.Execute` (`:183`, then `:246` closing a null stream) | +| `Avatar.AvatarFactory.AvatarFactoryModuleTests` | TestSetAppearance, TestSaveBakedTextures | `Assert.NotNull() Failure` at `AvatarFactoryModuleTests.cs:88` (`scene.AssetService.Get` returns null) | + +`ChatModuleTests.TestInterRegionChatDistanceEastWest` passes serially and fails only under parallel collections; +known order-flaky, not chased. + +### Root cause (33 of 35): NUnit lifecycle hooks orphaned by the xunit migration + +The xunit migration (`a115734ff3`, "Feature/xunit tests (#197)") removed the NUnit attributes but left the +methods behind: + +- `OpenSimTestCase.SetUp()` (`Tests/OpenSim.Tests.Common/OpenSimTestCase.cs:53`) is `public virtual` and was + **called by nothing**. Subclasses that had `[SetUp] public override void SetUp()` kept the override; xunit, + which has no `[SetUp]`, never invoked it. Every field those methods assign (`m_scene`, `m_cache`, `m_lo`, + `m_iarStream`, …) stayed null. Affected here: FlotsamAssetCacheTests, PrimCountModuleTests, MoapTests, + InventoryAccessModuleTests, InventoryTransferModuleTests, InventoryArchiveSaveTests, InventoryArchiveLoadTests, + InventoryArchiveLoadPathTests, InventoryArchiveTestCase (26 tests). Nine more classes across + `LindenUDP.Tests`, `OptionalModules.Tests`, `ScriptEngine.Tests`, `Permissions.Tests` and `LindenCaps.Tests` + override the same method and were silently unset the same way (`grep -rl 'override void SetUp()' Tests`). +- `InventoryArchiveTestCase.FixtureSetup()` / `TearDown()` (`InventoryArchiveTestCase.cs:72, :80`) were + `[TestFixtureSetUp]` / `[TestFixtureTearDown]`; with the attributes gone nothing built the default IAR bytes, + so `SetUp()` at `:91` threw on `new MemoryStream(null)` (5 IAR save tests plus, once the base fix landed, every IAR + test). +- `SerialiserTests.Init()` (`SerialiserTests.cs:592`) was `[SetUp]`; it became a private method with no caller (3 tests). + +Candidates eliminated: +- **Skia rework changed the in-memory asset service**: no. `SceneHelpers.StartAssetService` (`SceneHelpers.cs:214-234`) + still wires `LocalAssetServicesConnector` over the `OpenSim.Tests.Common.dll` storage provider and, when given, + `TestsAssetCache`; the cache is registered before the connector's `RegionLoaded` (`SceneHelpers.cs:171-179`). The + null objects are test-class fields, not service results. +- **A config default the fixture no longer supplies**: no. Every null is a field a lifecycle hook was supposed to + assign; `SetupScene` itself succeeds in the tests that construct the scene inline (44 passed before the fix). + +### The fix (fixture only) + +1. `Tests/OpenSim.Tests.Common/OpenSimTestCase.cs`: the base class implements `Xunit.IAsyncLifetime`; + `InitializeAsync()` calls `SetUp()`. xunit invokes `InitializeAsync` after the constructor (subclass constructor + bodies included) and before the test method, which is exactly where NUnit ran `[SetUp]`. `DisposeAsync` is a + no-op because xunit 2 also calls `Dispose`, where the existing teardown overrides live. (A first attempt called + `SetUp()` from the constructor; that runs before subclass field work in `InventoryArchiveTestCase` and was + replaced.) +2. `InventoryArchiveTestCase`: `SetUp()` calls `FixtureSetup()` (per instance; xunit builds one per test) and a + `Dispose()` override calls `TearDown()`. +3. `SerialiserTests`: a `SetUp()` override calls the orphaned `Init()`. + +No test assertion and no production code was changed. + +### Counts + +| Run (serial) | Failed | Passed | Total | +|---|---|---|---| +| clean HEAD `db7c746248` | 35 | 44 | 79 | +| after the base-class hook alone | 19 | 60 | 79 | +| after wiring `FixtureSetup`/`Init` as well | **5** | **74** | 79 | + +### Remaining 5 — separate causes, not fixed here + +| Test | What it fails on now | Cause | +|---|---|---| +| `InventoryArchiveLoadTests.TestLoadIarCreatorAccountPresent`, `TestLoadIarV0_1AbsentCreator` | `Assert.Equal` expects the creator **name** ("Lord Lucan" / "Mr Tiddles"), the loaded item carries the creator **UUID string** | assertion written against an older `CreatorId` convention; the tests were unreachable (null scene) since #197 and never ran green on xunit; the assertion, not the fixture, is wrong or the loader changed — needs its own session | +| `InventoryArchiveLoadTests.TestLoadCoalesecedItem` | `Assert.Single` sees 2 coalesced objects (`Object1Part1`, `Object2Part1`) | same: reached for the first time; expectation vs `CoalescedSceneObjects` reader to be settled separately | +| `AvatarFactoryModuleTests.TestSetAppearance`, `TestSaveBakedTextures` | `scene.AssetService.Get(bakedTextureID)` returns null after `Store` of a `Temporary + Local` asset | not a lifecycle problem (the scene is built inline); `LocalAssetServicesConnector.Store` (`LocalAssetServiceConnector.cs`, `Store`) caches a Local asset only, `Get` asks the cache first — where the round trip breaks is in the connector/`TestsAssetCache`, unrelated to the shared cause | + +## Part 2 — `Tests/OpenSim.Region.ClientStack.LindenCaps.Tests` + +Not in `Tranquillity.sln`; last touched by the dotnet 10 SDK bump (`0914c8104a`). Contents: + +| File | State | Verdict | +|---|---|---| +| `EventQueue/Tests/EventQueueTests.cs` | 5 `[Fact]` tests (`TestAddForClient`, `TestRemoveForClient`, `TestEnqueueMessage`, `TestEnqueueMessageNoUser`, `TestEnqueueMessageToNpc`) exercising `EventQueueGetModule` through a real `BaseHttpServer` and `SceneHelpers`. Six assertions were mangled in the Skia rework `b6ee976cb7` (#130): `Assert.That(x, Is.EqualTo(1))` → `Assert.True(x));` etc. (`:89, :104, :130, :149, :167, :191`), which is why it does not compile. Setup is a `SetUp()` override — fixed by Part 1. | **keep**: the only tests of the event queue in the tree | +| `Tests/WebFetchInvDescModuleTests.cs` | the whole class is inside a `/* … */` block comment (`:55-`); it targets a `BaseHttpServer` constructor that no longer exists and uses `[TestFixtureSetUp]` | dead; left as is (already compiled out), noted for a later FetchInventoryDescendents2 harness | +| `.csproj` | references NUnit 4 while the sources use xunit `[Fact]`/`Assert`; pins `Microsoft.NET.Test.Sdk 17.14.1` and `Logging.Console 9.0.7` below `Tests.Common` (NU1605 downgrade errors) | rewritten to the `Tests.Common` package set (xunit 2.9.3, runner 3.1.5, Test.Sdk 18.8.1, Logging.Console 10.0.10) | + +Restored, in this order: +1. `.csproj` rewritten to the `Tests.Common` package set (NUnit dropped; `` gone). +2. The six mangled assertions rewritten to their pre-#130 meaning in xunit form (`Assert.Equal(1, keys.Count)`, + `Assert.Equal(0, ...)`, `Assert.Equal((int)HttpStatusCode.OK/NotFound, (int)response["int_response_code"])`, + `Assert.True(foundUpdate, "Did not find {0} in response")`). +3. `using Xunit;`, `using Nini.Config;`, `using OpenSim.Region.ClientStack.LindenCaps;` added (the class lives in + `OpenSim.Region.ClientStack.Linden.Tests`; the module moved namespaces). +4. Setup ported to the current `MainServer` API: `MainServer` is instance-based (`IMainServer.Instance`, read-only + `DefaultServer`), so `MainServer.RemoveHttpServer(port)` / `AddHttpServer(server)` / `Instance = server` became + `MainServer.Instance.RemoveHttpServer(port)` / `MainServer.Instance.AddHttpServer(m_server)` — the base test class + removes the previous default server, so the new one becomes `DefaultServer` — and + `MainServer.Instance.GetPollServiceHandlerKeys()` (no longer on `IMainServer`) became + `m_server.GetPollServiceHandlerKeys()` on the `BaseHttpServer` the test owns. +5. Project added to `Tranquillity.sln` under `Tests`. + +Result: 5 passed, 0 failed (serial). No test was skipped. diff --git a/Docs/feature/ssb-appearance/ADR-SET-ssb-appearance.md b/Docs/feature/ssb-appearance/ADR-SET-ssb-appearance.md new file mode 100644 index 00000000000..230e44a5cb9 --- /dev/null +++ b/Docs/feature/ssb-appearance/ADR-SET-ssb-appearance.md @@ -0,0 +1,120 @@ +# ADR Set — Server-Side Baking + +Status legend: **Proposed** = needs John's ruling · **Accepted** = ruled · **Carried** = ruled earlier (recon 2026-09-02), restated for the record. + +--- + +## ADR-001 — Viewer contract is the stock LL viewer; add-only + +**Status:** Carried (D-5 of the parity audit; viewer-compatibility policy 2026-08-31) +**Decision:** The wire behaviour in RECON addendum §3 (V1–V7) is the spec. No UDP appearance handler (`AgentSetAppearance`, `UploadBakedTexture`, `AgentCachedTexture`, `AvatarNowWearing`) is removed or altered in behaviour. All new behaviour is gated by `[Appearance] ServerSideBaking` per region. +**Consequences:** Firestorm client-bake keeps working on flag-off regions forever. Two code paths coexist in `SendAppearance` (with/without `AppearanceData`), selected per avatar by whether the sim baked it. + +--- + +## ADR-002 — Bake compute in the region; appearance service and reaper on Robust + +**Status:** Carried (D-2 "Robust-route") +**Decision:** Composition runs in-process in the region module that owns the ScenePresence (it has the params, the TE, and the `AvatarAppearance` sender). The **read** path viewers use for other avatars' bakes (`agent_appearance_service` → `texture///`) is a Robust HTTP handler that resolves the channel to the stored asset UUID via the avatar service and streams the asset. The expiry reaper is a Robust-side timer. +**Alternatives rejected:** (a) Bake on Robust — needs a second copy of appearance state and a new region→Robust bake RPC; more moving parts for no fidelity gain. (b) Serve bakes from the region's HTTP server — regions come and go; the URL in the login response must be stable across teleports. +**Consequences:** Standalone mode: the same handler registers on the standalone's HTTP server (Robust connectors are hosted in-process there already). `IBakeBackend` seam retained so compute *could* move out-of-process later (LL utility, GPU box) without touching the module. + +--- + +## ADR-003 — Shared compositor library, extracted from the web-viewer gateway + +**Status:** Proposed +**Decision (recommended):** Create `OpenSimNGC.Appearance.Baking` as a project in the Tranquillity tree under `Source/` (not `Addons/`, because Robust never loads it but the library is core infrastructure, not an optional module), targeting `net10.0`, dependencies SkiaSharp (already in tree since #130) + the tree's existing J2K encoder. Ship it to NuGet as `OpenSimNGC.Appearance.Baking` alongside the other NGC packages so the web-viewer gateway consumes it by package reference, not path. +**Alternative A:** Keep the compositor in the gateway and copy it into Tranquillity. Rejected: two copies, two harnesses, drift within a week. +**Alternative B:** Publish from a third repo. Rejected: one more repo for one library; the gateway already depends on NGC packages. +**Why proposed, not accepted:** publishing a new NGC package is Mike's call as ecosystem maintainer; John should raise it with him before S0b. Until then the gateway can use a local `ProjectReference` to `/d/tranquillity-develop/Source/OpenSimNGC.Appearance.Baking`. +**Consequences:** The library carries the golden-fixture test project; both consumers get the tests. The gateway's `gateway/src/Gateway/Baking/` is deleted, not deprecated. + +--- + +## ADR-004 — Bake persistence: assets with a bake marker, index in the avatar service, supersede-immediately + TTL reaper + +**Status:** Accepted; index, hash skip and supersede **implemented in S2** (the TTL reaper is still Proposed) +**Decision:** Bakes are stored through `IAssetService` as texture assets. Marker (recommended): `AssetBase.Flags |= AssetFlags.Collectable` is *not* used (it means "temp" in stock code paths and gets purged wrongly); instead the asset **name** is `bake::` and `Description` carries the input hash, and the authoritative index is the avatar-service key set (`Bake:`, `BakeHash:`, `BakeCOFVersion`, `BakeSize`, `BakeUpdated`). Supersede = delete the previous asset for that channel synchronously after the new one is confirmed stored. TTL reaper walks avatar-service records whose `BakeUpdated` is older than `BakeTTLDays` and whose presence record shows no login since, deletes assets, clears keys. +**Alternatives rejected:** (a) New `bakes` table — schema change in three DB backends for something the key/value Avatars table already expresses. (b) Never expire — violates D-6. (c) `AssetFlags` marker — see above; also not indexed. +**Consequences:** No migration. Grid owners on plain OpenSim never see any of this. A grid with the reaper off (default standalone) grows only until supersede, i.e. ≤11 bakes per avatar. + +**As built (S2).** The index is read and written through the three calls `IAvatarService` already has — `GetAvatar` +(every key), `SetItems` (one batched write per bake) and `RemoveItems` — which exist on both the local service and the +Robust connector, so no service change and no schema change were needed; the longest key, `BakeHash:LeftArm`, is 16 +characters against the table's `Name varchar(32)`. The skip is per channel and runs *before* any texture is fetched: the +hash is computed from the wearables alone, so a reused channel costs no asset fetch, no J2K decode, no composite, no +encode and no store. A channel is reused only when the stored hash matches **and** the stored asset still resolves — a +hash whose asset has vanished is never trusted. `BakeSize` is compared as well as being folded into the hash. Supersede +deletes the previous asset only after the new one is confirmed stored and the face has moved to it, and never an asset +any baked face still points at. One thing the design did not anticipate: any appearance save wipes the whole index, +because `AvatarService.SetAvatar` deletes every row for the principal first — Ledger Q-14. + +--- + +## ADR-005 — Fidelity policy on the sim: best-effort with a structured report; refusal only for corrupt input + +**Status:** Proposed (D-3) +**Decision (recommended):** Unlike the gateway (which refuses anything it cannot reproduce faithfully because its bake would replace a Firestorm bake), the sim on a bit-0 region **is** the only baker for LL viewers, so refusing means a permanent cloud. Policy: bake what is supported; skip unsupported layers; emit a fidelity report (INFO log + `[Appearance] FidelityReportPath` optional JSONL); the cap response is `success:true`. Refuse (`success:false`, `error`) only when inputs are unparseable or a texture fetch fails after retry — and in that case do **not** overwrite an existing good bake. +**Alternative:** Mirror the gateway's strict gate. Rejected for LL viewers; but see consequences. +**Consequences:** Firestorm users on a bit-0 region get the sim's best-effort bake instead of their own. The harness numbers (S0) must show the compositor at or above Firestorm's output on the stock-Library reference *before* any region flips the flag. If John rules strict, the flag stays off on Legion Grid until unsupported types are zero. + +--- + +## ADR-006 — COF version source is the inventory folder `Version`; no AIS dependency + +**Status:** Proposed +**Decision (recommended):** The sim reads the COF folder's `Version` from the inventory service and treats it as `cof_version`. AIS v3, when built, updates the same field, so nothing changes later. Anti-livelock rule per Design Brief §4.3. +**Alternative:** Block SSB on AIS (BP-v2 order). Rejected because the LL viewer's "log in as yourself" tier needs only the login-time bake, and the web viewer needs SSB now. +**Consequences:** S0 must verify the UDP link-create/delete path bumps `Version`. If it does not, that fix is a prerequisite slice, not a reason to wait for AIS. + +--- + +## ADR-007 — `avatar_lad.xml` ships with the library + +**Status:** Proposed (Q-2) +**Decision (recommended):** Vendor `avatar_lad.xml` into the library as an embedded resource, with a `THIRD-PARTY-NOTICES` entry naming its origin (Linden Lab viewer, LGPL 2.1 with the viewer's linking exception). The gateway currently reads it out of the LibreMetaverse package at runtime — that coupling ends with the extraction. **Extended (S0b/S0d):** the same rule covers the 56 parameter-mask / base-image TGAs the `layer_set` definitions name (`Data/character/*.tga`); the compositor cannot draw a clothing layer without them. Provenance rule for every vendored file: origin = the LL viewer tree, version recorded (`VIEWER_VERSION.txt` 26.1.1), SHA-256 listed in `THIRD-PARTY-NOTICES.md`, byte-identical to that tree (verified in S0d for all 56 masks and `avatar_lad.xml`). A file that is not in the viewer tree is not vendored (`head_wrinkles_highlights_alpha.tga`, referenced only by a bump-pass layer that is never rendered). +**Alternative:** Reference it from LibreMetaverse in both consumers. Rejected: the sim does not otherwise depend on LibreMetaverse, and a bake compositor must not change behaviour because a client library updated. +**Consequences:** One provenance line in Ledger Q-2 closes; the [[avatar-character-system]] Q-3 provenance question (system body mesh) is unaffected and stays open there. + +--- + +## ADR-008 — Bake size 1024, parameterised + +**Status:** Accepted, revised 2026-09-03 by measurement (supersedes the 512 default carried from D-7) +**Decision:** Sim default **1024** px per channel; `[Appearance] BakeSize` accepts 512, 1024 or 2048. Hash includes size so a config change invalidates stored bakes on next login rather than serving mixed sizes. + +**Why 1024 and not the original 512.** S1b ran both reference sets at all three sizes against the LL +compositor references (`S1b-FIDELITY.md` §6). 1024 is the knee: every channel improves from 512 to 1024, and on +the richer of the two outfits the improvement is large — Aleric's lower channel goes from mean abs RGB **2.29** +at 512 to **0.89** at 1024, the single worst number in the matrix and the only one that would have failed a +tighter threshold. Going on to 2048 buys nothing: five of the nine channel rows get *worse*, none improves +materially, and the encoded bytes rise ~1.8×. 512 costs about 0.69× of 1024's bytes, which does not pay for a +visible loss on a busy outfit. + +The references themselves are 2048 for head, upper, lower and hair, and 512 for eyes, on both avatars — so +1024 is also below the reference resolution everywhere except eyes, and the diff numbers already account for +that by resampling both images to the compared size. + +**Consequences:** No config change is needed anywhere: S1 already shipped `BakeSize = 1024` in +`OpenSimDefaults.ini` and the live region server has been running 1024 since the 2026-09-03 deploy. This ADR +now records what is actually running. 512 remains available for operators who want the smaller assets and +accept the loss; 2048 remains available but is not recommended. + +--- + +## ADR-009 — Gateway is a consumer on SSB regions + +**Status:** Accepted, **implemented in S6(b)** (web-viewer `7a31412b54`) +**Decision:** On `RegionHandshake` with bit 0 set, the gateway session switches to `server` appearance mode: never bakes, accepts `AppearanceData`-bearing `AvatarAppearance` for self, fetches bakes via the existing asset route. On bit-0-clear regions the S11/S12 path stays. Detected per region, re-evaluated on every teleport. +**Consequences:** Web-viewer G6 ("standalone, no grid reliance") holds — the gateway degrades gracefully to its own baker off-NGC. On Legion Grid the corruption hazard that caused e881646 disappears entirely for SSB regions, because the gateway sends nothing. + +**As built (S6(b)).** Five things the implementation settled that the decision above left open: + +1. **The mode is a type, not a flag.** `ServerAppearance` implements `IAppearanceMode` but **not** `IBakeSteps`, and `AppearanceBaker.RunAsync` takes an `IBakeSteps`. There is therefore no send step to reach in server mode — the guarantee is the absence of the code, not a branch inside it. The S6(a) refactor that moved the whole S11/S12 pipeline into `ClientBakeAppearance` exists for this reason. +2. **Detection is `Simulator.Protocols` at every `SimConnected`**, login and teleport landing alike, which is the viewer's own test — `llviewerregion.cpp:3083` computes `mCentralBakeVersion = region_protocols & 1` from the handshake. Every choice is logged with the region name and the protocols word. +3. **Handover aborts a bake in flight before it can send.** `AppearanceBaker` now gates the send on the cancellation token explicitly, because a CPU-bound step need not observe it. A send already made stands — it is on the wire, and the simulator overwrites it in its own time, which is exactly what the S6 pre-verify observed. +4. **`AppearanceVersion == 0` is refused in server mode.** LibreMetaverse leaves both versions at 0 when the packet carried no `AppearanceData` block, so 0 means the simulator declared no server bake. The viewer's own resolver is more forgiving (`llvoavatar.cpp:9663-9690` substitutes 1 when neither the field nor the 11000 param is set); the stricter rule is the gateway's, because in server mode it has no bake of its own to fall back on. +5. **Two routes to a bake, asset first.** The existing asset route is tried first; on an empty answer the gateway retries the grid's appearance service at the URL a viewer would build — `texture///`, the channel as its **name** (`llvoavatar.cpp:5912` from `mDefaultImageName`), the service from the login response's `agent_appearance_service` (`llstartup.cpp:4047-4051`). Which route served is logged. + +**Deferred, deliberately:** the gateway does not POST `UpdateAvatarAppearance`. That cap is how a viewer declares a COF change *it made*, and the gateway has no outfit-change UI, so it has nothing to declare. When one is added the POST belongs with it, and §4.3's handshake already answers it. diff --git a/Docs/feature/ssb-appearance/BUILD-PLAN-ssb-appearance.md b/Docs/feature/ssb-appearance/BUILD-PLAN-ssb-appearance.md new file mode 100644 index 00000000000..5239eb934b4 --- /dev/null +++ b/Docs/feature/ssb-appearance/BUILD-PLAN-ssb-appearance.md @@ -0,0 +1,104 @@ +# Build Plan — Server-Side Baking (L-2) + +**Status:** IN PROGRESS — S0a–S1 done and deployed; S2 next. Still gated on Ledger D-1/D-3 + ADR-003 for the later sessions. **Date:** 2026-09-03 (S1-close) +**Estimating convention:** wall-clock Claude Code minutes per session, anchored to measured web-viewer sessions (S4 21 min, S10 20 min, S6 28 min, S11 39 min, S9 55 min). A session past ~2× its estimate is stuck: stop it, report, re-scope. One feature per session. +**Branch:** `feature/ssb-appearance` off `develop` HEAD (`cb141dd61d` + maptile fix). Commit locally at each session's DoD; push is John's call. +**Repos:** T = `/d/tranquillity-develop` (worktree for this branch — see §0), W = `D:\web-viewer`. + +## 0. Setup (John, ~3 min, no CC) + +```bash +cd /d/tranquillity-develop +git fetch --all +git worktree add /d/tranq-ssb -b feature/ssb-appearance develop +cd /d/tranq-ssb +mkdir -p Docs/feature/ssb-appearance +cp /d/_TO_REVIEW/ssb-appearance/*.md Docs/feature/ssb-appearance/ +# then copy the five docs from this delivery into the same folder +git add Docs/feature/ssb-appearance +git commit -m "docs(ssb): recon + addendum, design brief, ADR set, build plan, ledger" +``` + +**Needs your attention:** the `cp` from `_TO_REVIEW` assumes the CC recon's filenames are `.md` at the top level of that folder — check `ls /d/_TO_REVIEW/ssb-appearance/` first. + +## 1. Session table + +| S | Repo | Feature | DoD (harness green, not "looks right") | Est. min | Verify loop (yours) | +|---|---|---|---|---|---| +| **S0a** ✅ | T | **Verification grep pass + library project skeleton.** Resolve every `[UNVERIFIED]` in the RECON addendum §2/§6 with file:line at HEAD; create `Source/OpenSimNGC.Appearance.Baking` (net10.0, SkiaSharp, tree's J2K encoder, embedded `avatar_lad.xml` per ADR-007) with an empty public API + test project. | Report table with file:line for all 6 items; solution builds; `dotnet test` runs 0 tests green. **Done:** `29105ccc44`, `7dbc092d2e`. | 25 → **?** | none | +| **S0b** ✅ | W→T | **Extract compositor.** Move `gateway/src/Gateway/Baking/` into the library; port its existing unit tests; golden-fixture harness: given a wearables+params fixture and Firestorm's bake assets for Truly's stock-Library outfit, pixel-diff per channel with a threshold (report SSIM/abs-diff per channel). Gateway switches to `ProjectReference` (NuGet later, ADR-003) and its `Baking/` dir is deleted. | Library tests green; gateway builds and its 9 S11 tests + S12 tests still green against the library; diff numbers printed for 6 legacy channels. **Done:** `303d2b39c1`, `8a245aa286`, `cbf3284e06`, `e3b969c9ab` (T); `be67e2d` (W). | 40 → **34** | **You:** produce the golden fixtures — log Truly in via Firestorm on the stock outfit, note the 6 bake UUIDs from Appearance debug (or from `AvatarAppearance` via the harness), pull the assets. This is the step that can't be automated and blocks S0b's last third. | +| **S1** ✅ | T | **Bake orchestrator + console trigger.** `AppearanceBakeModule` (Addons or OptionalModules per S0a finding): COF → wearables → textures → 11-channel composite via library → store assets → update ScenePresence TE → `SendAppearance` to all. Trigger: console `appearance bake `. No cap, no flag, no persistence keys yet. | Console command on Ebony bakes Truly; Firestorm observer (you) sees the sim's bake replace hers; harness diff of the stored assets vs goldens ≤ threshold. **Done:** `bbc065bc5f`, `99118ea1ab`; deployed to the live region server 2026-09-03 19:14 and verified in-world 19:46 (first server-composited avatar on the grid). **DoD closed in S2 Part 0** (commit `test(ssb): close S1 DoD — golden diff at the shipped bake size`): the caveat was that the harness-diff clause had been met only at 512 while the bakes stored on the live sim were made at 1024. The gate now bakes at the shipped `[Appearance] BakeSize` (1024, ADR-008) — `truly-stock/manifest.json` `bakeSize` raised 512 → 1024, `aleric-max` was already 1024 — and both sets pass every threshold against the 2048/512 references. Worst numbers at 1024: mean |dRGB| 1.25 (truly upper) against 4.0, pctRGB>8 0.75% against 5%, mean |dA| 1.00 against 2.0, mean |dM| 1.00 against 4.0, pctM>8 1.59% (aleric upper) against 5%. Every RGB mean is *better* at 1024 than it was at 512. | 35 → **15** | Firestorm side-by-side, 1 loop. | +| **S2** (part) | T | **Persistence + supersede + reaper** (ADR-004). Avatar-service keys, per-channel input hash, skip-compute on match, synchronous supersede-delete, Robust reaper with `BakeTTLDays`, off by default. | Unit tests: hash stability, supersede deletes old UUID, reaper deletes only past-TTL-and-not-logged-in; console bake twice → second run logs "reused 11/11". **Done for the index, the skip and supersede:** keys read/written through the existing `GetAvatar`/`SetItems`/`RemoveItems` (no service change, no schema change); reuse is per channel and decided before any texture is fetched; a stored hash whose asset has vanished is not trusted; `BakeSize` invalidates; supersede deletes only after the new asset is stored and never something a face points at. The one-line summary now ends `reused N/M`. **Still open in S2:** the Robust TTL reaper with `BakeTTLDays`. New finding: Ledger Q-14 (any appearance save wipes the index). Cost instrumentation and Q-10's answer landed here too. | 30 | none | +| **S3** ✅ | T | **Wire: flag, bit 0, `AppearanceData`, cap.** `[Appearance] ServerSideBaking` per region; `RegionProtocols |= 1` when set; `SendAppearance` emits `AppearanceData{1, CofVersion}` for sim-baked avatars only; `UpdateAvatarAppearance` cap with the §4.3 handshake + anti-livelock; login-time bake trigger on `MakeRootAgent`. Flag stays **false** in every shipped ini. | Unit tests for the handshake (equal / less / greater / livelock cap); with the flag on for a **test region only**, LL viewer logs in and is textured to itself; Firestorm on the same region POSTs and is textured. **Done:** `1e78b9a706` (Part 0, Q-14), `7554bf9b51` (Part 1); deployed to the live region 2026-09-04 at `1.1.216-alpha+7554bf9b51`, flag left false. Handshake tests cover all four branches plus a throwing re-read, window expiry, per-agent counters and the clear on success; the flag resolves per region in five configurations; the ADR-001 gate asserts a flag-off region's `AvatarAppearance` body is byte-identical to the pre-S3 form at three sizes. `cof_version` proven identical to AIS's folder version. **The DoD's two live clauses are not met and cannot be met by this session** — they need the flag on and a viewer in-world, which is John's loop. | 40 → **?** | **You:** stock LL viewer + Firestorm on the test region, 1 loop each. First moment the LL viewer isn't a cloud. **Also settles Q-6**, which gates S5's shape. | +| **S4** ✅ | T | **Appearance service on Robust** (ADR-002). `agent_appearance_service` in the login response; `GET texture///` resolving via avatar-service keys and streaming the asset; standalone registration. | curl the URL for Truly's `head` returns the J2K bytes; LL viewer sees **other** avatars textured on the test region. **Done:** `59b12538b9` (handler), `51194ac754` (login response); deployed to both roots 2026-09-05 at `1.1.219-alpha+51194ac754`, with the connector and the URL left unconfigured. Channel token established from `llvoavatar.cpp:5912` (a name — `head`, `upper`, `lower`, `eyes`, `skirt`, `hair`, `leftarm`, `leftleg`, `aux1..3`), not assumed. 404 on every miss including a UUID that disagrees with the index. **The DoD's two clauses are John's loop and cannot be met here** — both need the service configured and a viewer in-world. | 30 → **?** | LL viewer observing Firestorm-and-sim-baked Legion, 1 loop. **Prerequisite for any flag flip**: S3's flip without S4 produced a correct cloud. | +| **S5** ✅ | T | **Change triggers + BoM aux channels.** Rebake on `AvatarNowWearing` (Firestorm on a bit-0 region) and on cap POST with a newer COF version; the 5 BoM aux channels produced when universal wearables are present. | Firestorm on the test region changes a shirt → new bake within one POST; fixture with a universal wearable yields 11 stored channels. **Done:** `d7ac58d187` (trigger), `9417a09402` (aux channels). Trigger is the save-completion event, not the arrival of the change (Q-16); both signal routes converge on it after the cap was changed to queue a save rather than bake on arrival; 2 s debounce sized against the 5 s save delay (the signal spread is unmeasured — Q-6). Aux channels exercised end to end with a synthetic Universal — composited, stored, faces 40-44, served by the Robust route. **The DoD's live clause is John's loop**; the aux clause is met by fixture, not by real content, and the fidelity gap is recorded rather than closed. | 30 → **?** | Firestorm outfit change, 1 loop. Watch for `reason=CofChanged` and one bake per change. | +| S6 | W | **Gateway SSB-aware mode** (ADR-009). Detect bit 0 per region; `server` appearance mode; accept `AppearanceData` for self; no bake path reachable on bit-0 regions (structural, like the S11 invariant). | Unit test proving the bake step is unreachable when bit 0 is set; live: Truly logs in via the web viewer on the test region and is textured with **zero** gateway bakes logged; on Transylvania (flag off) the S12 path still runs. | 25 | Web-viewer login on both regions, 1 loop. | +| S7 | T+W | **Soak + fidelity sign-off.** Harness against all three test avatars' outfits; 30-minute soak with LL viewer + Firestorm + web viewer on the test region; region restart with flag on → no rebake (persistence); flag off → Firestorm reverts, LL viewer clouds, nothing deleted. | All harness diffs ≤ threshold; no `AppearanceData` regressions on the flag-off region; report lists every unsupported layer seen. | 30 | Your call on flipping Ebony/Transylvania/Elm. | + +**Total:** ~4.75 h CC across 9 sessions; 5 short verify loops of yours. Comparable to two web-viewer working days at the measured pace. + +## 1a. Unplanned slices added mid-programme + +Five sessions below were **not in the original table** — they were cut out of S0b/S1 when the work turned out to be +a separate concern, and one (T-1) was pure repo hygiene that S1 tripped over. They are listed here so the estimate +column above stays honest about what the programme actually cost. + +| S | Feature | Commit(s) | Est. → actual (min) | Why it was added | +|---|---|---|---|---| +| **S0c** ✅ | `Client_OnAvatarNowWearing` merges instead of wiping unlisted wearable slots; 4 xunit tests | `a5e88d72f1` (now `11a2456833` on the deployed branch) | 20 → **9** | Ledger Q-3 / R-4 found the wipe bug present in S0a; it is a hard gate before any production flag flip, so it could not wait for S3. | +| **S0d** ✅ | 5th J2C component is the **morph mask**, not a bump pass; compositor + encoder + decoder | `30c82d472b` | 30 → **14** | Q-8 opened by S0b's golden diff: viewer bakes carry a component the server did not produce. Parity gap, had to close before S3. | +| **S0e** ✅ | Plain vs template layer semantics (`isUserSettable`); morph gather corrected to match the colour pass; doc renamed `MORPH-MASK-PASS.md` | `916dc35d00`, `22b3695389` (T); `0a6acff` (W) | 25 → **8** | S0d shipped on a wrong premise about per-instance layers; caught reading `lltexlayer.cpp` for the packing-order citation. | +| **A0** ✅ | AIS v3 spec + `AISv3Module` skeleton (separate worktree `D:\tranq-ais`) | 2 commits on `feature/ais-v3` | 30 → **21** | Track L's other half; sequenced after SSB per D-1 but started early because it is independent. | +| **T-1** ✅ | NUnit lifecycle hooks orphaned by the xunit migration — `CoreModules.Tests` 35 → 5 failing; `LindenCaps.Tests` restored to the solution | `c1fc7fff3e`, `d43f8cb362` | 30 → **18** | S0c reported 35 pre-existing `CoreModules.Tests` failures and flagged them as needing a separate owner; S1's test work needed a trustworthy baseline. | + +## 1b. Estimate vs actual + +| S | Est. | Actual | Note | +|---|---|---|---| +| S0a | 25 | ? | Not recorded at the time. | +| S0b | 40 | 34 | The one session that ran near estimate; the golden harness carried most of it. | +| S0c | 20 | 9 | Unplanned. | +| S0d | 30 | 14 | Unplanned. | +| S0e | 25 | 8 | Unplanned. | +| A0 | 30 | 21 | Unplanned (AIS worktree). | +| T-1 | 30 | 18 | Unplanned (repo hygiene). | +| S1 | 35 | 15 | Plus deploy and in-world verify. | + +Across the seven sessions with a recorded actual, **estimates are running roughly 2:1 over actuals** (210 est. → 119 actual). +Two readings, and they are not exclusive: the estimating convention was anchored to web-viewer sessions that involved +more unknown-shape exploration than this programme has needed, and four of the seven were narrow slices carved out +of a session already scoped and understood. The ratio is **not** a reason to re-estimate S2–S7 downward: those +sessions carry the wire protocol, the cap handshake and the live flag flip, which is where the web-viewer sessions +overran too. Treat the "past ~2× its estimate means stuck" rule as unchanged. + + +## 2. Order and gates + +``` +S0a ──► S0b ──► S1 ──► S2 ──► S3 ──► S4 ──► S5 ──► S7 + ▲ │ + goldens (you) ┘ └──► S6 (any time after S3) +``` + +Gates: +- **Before S0a:** D-1, D-3, ADR-003 ruled (Ledger). +- **Before S1:** golden fixtures exist (your step in S0b). If they lag, S1 can proceed and S0b's diff numbers land in S1's DoD instead. +- **Before S3:** S0b diff ≤ threshold on the stock outfit. This is the rule that keeps a worse-than-Firestorm bake from ever reaching a bit-0 region. +- **Before flipping any production region (after S7):** the wipe-loop check from S0a is resolved on Tranquillity. + +## 3. Each session prompt carries + +Per the standing prompt structure: the S0a grep results and file:line anchors; the library's public API; what *not* to read (no LibreMetaverse Baker, no Halcyon wire code, no `appearance-utility-bin`); test avatars Truly/Aleric only, never Legion; reporting contract (done / VERIFY-resolved with file:line / decisions needed). Prompts are written here per session, in a code block, when you say go. + +## 4. Deploy notes + +**Branch state as of 2026-09-03 (S1-close).** The deployed branch `fix/maptile-legacy-renderer` is at `11a2456833` +and now carries, besides the maptile fix itself (`db7c746248`), the **S0c wearable-wipe fix** (`11a2456833` — the +rebased form of `a5e88d72f1`) and the **T-1 fixture repair** (`c1fc7fff3e`, `d43f8cb362`). Both reached the live +region server with the S1 deploy. Both feature worktrees — `D:\tranq-ssb` (`feature/ssb-appearance`) and +`D:\tranq-ais` (`feature/ais-v3`) — are rebased onto `11a2456833`, so neither carries a stale copy of the +wipe fix or of the test fixtures. Nothing is pushed. + +- Test region = one region only, flag on in its own ini section. Recommended: Transylvania (currently loads 0 objects anyway — separate issue — so nothing to disturb). +- Both servers down before deploy (your practice). Publish path is `bin\Release\net10.0\win-x64\publish\` per project (BUILDING.md is wrong on this; noted in [[repo-audit]]). +- Robust must be redeployed at S4 (appearance service) and S2 if the reaper is enabled; region-only for the rest. diff --git a/Docs/feature/ssb-appearance/BUILD-PLAN-track-L-combined.md b/Docs/feature/ssb-appearance/BUILD-PLAN-track-L-combined.md new file mode 100644 index 00000000000..da8c4fe775c --- /dev/null +++ b/Docs/feature/ssb-appearance/BUILD-PLAN-track-L-combined.md @@ -0,0 +1,61 @@ +# Build Plan — Track L combined: AIS v3 (L-1) + SSB (L-2) together + +**Status:** DRAFT, prompted by Mike's 2026-09-03 feedback. **Supersedes** nothing yet: `BUILD-PLAN-ssb-appearance.md` stays the SSB lane's detail; this document adds the AIS lane and the interleave. +**Estimating convention:** wall-clock Claude Code minutes per session, anchored to measured sessions (20–55 min). A session past ~2× its estimate is stuck: stop, report, re-scope. One feature per session. +**Branches:** `feature/ssb-appearance` (worktree `/d/tranq-ssb`) and `feature/ais-v3` (worktree `/d/tranq-ais`), both off `develop` HEAD. They touch disjoint files except the caps registration switch and the inventory folder-version path (see §3). Merge order: AIS first (it owns folder versions), then SSB. + +## 1. Why together + +- The LL viewer needs **both** to be a usable viewer: SSB makes the avatar visible, AIS makes it changeable. Either alone ships a half-viewer. +- They share one invariant — the COF folder `Version` — and SSB's change-trigger session (S5) can only be tested properly with the LL viewer once AIS's SlamFolder exists. +- Verify loops are the scarce resource (yours), not CC minutes. A joint soak on one test region with LL viewer + Firestorm + web viewer replaces two soaks. + +## 2. AIS lane + +Spec source: RECON-02 §3 (routes and `else`-branch table from `llaisapi.cpp` / `llviewerinventory.cpp` at `62033f2`). The prompt for A0 carries the extracted route/verb/envelope table so CC never opens the viewer tree. + +| A | Feature | DoD | Est. min | +|---|---|---|---| +| A0 | **Verification + harness skeleton.** Grep pass at HEAD: `BunchOfCaps` switch and `validCaps` path, `XInventoryService` folder-version bump sites, `CreateInventoryCategory` cap, COF folder type resolution. Create `OpenSim.Region.ClientStack.Linden.Caps/AIS/` (or `Addons/`, per finding) module skeleton with `[AIS] Enabled = false` gate, plus the acceptance harness: HTTP client + LLSD envelope fixtures (`_embedded{categories,items,links}`, `_links`, `_updated_items`, `_created_items`, `_removed_items`, `_updated_categories`, `_category_items_removed`) as golden files. | Report with file:line; harness runs 0 tests green; **decision A-D1** answered (region-side proxy vs Robust-hosted, §4). | 30 | +| A1 | **Fetch surface.** `GET /item/`, `GET /category/`, `/children?depth=n`, `/children?depth=*&children=…` (subset), `/categories`, `/links`, `/category/current/links` (COF alias), `/orphans`. Links are a separate collection, never items. | Harness green on all fetch routes against a fake `IInventoryService` and against a real region for Truly. | 45 | +| A2 | **Item/category mutations.** `PATCH /item`, `PATCH /category` (name, desc, thumbnail, sale info), `DELETE /item`, `DELETE /category` (folder + descendents), `tid` echo, per-operation version-bump rule (parent bump on item ops, self+parent on moves), delta envelopes. | Harness asserts exact delta sets and version numbers per op. | 40 | +| A3 | **SlamFolder + create.** `PUT /category//links?tid=` atomic replace-all-links under the folder lock (all-or-nothing, proven by a fault-injection test), `POST /category//children` creating items/categories/links. | Fault-injection test: failure mid-slam leaves the COF unchanged; LL viewer on the test region changes outfit and it persists across relogin. | 40 | +| A4 | **Purge, library copy, simulate.** `DELETE /category//children` (Empty Trash / Lost and Found), `COPY /category/?tid=` (CopyLibraryCategory), `simulate` dry-run on mutations, HTTP status codes the viewer branches on. | Harness green; Empty Trash works in the LL viewer. | 35 | +| A5 | **Advertise.** `InventoryAPIv3` in the caps seed **only** when `[AIS] Enabled` — through `validCaps`, not just the flag switch (RECON-01 rule). Firestorm-on-cap behaviour check (RECON-02 UNVERIFIED). | Cap absent by default; present on the test region; Firestorm on that region runs inventory through AIS without regressions on a scripted checklist. | 25 | + +AIS lane total: **~3.6 h CC, 6 sessions.** Verify loops of yours: A3 (LL viewer outfit change), A5 (Firestorm checklist). + +## 3. Interleave + +``` +week-view (CC sessions, left→right; ≈ your verify loop) + +AIS : A0 ──► A1 ──► A2 ──► A3≈ ──► A4 ──► A5≈ ─────────────┐ + ├──► J1 joint soak ≈ +SSB : S0a ──► S0b≈ ──► S1≈ ──► S2 ──► S3≈ ──► S4≈ ──► S5≈ ─┘ + ▲ + A3 must land before S5 ──────┘ S6 (web viewer) any time after S3 +``` + +Rules: +- **Alternate, don't overlap.** Two worktrees, but one CC session at a time, so a bug is attributable to one session. Order that respects the dependencies: `S0a, A0, S0b, A1, S1, A2, S2, A3, S3, A4, S4, A5, S5, S6, J1`. +- Both lanes gated on their region flags; both default off in every shipped ini. +- **J1 — joint soak (T+W, ~35 min):** replaces SSB S7 and an AIS soak. Test region with both flags on: LL viewer logs in textured, changes outfit → SlamFolder → `UpdateAvatarAppearance` → rebake → `AvatarAppearance` with new `CofVersion`; Firestorm on the same region does the same; web viewer consumes. Flags off → all three revert cleanly. Harness diffs ≤ threshold on all three test avatars' outfits. + +## 4. Decisions this plan adds + +| ID | Decision | Recommendation | +|---|---|---| +| A-D1 | AIS hosting: region-side caps module translating to `IInventoryService` (Phase 1) vs Robust-hosted service with a per-agent tokenized URL (Mike's "inventory out of the simulator") | **Phase 1 region-side, behind an interface so Phase 2 can lift the same handler onto a Robust connector.** Region-side gets auth free from the caps seed and needs no new wire trust; the translation layer is identical either way. Phase 2 is a single session later, not a redesign. | +| A-D2 | Merge order | AIS branch merges first (owns folder-version semantics); SSB rebases on it before S5. | +| A-D3 | Shared test region | Transylvania (D-5), both flags on. | + +## 5. Timeline answer + +| | CC wall-clock | Sessions | Your verify loops | +|---|---|---|---| +| SSB alone | ~4.75 h | 9 | 5 | +| AIS alone | ~3.6 h | 6 | 2 | +| **Both, interleaved** | **~8.5 h** (S7 folded into J1) | **15** | **~7** | + +At the web-viewer cadence (12 sessions over two working days, verify loops between), that is **three working days**, four if the golden fixtures (Q-7) or the wipe-loop check (Q-3) turn up work. Running the two lanes strictly one-at-a-time costs nothing in CC time versus running them concurrently — the CC minutes are the same — and keeps bugs attributable, which is what made the web-viewer sessions cheap to verify. diff --git a/Docs/feature/ssb-appearance/DESIGN-BRIEF-ssb-appearance.md b/Docs/feature/ssb-appearance/DESIGN-BRIEF-ssb-appearance.md new file mode 100644 index 00000000000..ab8453a0288 --- /dev/null +++ b/Docs/feature/ssb-appearance/DESIGN-BRIEF-ssb-appearance.md @@ -0,0 +1,456 @@ +# Design Brief — Server-Side Baking for NGC-Tranquillity + +**Programme:** Track L / L-2. **Status:** DRAFT for decision. **Date:** 2026-09-03 +**Companion docs:** RECON (CC, 2026-09-02) + RECON addendum, ADR set, Build Plan, Ledger — all under `Docs/feature/ssb-appearance/`. + +## 1. Problem + +The stock LL viewer only bakes server-side (V1–V5). Tranquillity has no `UpdateAvatarAppearance` cap, no compositor, no appearance service, and sends no `AppearanceData` block. An LL-viewer user on a Tranquillity grid is a permanent cloud, and so is any appearance-passive client such as the web-viewer gateway. Firestorm masks this by baking client-side. + +The web viewer has, in the meantime, built the hard part (a faithful compositor) in the wrong place: a per-client gateway whose bake the sim persists for everyone. That is both duplicated effort and a fidelity hazard. The correct home for baking on a grid that runs NGC code is the sim. + +## 2. Goals + +G1. Stock LL viewer (`--loginuri` pointed at Legion Grid) sees itself and others fully textured on an SSB-enabled region. +G2. Firestorm on the same region continues to work — with SSB (its SL codepath) when the region flag is on, with client bake when it is off. +G3. The web-viewer gateway becomes a pure consumer on SSB regions: no baking code runs for a session on a region advertising bit 0. +G4. One compositor, one test harness, shared by the gateway and the sim (`OpenSimNGC.Appearance.Baking`). +G5. Bakes persist across logins (Halcyon rule) and expire so they do not accumulate in an operator's asset store (D-6). +G6. Ordinary OpenSim grid owners can run the web viewer without any of this; SSB is an NGC-Tranquillity feature behind a per-region flag. Add-only: no legacy handler is removed. + +## 3. Non-goals (this programme) + +- Outfit changes from the LL viewer (that is AIS v3, L-1). SSB ships "log in as yourself" first. +- Bakes-on-Mesh **rendering** in the web viewer (rendering-side, [[web-viewer]] S13). The sim *does* produce the 5 BoM aux bakes (§6.4). +- Physics wearables, hover height, animesh, PBR overrides. +- The `appearance-utility-bin` (LL's GL-based baker). Kept only as a future `IBakeBackend` for exact-parity operators; not built. +- Bake sizes above 512 (D-7). The library is parameterised; the sim default is 512. + +## 4. Architecture + +``` + +-------------------------------+ + LL viewer / | Region (Tranquillity sim) | + Firestorm(bit0) ---->| AppearanceBakeModule | +---------------------------+ + POST UpdateAvatar | - cap UpdateAvatarAppearance| | OpenSimNGC.Appearance. | + Appearance | - login-time bake trigger |-------->| Baking (shared library) | + | - COF/wearable resolver | in-proc| - LLWearable parser | + | - IBakeBackend (in-proc) | | - avatar_lad layer sets | + | - BakeStore (persist/expire)| | - compositor (Skia) | + | - AvatarAppearance sender | | - J2K encode | + +-------+---------------+-------+ | - fidelity report | + | | +------------+--------------+ + assets (bakes) | | avatar-service keys ^ + v v | + +-------+-------+ +-----+------+ | + | AssetService | | Avatar Svc | | + +-------+-------+ +------------+ | + | | + +-------v----------------------+ | + LL viewer GET | Robust: AppearanceService | | + texture// | texture/// | | + / --->| (proxy to AssetService) | | + +------------------------------+ | + | + Web-viewer gateway (D:\web-viewer) ---- on non-SSB grids only ----------------+ + on SSB regions: appearance-passive, consumes AvatarAppearance + asset route +``` + +### 4.1 Components + +| # | Component | Repo / location | New or changed | +|---|---|---|---| +| C1 | `OpenSimNGC.Appearance.Baking` — shared compositor library | Tranquillity tree (placement: ADR-003) | **new project**, code lifted from `D:\web-viewer\gateway\src\Gateway\Baking\` | +| C2 | `AppearanceBakeModule` — region module: orchestration, cap, triggers, sender | `Addons/` or `Source/OpenSim.Region.OptionalModules` (ADR-003) | new | +| C3 | `BakeStore` — persist bakes as assets, record channel→UUID + input hash + COF version in the avatar service, expiry reaper | region module + Robust reaper | new | +| C4 | `AppearanceServiceConnector` — Robust HTTP handler for `texture///` and login-response `agent_appearance_service` | Robust | new (ADR-002) | +| C5 | `LLClientView.SendAppearance` — emit `AppearanceData{AppearanceVersion=1, CofVersion}` when the avatar is server-baked; unchanged otherwise | `OpenSim.Region.ClientStack.Linden.UDP` | changed, add-only | +| C6 | `RegionHandshake` — set bit 0 of `RegionProtocols` when `[Appearance] ServerSideBaking = true` | ClientStack | changed, flag-gated | +| C7 | Gateway SSB-aware mode | `D:\web-viewer` | changed (Build Plan S6) | + +### 4.2 Bake pipeline (C2 → C1 → C3) + +1. **Trigger** (one of): login/`MakeRootAgent` on an SSB region; `UpdateAvatarAppearance` POST; console `appearance bake `; wearables changed via legacy UDP path (`AvatarNowWearing`, Firestorm on a bit-0 region still sends it — VERIFY). +2. **Resolve inputs**: read the agent's COF folder from the inventory service → link items → wearable items → wearable assets (types 5/13) → parse `LLWearable` text → visual params + per-face texture UUIDs; collect `VisualParams` from `AvatarAppearance` as the authoritative param vector (the two must agree — mismatch is a fidelity report entry, not an error). +3. **Hash**: per bake channel, hash (wearable asset IDs, texture IDs, the subset of params that feed that channel's layer set, bake size). Compare with the avatar-service record. Unchanged → skip compute, reuse stored bake UUIDs, still send `AvatarAppearance`. +4. **Fetch** every referenced texture asset once; decode J2K. +5. **Composite** each of the 11 channels via C1 (head, upper, lower, eyes, skirt, hair, leftarm, leftleg, aux1–3; skirt/hair/aux only if the layer set has content). +6. **Encode** J2K, store as assets with the bake flag (ADR-004), record UUIDs + hash + COF version in the avatar service, update `AvatarAppearance.Texture` baked faces in the ScenePresence. +7. **Send** `AvatarAppearance` to self (with `AppearanceData`) and to everyone in view (with `AppearanceData` too — harmless for Firestorm, required for LL viewers observing). +8. **Report**: a structured fidelity report per bake (unsupported layers, missing textures, param/COF disagreement) goes to the log at INFO and to the cap response `error` field only when the bake was refused. + +### 4.3 COF version handshake without AIS + +The viewer's `cof_version` is the COF folder's inventory `Version`. The sim reads the same number from the inventory service and records it. Both are the same field with one writer — the data layer's folder-version bump — which S3 proved rather than assumed (`AisMutation.ReportVersion` and `ServerSideBakingModule.CofVersionOf` read the same `InventoryFolderBase.Version`). + +Cap response: + +- `cof_version == server's` → **accept**: `{success:true}`. +- `cof_version < server's` → `{success:false, expected:}`; viewer re-requests. +- `cof_version > server's` → the viewer changed the COF through a path the sim hasn't seen yet; re-read the folder once, then respond as above. Never livelock: after N mismatches within T seconds, accept anyway at the server's version and log it (Ledger R-2). + +**What `success:true` means (revised in S5).** It means *accepted — the bake will follow within the save cycle*. It does **not** mean "baked", which is what S3 shipped and what the first three bullets used to say. + +S3 had the cap bake synchronously and answer afterwards. That is bake-on-arrival, which Q-16 rules out: the POST arrives before the region has resolved the new items to asset ids, and the appearance save that resolves them is up to `DelayBeforeAppearanceSave` (5 s) away. A bake at POST time composites wearables still carrying `UUID.Zero` and stores the result as if it were the new look. + +**On the ordering of the two signals.** The 5 s save delay is a configured value and is the only interval here that is established. The spread between `AgentIsNowWearing` and the cap POST for one change is **not measured** — an earlier "310 ms" figure was written into these notes without a source and has been withdrawn (Ledger Q-6). It does not affect the design: S5 makes *both* routes queue an appearance save and bake off its completion, so the ordering between them stops mattering. It affects only the debounce, which is sized against the save delay instead. + +So the cap now answers the handshake and queues an appearance save. The bake happens when that save completes, off `OnAvatarAppearanceChange` (§4.6). The legacy `AgentIsNowWearing` route already queued a save, so **both signals converge on one trigger with one ordering** — which matters because Q-6 established that both arrive, not one or the other. + +Consequences worth stating: + +- `BakeReason.Cap` is **no longer produced**. A cap-driven rebake surfaces as `CofChanged`, because that is what it is. The enum value remains valid. +- The `AvatarAppearance` the viewer is waiting for arrives after the save cycle rather than in step with the cap response. The viewer waits for it either way and does not re-request, so this is a latency change, not a protocol one. +- A change whose channels all hash the same still sends the appearance. Nothing is recomputed, but the message must go out: the viewer will not ask again. + +Pre-AIS the LL viewer cannot change the COF, so in practice only the login case fires. Firestorm on a bit-0 region *can* change the COF via UDP and will POST; the path above handles it as long as the inventory service bumps `Version` on UDP link changes (S0 verification). + +### 4.4 Persistence and expiry (G5) + +- Bakes are assets. They carry a marker (ADR-004) so a reaper can find them. +- The avatar service holds, per agent: `Bake:` = asset UUID, `BakeHash:`, `BakeCOFVersion`, `BakeSize`, `BakeUpdated` (UTC). No schema change — the Avatars table is key/value. +- A bake is **superseded** when a new one for the same channel is stored; the old asset is deleted immediately (no reaper needed for the common case). +- A bake is **expired** by the reaper when `BakeUpdated` is older than `[Appearance] BakeTTLDays` (default 30) *and* the agent has not logged in since; expiry deletes the assets and clears the keys, so the next login rebakes. This is the "don't clog someone's database" rule (D-6) — the reaper is Robust-side, opt-in, off by default for standalone. +- Stored bakes are also what the web-viewer gateway and any observer fetch via the ordinary asset route, so no second copy is ever needed. + +### 4.5 Rollout hazards (from BP-v2) + +- `[Appearance] ServerSideBaking` is per region. Default `false`. Turn on for one test region with both viewers present before any other region. +- Flipping bit 0 makes Firestorm switch to `UpdateAvatarAppearance` immediately. If the compositor produces a worse bake than Firestorm would, every Firestorm user on that region degrades — which is why the fidelity harness (S0) precedes the flag (S3), not follows it. +- Turning the flag **off** again is safe: Firestorm reverts to client bake on next login; the LL viewer reverts to cloud; stored bakes are ignored, not deleted. + +### 4.6 The AIS/COF seam (for S5) + +AIS v3 is live on Ebony and the Current Outfit folder is the authoritative record of what an agent wears: an +outfit change is `SlamFolder` on the COF and a take-off is `DELETE /item` / `RemoveItem` (AIS ledger A10, which +corrected A7 on exactly this point). The bake does not read the COF. `ServerSideBakingModule` passes +`sp.Appearance.Wearables` to the orchestrator (`ServerSideBakingModule.cs:125`) — a deliberate S1 Part 1a choice, +made when nothing could change the COF behind the region's back. AIS changed that, and S5 owns the consequence. + +#### Which store wins + +Neither, as stated — the question is malformed, and getting it wrong is how S5 discovers this late. + +- **The COF is authoritative about *membership*: which items are worn.** It is what the viewer reads back, what + `cof_version` counts (§4.3), and what survives a relog. +- **`sp.Appearance.Wearables` is authoritative about *what a bake can be made from*.** The COF holds + `AssetType.Link` rows (`AisEnvelope.cs:47`), so it names items, not assets; a bake needs asset ids and the + wearable bodies behind them. The ScenePresence is the only place in the region where membership has already + been resolved to assets. + +So the rule for S5 is an ordering rule, not a precedence rule: **the COF decides *whether* to bake; the +ScenePresence decides *what* to bake; and the bake must not run until the ScenePresence has caught up with the +COF.** A bake that reads one store while the other has moved is not a merge conflict to resolve — it is simply +early. + +#### What happens today if they disagree — the trace + +**AIS never touches the ScenePresence.** The AIS surface is inventory-backend only by design (Ledger P-2; +`IAisInventoryBackend.cs:9`, `AisInventory.cs:22`): nothing under +`Source/OpenSim.Region.ClientStack.LindenCaps/AIS/` references `ScenePresence`, `AvatarAppearance` or +`AvatarFactory`. A `SlamFolder` that rewrites the COF leaves `sp.Appearance.Wearables` exactly as it was. + +**The region learns from the viewer, over UDP, as a separate message.** The only assignment to +`Appearance.Wearables` anywhere in the tree is `AvatarFactoryModule.cs:1289`, inside `Client_OnAvatarNowWearing` +(`:1256`), which is raised by the `AgentIsNowWearing` packet (`LLClientView.cs:8431`, handler `:9229-9244`, event +`:86`). AIS writing the COF and the viewer sending `AgentIsNowWearing` are two independent messages with no +ordering guarantee between them. + +**And when it arrives, the asset ids are not there yet.** `MergeNowWearing` (`:1315-1354`) fills a listed item's +`AssetID` from the *existing* contents of that slot (`GetAsset`, `:1345-1349`); an item that was not already in +the slot gets **`UUID.Zero`**. The real asset ids are resolved only in `SetAppearanceAssets` (`:901-947`, its live body; a long commented-out block follows), called +from `SaveAppearance` (`:888`), which runs on a thread pool behind the save queue — `DelayBeforeAppearanceSave`, +default **5 seconds** (`:51`, `:71`), on a 500 ms tick (`:155`). + +**So the path is reliable but late, and there is a window.** Between `AgentIsNowWearing` and the queued save, the +newly worn item sits in `sp.Appearance.Wearables` with `AssetID == UUID.Zero`. A bake in that window reads it +through `BakeOrchestrator.ResolveWearables` (`BakeOrchestrator.cs:86-96`) as **worn but assetless** — a genuine +worn instance that contributes its layers' morph masks and no textures at all (the S1c/Q-12 rule, correct in its +own right and exactly wrong here). The avatar is baked wearing the *shape* of the new shirt and none of its +pixels. The window is at least the 5-second save delay, longer when the queue is busy, and unbounded at the front +because nothing bounds the gap between the AIS write and the UDP packet. + +Two things already in place soften this and neither is sufficient. The input hash includes the wearable's asset +id (`BakeHash.cs:46`, `:58`), so a stale bake's hash differs from the correct one and a *later* bake will not +reuse it — but the stale bake has already been stored, its face already applied, and the previous good bake +already deleted by supersede. And `SetAppearanceAssets` drops an item whose inventory row is missing +(`AvatarFactoryModule.cs:936-939`), which is a different failure from this one. + +#### The options for S5's trigger + +**(a) Read the COF directly.** Authoritative, and independent of whether the viewer sends anything. Costs: +`GetFolderForType` plus a folder-content fetch per trigger — a Robust round trip on a grid — then link → item → +asset resolution for every worn item (the descendents cap already does this dance at +`FetchInvDescHandler.cs:436-455`), plus a dependency on the folder `Version` bump (Ledger Q-1: present at the +data layer, `MySQLXInventoryData.cs:162`, `:244`, `:277-278`, `:288`). It also puts a *second* wearable resolver +in the tree alongside `SetAppearanceAssets`, which is the same class of mistake as two lanes deploying from two +branches. Worse, it does not actually fix the disagreement: a bake made from COF-resolved assets writes faces +onto an appearance whose `Wearables` still say `UUID.Zero`, so the next save and the next bake disagree with the +one just stored. + +**(b) Keep reading the ScenePresence, and trigger only after the region has applied the change.** Cheap, and it +reuses the one resolver. It depends on the apply path being reliable — and the trace above says it is reliable, +just late, with a precise completion point: `AvatarFactoryModule.cs:890`, immediately after `SetAppearanceAssets` +and `AvatarService.SetAppearance`. An event for exactly this already exists and is unused: +`EventManager.OnAvatarAppearanceChange` / `TriggerAvatarAppearanceChanged` (`EventManager.cs:404-405`, +`:1948-1967`), whose only call site in the tree is **commented out**, on the very next line +(`AvatarFactoryModule.cs:891`), with no subscribers anywhere. + +#### What Q-14 means for this, and why it decides the choice + +Q-14: an appearance save wipes the bake index, because `AvatarService.SetAvatar` deletes every row for the +principal before rewriting the appearance-derived keys (`AvatarService.cs:93`). **An outfit change is exactly +when an appearance save happens** — `Client_OnAvatarNowWearing` queues one at `AvatarFactoryModule.cs:1292`. + +So a bake triggered on the *arrival* of a COF change is wrong twice over: it composites from `UUID.Zero` asset +ids, and about five seconds later the queued save deletes the index it just wrote — so the work is lost as well +as incorrect, and the next login re-bakes from nothing. A bake triggered *after* that save has run reads resolved +asset ids **and** writes its index into a record that has just been rewritten and will not be rewritten again by +this change. + +**The stale-wearables problem and the wiped-index problem have the same fix, and it is an ordering fix.** That is +the strongest argument in this section. + +#### Recommendation + +**(b), hooked to the completion of `SaveAppearance`** — uncomment `TriggerAvatarAppearanceChanged` at +`AvatarFactoryModule.cs:891` (or raise an equivalent event on that line) and make S5's COF-change trigger a +subscriber to it. + +In terms of what breaks if the recommendation is wrong: + +- **If (b) is wrong** — some outfit change reaches the COF and never produces an `AgentIsNowWearing`, so the save + never fires — the failure is **a bake that does not happen**. The avatar keeps its previous, *valid* bake and + looks stale until the next login or one `appearance serverbake`. It is visible, residents report it as "my + shirt didn't change", it is diagnosable from the absence of an `[SSB]` line, and it is recoverable with one + console command. Nothing is corrupted. +- **If (a) is wrong** — the COF and the ScenePresence disagree and the bake follows the COF — the failure is **a + bake that is wrong and stored**: faces written from one truth while `Wearables` holds another, an index whose + hash describes inputs the ScenePresence never had, and a supersede that has *already deleted the previous good + asset*. That is the S1d/Q-13 class of defect — a bad bake painted over a good one — and it needs an + asset-level repair, not a re-trigger. + +The asymmetry is the whole argument: **(b) fails late, (a) fails wrong.** A baker that is occasionally late is a +nuisance; a baker that is confidently wrong destroys the previous good bake through supersede. Cost is the +secondary argument and points the same way — (b) is free, (a) is a Robust round trip plus N link resolutions on +every trigger, on a path S2 measured live at 2823 ms cold. + +**What (b) requires before S5 can rely on it.** These are S5's work, not caveats: + +1. The trigger must fire *after* `AvatarService.SetAppearance`, never before — that is the Q-14 ordering, and it + is the entire point. +2. `Client_OnAvatarNowWearing` returns early when nothing changed (`:1278-1283`), so no save is queued and no + event fires. Harmless for the bake (unchanged inputs would be `Reused` anyway), but "no event" must not be + read as "no change" by anything else that subscribes. +3. `SaveAppearance` drops a queued save when the presence has gone (`:871-880`); `FlushAppearanceSaveOnClose` + (`:129`) covers the normal close. A bake must not be attempted for a presence that is closing. +4. `BakeCOFVersion` is stored but not compared today (S2, ADR-004 "as built"). §4.3's handshake is what will + compare it, and it should be read *at the same point* the bake reads the wearables, not earlier. +5. **Ledger Q-6 stands and is the one thing that could overturn this.** If Firestorm on a bit-0 region stops + sending `AgentIsNowWearing` and only POSTs the cap, (b)'s trigger disappears on precisely the regions SSB is + enabled for — and then the cap POST becomes the trigger and this section's ordering rule applies to it + unchanged. That is a measurement, not an argument, and S5 should take it first. + +## 5. Interaction with the web viewer (G3, G4) + +| Situation | Gateway behaviour | +|---|---| +| Region advertises bit 0 | Do not bake. Accept the sim's `AvatarAppearance` for self (must accept `AppearanceData`-bearing messages), fetch bakes via the existing asset route, render. `appearance.status = "server"`. | +| Region does not advertise bit 0 (ordinary OpenSim, or flag off) | Current S11/S12 behaviour: gateway-side bake with the complete-or-nothing invariant and the fidelity gate. | +| Library update | Both consumers reference the same `OpenSimNGC.Appearance.Baking` version; golden-fixture tests live with the library, run in both CI paths. | + +The gateway's compositor directory is **deleted** once the library reference lands (S0b); no two copies. + +## 6. Open design questions for John + +Listed in the Ledger as D-1 … D-5 and Q-1 … Q-4. The three that block S0: +- D-1: SSB before AIS (order change from BP-v2's AIS→SSB). +- D-3: sim-side fidelity policy — bake best-effort with a logged report (recommended) vs refuse like the gateway. +- ADR-003: library placement (in-tree project vs NGC NuGet package — affects Mike and the gateway's reference style). + +## 7. S6 recon — gateway SSB-aware mode (ADR-009) + +Recorded 2026-09-05 against `feature/ais-v3` `b13f15add3` and web-viewer `master` `0a6acffea9`. No gateway code was changed; this section is the answer to the five recon questions S6 asked before building. + +### 7.1 Does LibreMetaverse 3.1.4 expose `RegionProtocols`? — Ledger Q-5, **answered: yes** + +Reflected out of `LibreMetaverse.dll` (net10.0, `~/.nuget/packages/libremetaverse/3.1.4/lib/net10.0/`), assembly identity `LibreMetaverse, Version=3.0.0.0`. Namespace is `LibreMetaverse.Packets`, **not** `OpenMetaverse.Packets` — the gateway's package and the OpenSim tree's vendored OMV are different assemblies with different namespaces. + +- `LibreMetaverse.Packets.RegionHandshakePacket+RegionInfo4Block` carries `UInt64 RegionProtocols` alongside `UInt64 RegionFlagsExtended`. Raw access is available. +- Better, the library already decodes it: `LibreMetaverse.Simulator.Protocols` is an instance field of type `LibreMetaverse.RegionProtocols`, a flags enum: + +| member | value | +|---|---| +| `None` | `0x0` | +| **`AgentAppearanceService`** | **`0x1`** | +| `SelfAppearanceSupport` | `0x4` | + +So bit 0 is named, decoded, and held per `Simulator`. **No raw-packet path is needed**, and Q-5's fallback question does not arise. The gateway reads `simulator.Protocols.HasFlag(RegionProtocols.AgentAppearanceService)`. + +That the sim's bit 0 is the right thing to key on is the viewer's own rule: `llviewerregion.cpp:3034` reads `RegionInfo4 / RegionProtocols` out of the handshake, `:3044` stores it, and `:3083` computes `mCentralBakeVersion = region_protocols & 1`. Consumers read it back through `getCentralBakeVersion()` (`llvoavatar.cpp:3945`). + +### 7.2 Does it parse the `AppearanceData` block? — **yes, and it surfaces it** + +- `LibreMetaverse.Packets.AvatarAppearancePacket+AppearanceDataBlock` = `{ Byte AppearanceVersion, Int32 CofVersion, UInt32 Flags }` — the same three fields the sim writes (S3). +- It reaches the consumer without touching packets: `LibreMetaverse.AvatarAppearanceEventArgs` exposes `AppearanceVersion`, `COFVersion`, `AppearanceFlags`, plus `FaceTextures`, `DefaultTexture`, `VisualParams` and `AvatarID`. + +So the gateway gets `cof_version` and `appearance_version` for self and for others from the ordinary appearance event. Note `AppearanceFlags` is an enum whose only member is `None`, so the `Flags` word carries nothing the library names — consistent with the sim writing 0. + +### 7.3 Does it surface `agent_appearance_service`? — **yes** + +`LibreMetaverse.LoginResponseData.AgentAppearanceServiceURL` (get/set) and `LibreMetaverse.NetworkManager.AgentAppearanceServiceURL` (get). The gateway does not need to read the raw login LLSD. This is the value S4 taught Robust to advertise and `llstartup.cpp` adopts only when non-empty. + +### 7.4 Where the compositor project reference points — **stale branch, current content** + +`gateway/src/Gateway/Gateway.csproj` references + + D:\tranq-ssb\Source\OpenSimNGC.Appearance.Baking\OpenSimNGC.Appearance.Baking.csproj + +`D:\tranq-ssb` is a worktree on **`feature/ssb-appearance` at `162bfadcc3`** ("perf(ssb): instrument the bake phases and answer Q-10", S2 Part 2), not the integration branch. It is 20+ commits behind `feature/ais-v3`. + +**It does not currently matter for correctness.** `git diff 162bfadcc3 b13f15add3 -- Source/OpenSimNGC.Appearance.Baking/` is empty and no commit in that range touches the library: S3, S4 and S5 changed the region module, the services and the wire, never the compositor. The gateway is therefore building a byte-identical library to the one at `b13f15add3`. + +It is still a hazard rather than a fact to file away: the next change to the library will land on `feature/ais-v3` and the gateway will silently keep building the old one. **Not repointed in this session — John's decision.** + +### 7.5 The gateway's self-appearance flow — every route to the bake/send step + +Three entry points, all funnelling into one method: + +| # | site | trigger | gate | +|---|---|---|---| +| 1 | `AgentSession.Appearance.cs:117` | login / appearance ready | `_opts.BakeOnLogin` (`:112`) | +| 2 | `AgentSession.Appearance.cs:121` `RequestRebake` | client `appearance.rebake`, from `SessionHub.cs:300` | none | +| 3 | `AgentSession.Appearance.cs:128` `OnRebakeRequested` | sim's `RebakeAvatarRequested` (subscribed `:70`, released `:79`) | `_opts.BakeOnLogin` | + +All three call `RunBakeAsync` (`AgentSession.Appearance.cs:131`), which single-flights on `_bakeRunning` and calls `AppearanceBaker.RunAsync(this, …)` (`:145`). `AgentSession` implements `IBakeSteps` (`:26`), so the pipeline is: `GatherWearablesAsync` (`:180`) → `DownloadWearablesAsync` (`:205`) → `CheckSupportAsync` (`:259`, the fidelity gate) → `DownloadTexturesAsync` (`:269`) → `CreateAndUploadBakesAsync` (`:332`) → `SendAppearanceAsync` (`:402`). + +`SendAppearanceAsync` is the only place an `AgentSetAppearance` leaves the gateway (`AppearanceBaker.cs:34`, and the S11 invariant at `:47-51`). It builds the packet in `BuildAppearancePacket` (`:481`) and publishes to the client in `PublishSelfAppearance` (`:430`). + +**Consequence for S6.** The `server`-mode construction has to cut the type, not the call: all three entry points already converge, so the branch point is a single one — but `AgentSession` *is* the `IBakeSteps` implementation, so "the code that can send appearance does not exist on that branch" means the server-mode session must not be an `IBakeSteps` at all, rather than an `AgentSession` that declines to bake. + +## 4.9 The config contract (S12) + +**An operator turns both lanes on for the whole simulator with two lines, and never names a region.** + +```ini +[AIS] + Enabled = true + +[Appearance] + ServerSideBaking = true +``` + +That is the switch, verbatim. A `[]` section is an **optional override and never the way to opt +in**: a single region opts *out* of a simulator-wide true with `AIS_Enabled = false` or +`ServerSideBaking = false` in its own section. + +**Precedence, both lanes, identical:** the region section's value if that section carries the key, else the +global, else `false`. `AISv3Module.ResolveEnabled` and `ServerSideBakingRegion.ResolveEnabled` have always done +this - S12 did not change the resolution, it changed what the configuration files *tell* an operator to do and +added the evidence. Note the asymmetry the operator sees and cannot avoid: the global AIS key is `Enabled` +(inside `[AIS]`) while the per-region key is `AIS_Enabled`, because a region section holds settings for many +modules and the key has to say which one it belongs to. `ServerSideBaking` is the same word in both places. + +**A region section that exists but says nothing about these keys does not opt out.** Regions commonly have a +section for other settings; the override applies only when the key itself is present. That is a test, not a +convention. + +**Every region logs one line at INFO when it loads, naming which config decided:** + +``` +[AIS]: region "Ebony": AIS v3 "ON" ("global") +[SSB]: region "Ebony": server-side baking "ON" ("global") +``` + +**The quotes are real.** The logger is structured and renders every argument quoted, so a verify grep written against the unquoted form matches nothing - use `grep -E 'AIS v3 "ON"|server-side baking "ON"'`. `("region section")` in place of `("global")` when the region's own section carried the key. This is what a flip +verify reads - after the two global lines go in and a region's own lines come out, every region must say +`("global")`, and a region still saying `("region section")` is one whose section was missed. + +## 4.8 What triggers a bake (S5, S9) + +Every trigger converges on one place: an appearance **save** completing, which raises `OnAvatarAppearanceChange` +(Q-16, S5). Nothing bakes on the arrival of a change, because the items are not resolved to asset ids yet. + +| # | Change | What the viewer sends | What queues the save | Session | +|---|---|---|---|---| +| 1 | Login / teleport arrival | - | the baked-texture cache check (`ScenePresence.cs:2291-2294`) | S3/S5 | +| 2 | Wear or take off a wearable | `AgentIsNowWearing` | `Client_OnAvatarNowWearing` (`AvatarFactoryModule.cs:1298`) | pre-existing | +| 3 | Wear or take off, AIS route | `UpdateAvatarAppearance` POST | the cap handler, after the §4.3 handshake: **it reads the COF and derives the worn set first**, then queues the save | S5, **S10** | +| 4 | Attach or detach | - | `AttachmentsModule`, six `QueueAppearanceSave` call sites | pre-existing | +| 5 | **Edit a worn wearable** (colour, texture, params) | AIS `UpdateItem` carrying `hash_id` | **the AIS `UpdateItem` handler, when the item's asset changed and the item is worn** | **S9** | + +**Why #5 needs its own trigger.** An edit moves nothing else the region watches. The worn set is unchanged +because the viewer keeps the item id (`llagentwearables.cpp:319`), so no `AgentIsNowWearing` follows. The COF +*does* move - the edit panel replaces the link - but a COF version bump is not a trigger by itself. And #3 is +present but unreliable here: `requestServerAppearanceUpdate` defers while any upload is pending +(`llappearancemgr.cpp:3849`), and an edit always has one, so the POST arrives late with a `cof_version` the COF +has already moved past and is refused as stale. Observed 2026-09-05: four edits, no bake, one POST at 20:57:40 +refused with "client cof_version 578, server 579". + +**Why #3 has to read the folder (S10).** Until S10 the cap only queued a save, and on a bit-0 region that +made #3 a trigger with no input: **nothing in the tree turned a COF link into an `AvatarAppearance.Wearables` +entry.** #2 cannot, because the LL viewer's only `AgentIsNowWearing` sender is +`LLAgentWearables::sendDummyAgentWearablesUpdate` — four hard-coded nonsense item ids, and no callers left +(`llagentwearables.cpp:819-851`). #5 cannot, because it only rewrites the asset of an item id already worn +(`AisWornAssets.cs:32-52`). So a wear that added a link produced a save of the wearables the sim already had and +a bake that reused every channel. Observed on Ebony 2026-09-06 10:09:52: two shirts linked in the COF, `reused +6/6`, and the `Avatars` record holding `Wearable 4:0` alone. + +The cap now calls `ServerSideBakingModule.ApplyCofToWearables` (`:382`) before `QueueAppearanceSave` (`:365`): +`GetFolderContent` on the COF, each `AssetType.Link` resolved to its target, and `CofWearables.Derive` applied. +**Read, do not bake** — the Q-16 ordering is unchanged, because the derived items still carry unresolved asset +ids until `SetAppearanceAssets` runs inside the save. + +**Order within a type.** The viewer keeps it in the link item's description as `"@" + (type * 100 + index)` +(`build_order_string`, `llappearancemgr.cpp:3637-3642`, written by `getWearableOrderingDescUpdates` `:3676-3702` +and pushed to the server by `updateClothingOrderingInfo` `:3733`), and layers by it, later index on top +(`LLTexLayerTemplate::render`, `lltexlayer.cpp:1659-1689`, over the cache built `0..n-1` at `:1615-1638`). +`CofWearables.Derive` sorts on that key and sinks unnumbered links below the numbered ones, as +`WearablesOrderComparator` does (`:3644-3674`). Nothing downstream needed changing: `AvatarWearable` already +holds five per type in order, `BakeOrchestrator.ResolveWearables` already walks `j` over `slot.Count`, and +`TexLayerCompositor` already composites every instance of a type in list order +(`TexLayerCompositor.cs:423-431`). + +**S8 is what makes an unresolvable link safe.** A link whose target this region cannot read is dropped in the +reader rather than attributed to a guessed type; its type is then one this read says nothing about, and `Derive` +keeps what the agent already wears — the same answer `SetAppearanceAssets` gives (`AvatarFactoryModule.cs:975-989`). + +**The save contract (A19).** #5 fires off the AIS `UpdateItem` PATCH, and that PATCH must only report success +when the save actually happened. Until A19 it always reported success: the asset-transaction chain was `void` from +`IAgentAssetTransactions` down, so a validator refusal reached nobody and the cap answered `200` with the item's +old asset id. It now carries `AisAssetTransaction` - `Applied`, `NotResolvable`, `Refused` - and a **`Refused` +answers 403 and emits no `_updated_category_versions`**, so the folder version the viewer holds does not advance +and its next fetch still sees the true state. `NotResolvable` (no transaction module, no client, the library +backend, an unknown transaction id whose xfer is still in flight) is **not** a failure and still answers 200. + +The bake side follows from that: a refused save changes no asset, so the S9 hook does not fire and no bake is +queued for an outfit that did not change. That is pinned by a test rather than left to follow from the code. + +**Cost.** #5 queues a save; it does not bake. The save re-resolves every worn item and the bake's per-channel +input hash then decides what is recomputed, so an edit that changed nothing visible costs one hash check per +channel. An item that is not worn queues nothing. + +## 4.7 The body-part guard (S8) + +A bake is refused when the incoming wearable set has lost one of the four body-part slots — shape, skin, hair, +eyes — since the last set this sim successfully baked from. + +**Why a refusal and not a warning.** Storing a bake supersedes the asset it replaces, and supersede means delete +(ADR-004). A bake composed from a set with no skin is a valid-looking bake of nothing, and once it is stored the +good bakes are gone; baking again cannot recover them. On 2026-09-05 exactly that happened: four unresolvable +item ids emptied slots 1-4, and the `reason=CofChanged` bake that followed reported "no Skin worn / no Eyes worn +/ no Hair worn", stored 4 channels and superseded 4. + +**Why these four slots and why only present -> absent.** A resident cannot take off a body part — no viewer +offers it, and every avatar has all four from creation — so that transition is never something the resident did. +It is always a failure upstream of the bake: a stale viewer cache, an inventory service that answered late, an +item from another grid. Everything a resident *can* do passes through untouched, because it either keeps the +body parts populated or replaces them: changing clothes, stripping to underwear, swapping a shape. + +**The baseline.** Recorded only after a bake succeeds, never after a refusal. Recording a refused set would make +the next attempt see no loss and do the damage anyway — the guard would delay it, not prevent it. The first bake +of a session always proceeds, there being nothing to compare against, and `Forget` clears the baseline on close. + +**Standing.** This is a backstop, not the cure. With S8's `SetAppearanceAssets` and child-presence fixes in +place the empty set should not reach the baker at all; the guard exists because the cost of being wrong here is +unrecoverable and the cost of a false refusal is one skipped bake. diff --git a/Docs/feature/ssb-appearance/FACT-SHEET-ais-ssb-for-mike.md b/Docs/feature/ssb-appearance/FACT-SHEET-ais-ssb-for-mike.md new file mode 100644 index 00000000000..de72dd74e02 --- /dev/null +++ b/Docs/feature/ssb-appearance/FACT-SHEET-ais-ssb-for-mike.md @@ -0,0 +1,40 @@ +# AIS v3 + Server-Side Baking on NGC-Tranquillity — fact sheet + +*Prepared 2026-09-03 for Mike. Sources: ten-pass SL-parity audit of the tree at `645b0f3` against the LL viewer at `62033f2`; web-viewer sessions 11–12.* + +## Why both, why now +The current LL viewer cannot function on Tranquillity today. Two of the four reasons are these: it **only** bakes server-side (client-bake code is retained but has no callers), and its outfit changes, item/folder deletes, and Empty Trash are **AIS-only** (they no-op with a log warning otherwise). Firestorm masks both. SSB makes an avatar visible; AIS makes it changeable. One without the other is half a viewer. + +## SSB — what it is on the wire +- Viewer decides per region from `RegionHandshake.RegionProtocols & 1`. +- After every outfit change it POSTs `UpdateAvatarAppearance {cof_version}`; expects `{success, expected, error}`. +- Sim composites the 11 bake channels (6 legacy + 5 Bakes-on-Mesh), stores them, and sends `AvatarAppearance` **with** an `AppearanceData{AppearanceVersion=1, CofVersion}` block — the viewer drops its own appearance as stale without it. +- Other avatars' bakes are fetched from `agent_appearance_service` (login response) at `texture///`. + +**Where we stand:** a faithful C#/.NET 10 compositor already exists — built for the web-viewer gateway, driven by the viewer's own `avatar_lad.xml` with `LLTexLayerSet` semantics, tested against Firestorm's bakes. The viewer C++ (`lltexlayer.cpp`, `llavatarappearance.cpp`) was the template, as you suggested; we did **not** wrap LL's GL-based `appearance-utility-bin` (x86 Linux + Xvfb, not a fit). LibreMetaverse's baker is disqualified (decompile: tiles sub-1024 layers into a 2×2 mosaic; skips layers — two upstream bugs reported). Plan is to lift the compositor into a shared library (`OpenSimNGC.Appearance.Baking`, proposed NuGet — your call) used by both the sim and the gateway. The remaining SSB work is plumbing: cap, `AppearanceData`, per-region flag, persistence with expiry, Robust appearance service. + +**On your two benefits:** fewer transfers — yes, 11 bakes replace every wearable texture per avatar per observer. Raw-texture protection — **only partly** on OpenSim as-is: `GetTexture` serves any asset UUID, so a client that learns a skin's UUID can still fetch it. Making SSB actually protective needs one more slice: on bit-0 regions, refuse `GetTexture` for wearable-referenced textures to anyone but the owner (SL's behaviour). Filed as a follow-up, not in the first build. + +## AIS v3 — what makes it "janky" +You're right that it isn't plain REST. The parts that carry the effort: +- **HAL-style envelopes** with `_embedded{categories,items,links}` (links are a separate collection, not items) and, on mutations, delta sets (`_updated_items`, `_created_items`, `_removed_items`, `_updated_categories`…) that the viewer applies directly to its local model — wrong deltas = silently divergent inventory. +- **Per-operation folder-version bump rules** (which folders a slam, a link create, a move must bump) and echo of a client `tid`. +- **SlamFolder** (`PUT /category//links`) — atomic replace-all-links; done non-atomically, a mid-way failure strips the avatar's COF. +- `simulate` dry-run, `COPY` verb for library copy, `/category/current` alias, `/orphans`. +- **All-or-nothing:** once `InventoryAPIv3` is advertised the viewer routes *all* inventory through it, fetches included. A partial AIS is worse than none. + +**Where we stand:** `OpenSim.Services.AISv3` in the tree is the `dotnet new webapi` template (32-line weather controller). The full route/verb/envelope table is already extracted from `llaisapi.cpp` for the build. Hosting: Phase 1 as a region-side caps module translating to `IInventoryService` (auth free via the caps seed); behind an interface so Phase 2 can host the same handler on Robust with a tokenized URL — that's the "inventory out of the simulator" step, one later session, not a redesign. + +## Rules that hold for both +- **Add-only.** No UDP handler (`AgentSetAppearance`, `UploadBakedTexture`, UDP inventory ops) is removed. Firestorm keeps working either way. +- **Per-region flags**, default off in every shipped ini (`[Appearance] ServerSideBaking`, `[AIS] Enabled`). One test region with both viewers before any other region flips. +- **Harness-defined done**: AIS — HTTP acceptance harness against fixture envelopes; SSB — pixel-diff against Firestorm's bakes of a stock-Library outfit. + +## Size (Claude Code wall-clock, measured cadence) +| | CC time | Sessions | +|---|---|---| +| SSB | ~4.75 h | 9 | +| AIS | ~3.6 h | 6 | +| Both, interleaved, joint soak | **~8.5 h** | 15 | + +About three working days at the cadence the web viewer ran at. Two questions where your input matters: publishing the compositor as an NGC NuGet package, and whether Phase-2 Robust hosting of AIS is the direction you want. diff --git a/Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md b/Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md new file mode 100644 index 00000000000..4c116928ca2 --- /dev/null +++ b/Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md @@ -0,0 +1,80 @@ +# RECON Addendum — Server-Side Baking (SSB) for NGC-Tranquillity + +**Programme:** Track L, item L-2 (BUILD-PLAN-sl-parity-v2) +**Supplements:** `RECON-ssb-appearance.md` (Claude Code recon, delivered 2026-09-02 to `D:\_TO_REVIEW\ssb-appearance\`) +**Tree pin:** the parity audit's findings are pinned to `645b0f3`; live grid runs `cb141dd61d` + `db7c746248` (maptile fix). Nothing appearance-related has changed between those commits as far as chat history shows — **VERIFY at S0** (Build Plan). +**Date:** 2026-09-03 + +## 1. Why an addendum + +The CC recon was written before the web-viewer Sessions 11 and 12 and the appearance wire spike. Those three pieces of work changed the SSB picture materially: + +1. A **working, data-driven bake compositor now exists in C# on .NET 10** — `gateway/src/Gateway/Baking/` in `D:\web-viewer`. It interprets `avatar_lad.xml` layer sets with the viewer's `LLTexLayerSet` semantics, has a fidelity gate, and has been compared against Firestorm bakes on real avatars. The recon's "port BakeLayer.cs onto SkiaSharp+CoreJ2K" recommendation is therefore **already ~done, in the wrong repo**. SSB on the sim is no longer a compositor project; it is a *plumbing* project plus a *library extraction*. +2. **LibreMetaverse 3.1.4's `Baker` is disqualified** as a backend for anything that persists (decompile-confirmed: tiles sub-1024 layers into a 2×2 mosaic; earlier: skips layers). The recon's `IBakeBackend` seam stays, but "managed baker as default" now means *our* compositor, not LibreMetaverse's. +3. The wire spike established what the **sim already delivers** with zero grid changes: other avatars' baked-texture UUIDs (5 legacy slots) in `AvatarAppearance`, fetchable as ordinary assets; `VisualParams` present; `AppearanceData` block still omitted (count 0). The only reason a passive client stays a cloud is that nothing bakes for it. + +## 2. State of the tree — appearance surface + +Verdicts carry a source tag. `[P3]` = RECON-03 of the parity audit at `645b0f3`; `[WS]` = wire spike 2026-09-02, live grid; `[S11/S12]` = web-viewer sessions; `[UNVERIFIED]` = needs the S0 grep pass before any code is written. + +| Element | State | Source | +|---|---|---| +| `RegionProtocols` in `RegionHandshake` | `1UL << 63` only; bit 0 (server bake) clear | [P3] | +| `UpdateAvatarAppearance` cap | absent | [P3] | +| `AgentSetAppearance` UDP handler | present (Firestorm/client-bake path) | [P3] | +| `UploadBakedTexture` cap | present (Firestorm path) | [P3] | +| `AvatarAppearance` → `AppearanceData` block | count 0 (`LLClientView.cs:4521` at 645b0f3) | [P3] [WS] | +| `AvatarAppearance` → `AppearanceHover` | 1 block, hover Z | [P3] | +| `AvatarAppearance` → baked TE UUIDs for others | present, 5 legacy slots; BoM aux slots not observed | [WS] | +| `VisualParams` on the wire | present | [WS] | +| `agent_appearance_service` in login response | absent | [P3] | +| Baked-texture service (`texture///`) | absent | [P3] | +| `[BakedTextureService]` / XBakes-style store | present in stock OpenSim; **Tranquillity status UNVERIFIED** | [UNVERIFIED] | +| Avatar service persistence of textures | persists wearables + params; sim asks each login to rebake, so bakes are *not* durably persisted | [WS] | +| `AgentCachedTexture` handler | present (stock) | [UNVERIFIED] | +| `Client_OnAvatarNowWearing` wipe-loop fix | fixed on Legion Dec-2025 tree; **port status on Tranquillity UNVERIFIED** | [UNVERIFIED] | +| COF folder `Version` increments on link add/remove | stock OpenSim behaviour; **verify the inventory service actually bumps it on the UDP link path** | [UNVERIFIED] | +| Inventory API v3 (AIS) | absent (`OpenSim.Services.AISv3` is an empty template) | [P2] | + +## 3. Viewer contract (stock LL viewer @ `62033f2`) + +Reproduced from RECON-03 §3.1 because every design choice below hangs off it. + +| # | Rule | Evidence | +|---|---|---| +| V1 | Viewer chooses server bake iff `RegionHandshake.RegionProtocols & 1` | `llviewerregion.cpp:3097` | +| V2 | Client-side bake path (`UploadBakedTexture` → `AgentSetAppearance`) has **no callers** — code retained, dead | `sendAppearanceMessage` only at definition | +| V3 | After every COF change the viewer POSTs `UpdateAvatarAppearance` with `{cof_version}` and expects `{success, expected, error}` | `llappearancemgr.cpp:2572, 3865–3882`, `requestServerAppearanceUpdateCoro` | +| V4 | The viewer **drops its own** `AvatarAppearance` as "Stale appearance" unless `AppearanceData.CofVersion` > last received; a message with **no** `AppearanceData` block is rejected for self | `llvoavatar.cpp:9779–9800` | +| V5 | `AppearanceVersion` forced to 1 when server bakes | `llvoavatar.cpp:9727–9737` | +| V6 | Other avatars' bakes are fetched from `texture///`; empty service URL → `""` + warning, avatar never textures | `LLVOAvatar::getImageURL` | +| V7 | Outfit *changes* (wear/take-off/replace outfit, empty trash) are AIS-only in the LL viewer | RECON-02 | + +Consequence of V7: **SSB without AIS gives the LL viewer "log in as yourself, can't change clothes."** That is exactly the tier John accepted for the web viewer on 2026-09-02, so SSB is shippable ahead of AIS. See Ledger D-1. + +## 4. Web-viewer facts that constrain the grid design + +| Fact | Implication for SSB | +|---|---| +| Gateway must be **appearance-passive** by default (S3 hotfix e881646) — sending appearance from a partial wearables fetch corrupted stored looks | The sim's SSB must never depend on the *client* sending anything appearance-shaped; login-time bake is server-initiated | +| Gateway compositor is data-driven from `avatar_lad.xml`, read at runtime from the LibreMetaverse NuGet, not copied into the tree | Grid-side library must ship or reference `avatar_lad.xml` explicitly — Ledger Q-2 | +| Fidelity gate (S12): compositor refuses unsupported wearable types, multi-wearables, and the 5 BoM extra slots → `"unsupported"`, no bake persisted | Gate is correct in the gateway (its bake persists for all viewers). On the sim, for an LL viewer, refusing means a permanent cloud — different policy needed. Ledger D-3 | +| Ruth2 v4 / Roth2 v2 body meshes are AGPL/CC — rendering-side only | Irrelevant to the sim; bakes are body-agnostic | +| Gateway's bake is persisted by the sim and seen by every viewer | Once SSB is live on a region, the gateway must **stop baking there** and consume the sim's bakes (S6 in the Build Plan) | +| Stock-Library outfit on Truly is the clean reference; Firestorm bakes of it are the golden images | Golden fixtures for the shared library's test harness come from this — his step, still pending | + +## 5. Halcyon reference — what to take, what not + +From `/d/halcyon-reference-fresh/` (read-only). The recon recommended "Halcyon's persistent-bake rule". Restating it precisely so it is not over-applied: + +- **Take:** bakes are first-class persisted assets tied to the avatar record; a login does not force a rebake if the stored bakes match the stored wearables; a change to wearables/params invalidates them. +- **Take:** hash-of-inputs as the invalidation key (wearable asset IDs + visual params + texture IDs per bake channel), so the compositor is skipped when nothing changed. +- **Do not take:** Halcyon's client-driven bake upload path — Halcyon still had the viewer composite. The *compute* moves to the sim here; only the persistence rule is Halcyon-lineage. +- **Do not take:** any Halcyon wire message; the LL viewer contract in §3 is the spec. + +## 6. Items the CC recon should be re-checked on at S0 + +- Whether Tranquillity carries `OpenSim.Services.BakedTextureService`/`XBakes` at all, and if so whether it is region-side or Robust-side (affects ADR-002). +- Whether `Client_OnAvatarNowWearing` starts from an empty `AvatarAppearance` (the Legion wipe-loop bug) — must be fixed *before* any server-initiated bake touches stored wearables. +- Exact J2K encode path available in the tree (OpenJPEG via OpenMetaverse vs CoreJ2K/CSJ2K) — decides whether the shared library carries its own encoder dependency. +- Whether `SendAppearance` at HEAD still writes `AppearanceData` count 0 (line moved since 645b0f3?). diff --git a/Docs/feature/ssb-appearance/RECON-ssb-appearance.md b/Docs/feature/ssb-appearance/RECON-ssb-appearance.md new file mode 100644 index 00000000000..72f30f95ccc --- /dev/null +++ b/Docs/feature/ssb-appearance/RECON-ssb-appearance.md @@ -0,0 +1,263 @@ +# Recon Report — Server-Side Baking (SSB) / Server-Side Appearance + +**Status:** DRAFT for review — recon + design brief. No code, no branch, no build. Supersedes the appearance findings of `RECON-03-avatar-appearance.md` (Pass 3, pinned `645b0f3`) for this subject. +**Scope:** what the code does today, the two lineages that could deliver SSB, a recommendation, the compositor design, a safe rollout, dependencies, and a build-plan skeleton sized to a first demoable milestone. + +**Tree:** `JohnLegionH/OpenSim-Tranquillity` at `/d/tranquillity-develop` +**Commit:** `a68d59f232340b62b3e12ee4c9f62f4f2374e60d` — "fix(map): restore legacy MapImageModule terrain rendering after Skia rework", 2026-09-02, checkout branch `feature/voice-visibility-matrix`. 167 commits after Pass 3's `645b0f3bb3` (2026-07-31). +**Target framework:** `net10.0` +**References read (read-only):** Halcyon at `/d/halcyon-reference-fresh` ("Copyright (c) InWorldz Halcyon Developers"); Firestorm viewer source at `/d/phoenix-firestorm/indra/newview` (the LL-upstream code with Firestorm's `[Legacy Bake]` additions marked, which is how the stock-LL behaviour was isolated — no separate LL checkout exists on this machine, `/d/SLViewer-Source` is empty); stock OpenSim 0.9.3 at `/d/opensim - Use this december 2025`; libopenmetaverse source at `/d/libomv-src`; LibreMetaverse 3.1.4 (the web viewer gateway's library) from the NuGet cache; the live grid's configuration under `/d/legiongrid`. +**Method:** direct inspection with `grep`/`sed`; every file:line below is to the commit above unless another tree is named. + +--- + +## R0. Delta against Pass 3 (`645b0f3`) + +None of the 167 commits between `645b0f3` and HEAD touch the appearance path. `AvatarFactoryModule.cs`, `LLClientView.cs` (handshake and `SendAppearance`), `SimulatorFeaturesModule.cs`, `UploadBakedTextureModule.cs`, `XBakesModule.cs` and `AvatarAppearance.cs` carry no SSB-related change. **Pass 3's findings stand at HEAD.** Two of them are *refined* below rather than corrected: the missing `AppearanceData` block (R3) explains why a stock LL viewer shows *other* avatars as clouds too, not only itself; and the web-viewer side turns out to already speak the SL SSB client contract (R9), which changes the sizing of the "serve both viewers" requirement. + +## R1. No bake cap is registered; the SSB advertisement bit is clear + +**Caps.** The complete set of cap names registered anywhere under `Source/OpenSim.Region.ClientStack.LindenCaps` and `Source/OpenSim.Region.CoreModules/Framework` was enumerated (`RegisterHandler(`, `RegisterSimpleHandler(`). It contains `UploadBakedTexture` and `GetTexture`; it does **not** contain `UpdateAvatarAppearance`, nor any appearance-service cap. The `UploadBakedTexture` cap is the *client-bake* upload path: `Source/OpenSim.Region.ClientStack.LindenCaps/UploadBakedTextureModule.cs:97-112` registers it (locally when `Cap_UploadBakedTexture = "localhost"`, `OpenSimDefaults.ini:867`) and `:189-209` stores whatever the viewer uploads as a `Temporary = true`, `Local = true` texture asset (`:207-208`). + +**Where a bake cap would register.** The pattern to copy is `UploadBakedTextureModule.RegisterCaps` (`:97-112`): an `ISharedRegionModule` hooking `Scene.EventManager.OnRegisterCaps` and calling `caps.RegisterSimpleHandler("UpdateAvatarAppearance", …)`. `SimulatorFeaturesModule.cs:188` shows the same hook for `SimulatorFeatures`. + +**RegionProtocols.** The RegionHandshake writer sets the `RegionInfo4` block explicitly, `Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs:986-995`: + +``` +//RegionInfo4 block +//RegionFlagsExtended +zc.AddByte(1); +zc.AddUInt64(regionFlags); +//RegionProtocols + // bit 0 signals server side texture baking + // bit 63 signals more than 6 baked textures support" +zc.AddUInt64(1UL << 63); +``` + +So **bit 0 (SSB) is 0 and bit 63 (eleven bake slots / Bakes-on-Mesh) is 1**, hard-coded, for every region. There is no configuration switch; `grep RegionProtocols` over `Source` finds only this site. `SimulatorFeatures` separately advertises `BakesOnMeshEnabled = true` (`SimulatorFeaturesModule.cs:136`). + +**Who reads it.** Viewer side: `llviewerregion.cpp:3264-3277` reads `RegionInfo4.RegionProtocols` from the handshake and `:3316` derives `mCentralBakeVersion = region_protocols & 1`. That single value gates the whole SSB request path: `llappearancemgr.cpp:4268-4272` returns "Region does not support baking" when it is 0, before the `UpdateAvatarAppearance` cap is even looked up (`:4274-4278`). LibreMetaverse 3.1.4 exposes the same bit as `RegionProtocols.AgentAppearanceService` (R9). Nothing on the sim side reads the bit back. + +## R2. What the appearance path does today: client-bake pass-through plus an optional bake cache + +The sim never composes a texture. It receives the viewer's own bakes and relays them. + +1. **Inbound.** `LLClientView.cs:8430` maps `AgentSetAppearance` to `HandlerAgentSetAppearance` (`:9196-9221`), which decodes the TextureEntry, visual params, avatar size and the `WearableData` cache items and raises `OnSetAppearance`. `AvatarFactoryModule.Client_OnSetAppearance` (`Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs:1179-1186`) → `SetAppearance` (`:168-215`): `SetVisualParams` (`:185`), `SetTextureEntries` (`:195`), `UpdateBakedTextureCache` (`:199`), then `QueueAppearanceSave` / `QueueAppearanceSend` (`:214-215`). The TextureEntry the viewer sends already *contains the baked texture UUIDs* it uploaded through `UploadBakedTexture`; the sim stores the ids, never the layers. +2. **Cache validation.** `AgentCachedTexture` (`LLClientView.cs:8368`, `:12389`) → `Client_OnCachedTextureRequest` (`AvatarFactoryModule.cs:1243-1263`) answers from `sp.Appearance.WearableCacheItems`; `ValidateBakedTextureCache` (`:485-670`) checks the region asset cache and, when `IBakedTextureModule` is present, the external bake store (`:589-640`, assets re-flagged `Temporary`/`Local` at `:621-622`). A miss ends in `RequestRebake` (`:674-718`) → `SendRebakeAvatarTextures` (`:715`), i.e. *the viewer is asked to bake again*. `ScenePresence.cs:2291-2295` runs this validation on region entry. +3. **Outbound.** `ScenePresence.SendAppearanceToAgent` (`ScenePresence.cs:4338-4347`) → `LLClientView.SendAppearance` (`:4499-4543`), the `AvatarAppearance` packet: TextureEntry, visual params, **no `AppearanceData` block** (`:4532-4533`, literally `// no AppearanceData` then a zero count) and an `AppearanceHover` block. +4. **Wearables and COF.** `SetAppearanceAssets` (`:839-1066`) resolves wearable item ids to asset ids through `IInventoryService` at save time; `TryAndRepairBrokenWearable` (`:1067-1116`) rebuilds Current Outfit Folder links. `Client_OnRequestWearables` (`:1158`) and `Client_OnAvatarNowWearing` (`:1194`) are the legacy UDP wearable paths. The sim reads COF and wearables today without AIS (relevant to §E). +5. **Bake storage.** `IBakedTextureModule` (`Source/OpenSim.Region.Framework/Interfaces/IBakedTextureModule.cs`: `Get(UUID)`, `Store(UUID, WearableCacheItem[])`, `UpdateMeshAvatar`) is implemented by `XBakesModule` (`Source/OpenSim.Region.CoreModules/Avatar/BakedTextures/XBakesModule.cs`), a REST client to Robust's `XBakes` file store (`Source/OpenSim.Server.Handlers/BakedTextures/XBakes.cs:56-107`, `BaseDirectory`). It is inert unless `[XBakes] URL` is set (`XBakesModule.cs:55-67`). On the live grid Robust *does* host the service (`/d/legiongrid/gridserver/config/Robust.ini:281-282`, `[BakedTextureService] LocalServiceModule = "OpenSim.Server.Handlers.dll:XBakes"`) but no region config sets `[XBakes] URL`, so the regions never talk to it; `PersistBakedTextures = false` (`OpenSimDefaults.ini:947`) keeps uploaded bakes temporary. + +**Net effect for a stock LL viewer.** It never sends `AgentSetAppearance` or uploads bakes (those code paths exist in Firestorm only inside `// [Legacy Bake]` blocks, e.g. `llagentwearables.cpp:510`, `:529`, `llagent.cpp:6369-6703`), never asks `UpdateAvatarAppearance` (R1), so the sim holds the default TextureEntry and everyone sees a cloud. Only Firestorm's legacy client-bake path (enabled on OpenSim grids) masks this — and it is Firestorm-only. + +## R3. Refinement: the missing `AppearanceData` block makes *other* avatars fail too + +The viewer resolves an avatar's appearance version from the `AvatarAppearance` packet (`llvoavatar.cpp:10729-10732` reads `AppearanceData.AppearanceVersion` and `CofVersion`; `:10838-10846` reads visual param 11000). LL-upstream's resolution, preserved as commented-out lines at `llvoavatar.cpp:10864-10874`, is: param if present, else field if > 0, **else 1** ("still not set, go with 1"). Firestorm's replacement (`:10875-10886`) resolves the same missing data to **0** (legacy). `:11015` then calls `setIsUsingServerBakes(appearance_version > 0)`. + +Because Tranquillity sends no `AppearanceData` (R2 step 3), a **stock LL viewer treats every avatar it sees as server-baked**, builds bake URLs from the appearance-service URL (`llvoavatar.cpp:6815-6836`: `texture///`), finds the URL empty ("`AgentAppearanceServiceURL not set - Baked texture requests will fail`", `:6825`) and never fetches. The fix for that is part of SSB anyway (R7), but note it: **once the sim emits `AppearanceVersion = 1` it must also serve bake URLs, and until then it must emit `AppearanceVersion = 0` explicitly** rather than omit the block. Emitting `0` is a zero-risk, viewer-visible improvement independent of SSB (Firestorm ignores it; LL viewers stop trying the bake service for legacy avatars). Recorded as ledger F-3 / D-4. + +## R4. The SL server-bake contract, as the viewer implements it + +This is the contract Tranquillity must meet; it is small and precisely observable in the viewer source. + +| Step | Viewer behaviour | Source | +|---|---|---| +| Advertise | `RegionProtocols` bit 0 → `mCentralBakeVersion` | `llviewerregion.cpp:3316` | +| Locate the bake server | login response field `agent_appearance_service` (URL, trailing slash expected) | `llstartup.cpp:5161-5166` | +| Request a bake | `POST ` with LLSD `{ "cof_version": N }` (a debug setting can send the whole COF instead, `:4343-4351`) after every outfit change; skipped while editing appearance | `llappearancemgr.cpp:4243-4351` | +| Reply | LLSD map: `success` (bool) required; on failure `error` (string) and optionally `expected` (int) — a COF-version mismatch makes the viewer re-request its own `AvatarAppearance` and retry with back-off up to `BAKE_RETRY_MAX_COUNT` | `:4359-4400` | +| Result delivery | the sim broadcasts `AvatarAppearance` with `AppearanceData.AppearanceVersion = 1` and `CofVersion = N`; visual param 11000 must agree | `llvoavatar.cpp:10729-10732`, `:10851-10863` | +| Fetch bakes | per baked slot: `GET texture///` (`FTT_SERVER_BAKE`, not written to the viewer's texture cache, expects J2C) | `llvoavatar.cpp:6831`, `lltexturefetch.cpp:1818`, `:2811` | +| Sanity | if the avatar is server-baked but the region says CBV 0, the viewer probes the bake URL and may force an update | `llvoavatarself.cpp:3636-3657` | + +`bakeName` is the texture entry's default image name from the viewer's avatar dictionary (`head`, `upper`, `lower`, `eyes`, `skirt`, `hair`, `leftarm`, `leftleg`, `aux1`…`aux3`); the sim can treat it as opaque and key on ``. + +The LLSD *success* payload's texture and visual-param contents are not consumed by the viewer in this code path (the viewer waits for the UDP `AvatarAppearance` instead — `:4380-4386` "the message will return through the UDP"); returning `{ success: true, cof_version: N }` is sufficient, with `textures`/`visual_params` optional for diagnostics. + +## R5. The appearance model already has the slots SSB needs + +`Source/OpenSim.Framework/AvatarAppearance.cs`: `VISUALPARAM_COUNT = 218` (`:54`), `TEXTURE_COUNT = 45` (`:57`), `BAKE_INDICES = { 8, 9, 10, 11, 19, 20, 40, 41, 42, 43, 44 }` (`:63`) — the six classic bakes plus left-arm, left-leg and aux1–3 for Bakes-on-Mesh — and `WearableCacheItems` (`:126`). `WearableCacheItem` (`Source/OpenSim.Framework/WearableCacheItem.cs:34-39`) carries `TextureIndex`, `CacheId`, `TextureID`, `TextureAsset`. `Serial` (`:77`) is the field Halcyon and the viewer both use as the COF version. Nothing here needs to change for SSB; the compositor writes into the same slots the viewer would have. + +## R6. Building blocks already in the tree, and one that is missing + +| Need | Present? | Where | +|---|---|---| +| J2K decode | yes | `CoreJ2K.Skia` (9 projects); `GetTextureHandler.cs:303` | +| J2K encode | **yes** | `Source/OpenSim.Framework/SkiaImageUtils.cs:27-52` `TryEncodeToJ2KLossless(SKBitmap)` (CoreJ2K encoder; lossless preset, a lossy preset is a one-line variant) | +| Raster ops (resize, blend, tint, masks) | yes | SkiaSharp 4.151.1 (11 projects) | +| Wearable asset parser (`LLWearable` text: params + textures) | yes | `UtopiaSkye.OpenMetaverse` 1.1.6 (`Directory.Build.props:13-17`); the DLL exports `AssetWearable` and `VisualParams` | +| Visual-param / alpha-mask definitions (`avatar_lad.xml`) and the TGA mask layers | yes, shipped | `/d/legiongrid/regionserver/openmetaverse_data/` (`avatar_lad.xml`, `head_alpha.tga`, …), loaded by libomv's `VisualParams` from `Settings.RESOURCE_DIR` | +| **The compositor itself** | **no** | `UtopiaSkye.OpenMetaverse` does not export `Imaging.Baker`/`BakeLayer` (checked against the DLL; the fork dropped System.Drawing-era imaging, see `Docs/OPENMETAVERSE_SYSTEM_DRAWING_SPIKE.md`). The reference implementation exists in libomv (`/d/libomv-src/OpenMetaverse/Imaging/BakeLayer.cs`, 672 lines, BSD-3) and in LibreMetaverse 3.1.4 (`LibreMetaverse.Imaging.Baker`) | +| Inventory/COF read from the sim | yes | `AvatarFactoryModule.cs:839-1116` via `IInventoryService` | +| Bake asset serving to viewers | partly | `GetTextureHandler.cs:142-163` serves any `AssetType.Texture` from the asset service; the SL bake URL shape (R4) is a different route that does not exist | +| Login-response field | no | `Source/OpenSim.Services.LLLoginService/LLLoginResponse.cs:486-497` has no `agent_appearance_service` | +| AIS v3 | **no** | `Source/OpenSim.Services.AISv3` contains only a `WeatherForecast` scaffold | + +## R7. Halcyon lineage: persistent grid-side bake cache, no compositor + +Halcyon is client-bake with a grid-wide cache; it never composes either. + +- **Upload persists.** `OpenSim/Region/CoreModules/Capabilities/AssetCapsModule.cs:206-209` registers `UploadBakedTexture`; `:387-420` stores the upload as a `Local = true` asset with `Temporary` deliberately *not* set ("Persist baked textures as we will use them in the baked texture cache", `:412-416`). +- **Cache is keyed by the viewer's cache id and lives in the user database.** `ScenePresence.SetAppearance` (`OpenSim/Region/Framework/Scenes/ScenePresence.cs:3546-3600`) builds `cacheId → textureId` from the `WearableData` blocks (`:3552-3563`, note the V1/V2 index conversion), sets `Serial` from the COF version (`:3566`) and hands both to `IAvatarFactory.UpdateDatabase` (`OpenSim/Region/Framework/Interfaces/IAvatarFactory.cs`, two methods). `AvatarFactoryModule.UpdateDatabase` (`OpenSim/Region/CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs:656-743`) coalesces updates for 3 s, refuses appearances with a zeroed required wearable, then calls `AvatarService.UpdateUserAppearance` and `SetCachedBakedTextures` (`:737-738`). The user server exposes `get_cached_baked_textures` / `set_cached_baked_textures` over XML-RPC (`OpenSim/Grid/UserServer.Modules/UserServerAvatarAppearanceModule.cs:76-77`, `:267-310`) backed by a `cachedbakedtextures (cache, texture)` table (`OpenSim/Data/MySQL/MySQLUserData.cs:1565-1610`). +- **Cache hits answer `AgentCachedTexture` from the grid**, not the region: `AvatarFactoryModule.cs:387-410` (`_cacheBakedTexturesEnabled`, `:298`, `:369-370`), zero-filling indexes the grid does not know so the viewer rebakes only those. +- **COF building for V1 viewers.** `BuildCOF` (`:58-170`) and `AvatarIsWearing` (`:535-650`) synthesise Current Outfit links for viewers that cannot manage their own COF — solved a 2011 problem, irrelevant to modern viewers. +- **No compositor.** A grep for `Oven`, `BakeLayer`, `Composite`, `ManagedImage` outside `ThirdParty/` hits only unrelated files; the only baker in the tree is libomv's client-side `ThirdParty/libopenmetaverse/OpenMetaverse/Imaging/BakeLayer.cs` and a desktop `Programs/Baker` tool. + +**Assessment.** Halcyon's design is a better *client-bake* experience than stock OpenSim's XBakes (cross-region and cross-login cache hits, so Firestorm users rebake less; bakes survive restarts because they are stored as non-temporary assets), but it does nothing for a viewer that will not bake. Porting it into Tranquillity would mean: a Robust `cachedbakedtextures` service (XBakes already stores by agent, not by cache id), a persistent-asset flag on `UploadBakedTexture`, and answering `AgentCachedTexture` from the grid. All of that is orthogonal to SSB and helps only Firestorm-legacy users. It does not move either gate in the brief. + +## R8. Stock-OpenSim lineage: no SSB either; XBakes is a cache of viewer bakes + +`grep UpdateAvatarAppearance` over stock 0.9.3 returns nothing; there is no appearance service and no compositor upstream. What upstream *did* build is exactly what Tranquillity carries: `UploadBakedTexture` (temporary/local storage), `XBakes` (Robust file store keyed by agent, `XBakes.cs:56-107`, region client `XBakesModule.cs`), `ValidateBakedTextureCache`/`RequestRebake`, bit 63 and `BakesOnMeshEnabled` for eleven-slot BOM, and the hard-coded `RegionProtocols` value with the comment that bit 0 "signals server side texture baking" (`LLClientView.cs:993`). Upstream's intent, as far as the tree shows it, is to *support viewers that bake* and to *refuse to advertise* SSB. "Porting or completing stock SSB" therefore has no source to port; it means writing the appearance service from the viewer contract (R4). + +## R9. The web viewer's gateway library already speaks the SL SSB client contract + +LibreMetaverse 3.1.4 (the `web-viewer` gateway's dependency) exports `AppearanceManager.UpdateAvatarAppearanceAsync(CancellationToken, int cofVersion)`, `AssetManager.RequestServerBakedImageAsync(UUID avatarId, UUID textureId, string bakeName, …)`, `NetworkManager.AgentAppearanceServiceURL` (populated from the login reply) and `RegionProtocols.AgentAppearanceService` (bit 0), plus `ImageType.ServerBaked`. It also still ships the client-side `LibreMetaverse.Imaging.Baker` (512×512 / 128×128 eyes) with the `content/linden/character` resources. + +Consequence: if Tranquillity implements the *SL* contract (cap + login field + bake URL route + `AppearanceVersion = 1`), the gateway needs no protocol work to dress avatars — it reads the bit, asks for a bake for the agent it is logged in as, receives `AvatarAppearance` for everyone, and fetches bakes by the same URL a viewer uses. A *non-SL* design (bakes only reachable through `GetTexture`, no appearance-service URL) would still work for the gateway if bakes are stored as ordinary texture assets, but would not work for the LL viewer at all (R3/R4). This is the strongest argument for keeping the wire contract SL-exact. + +## R10. Who bakes NPCs today + +`Source/OpenSim.Region.OptionalModules/World/NPC/NPCModule.cs:135` still carries upstream's "We can't just use IAvatarFactoryModule.SetAppearance() yet". NPCs are created from a stored `AvatarAppearance` whose TextureEntry points at baked textures that *some viewer* uploaded earlier; with `PersistBakedTextures = false` those assets are temporary, so NPC bodies survive only as long as the region's asset cache does. SSB with persistent bakes fixes NPC appearance as a side effect (a server bake is reproducible from wearables at any time). Relevant to the bot/NPC track (`feature/bot-npc-framework`). + +--- + +## A. The architectural fork + +### A.1 Option (a) — stock-OpenSim-style SSB + +There is nothing to port (R8); "stock-style" means: a region-side compositor + an `UpdateAvatarAppearance` cap + storing bakes through the existing asset service, with `XBakes` optionally kept as the persistent store. Work items: compositor (§B), the cap module, the bake-URL route, the login field, `AppearanceData` in `SendAppearance`, per-region protocol bit. Fidelity is whatever the compositor achieves. Serves the LL viewer and the gateway equally because it is the SL contract. + +### A.2 Option (b) — Halcyon/InWorldz-lineage server-side appearance + +Also has nothing that bakes (R7). Its transferable ideas are storage-side: persist bakes as real assets, key a grid cache by cache id, coalesce appearance saves, answer `AgentCachedTexture` from the grid. Porting means new Robust surface and database tables that serve *client-baking* viewers only. It does not open either gate and would still need everything in (a) to do so. + +### A.3 Where each is better or worse + +| Criterion | (a) SL-contract SSB on Tranquillity | (b) Halcyon-style cache | +|---|---|---| +| LL viewer shows a body | yes, once the compositor works | no | +| Web viewer shows a body | yes, no gateway protocol work (R9) | no (still needs a bake to exist) | +| Firestorm legacy users | unchanged until the region advertises bit 0; then Firestorm switches to SSB like LL (`llagent.cpp:6369-6382`) | fewer rebakes, bakes persist | +| Fidelity | bounded by the compositor (§B) — the viewer's own bake is the reference and will look slightly different | pixel-identical (it is the viewer's bake) | +| Effort | compositor + cap + route + login field + storage + rollout gating | grid service + table + cap change + cache answers | +| Risk | rollout (§D); compositor correctness | low; none of the risks in the brief | +| Persistence | bakes are reproducible; can be re-baked on demand | bakes persist but cannot be regenerated | + +### A.4 Recommendation: **(a), as an SL-contract appearance service inside the region, with (b)'s persistence rule adopted as storage policy.** + +Reasoning, tied to the brief's three criteria: + +- **Fidelity.** Only a compositor puts a body on a viewer that will not bake; (b) has none, so fidelity for the two gated viewers is zero under (b). Under (a) the achievable fidelity is "close to a viewer bake" (§B.4 lists the known gaps), and the gaps are in the compositor, which can be iterated without touching the protocol. +- **Effort.** (b) is cheaper but buys nothing against either gate; (a)'s protocol surface is small and fully specified by the viewer source (R4, seven concrete points). The compositor is the only sizeable unknown, and a BSD-3 reference implementation exists to port (R6). +- **Serving both viewers.** The SL contract is the *only* one both consumers implement today: the LL viewer (R4) and LibreMetaverse (R9). Any Tranquillity-specific variant would need custom code in the gateway and would never work for the LL viewer. + +From (b) keep one rule: **bakes are stored as persistent, regenerable texture assets** (not `Temporary`), so `GetTexture`, `XBakes`, NPCs and the gateway all see them, and a restart does not cloud everyone. + +**What would change this recommendation.** (1) If the compositor's fidelity proves unacceptable on real content (mesh bodies with BOM rely on the skin/tattoo/alpha bakes being right) *and* no better compositor can be sourced, the fallback is to keep the region legacy (bit 0 clear, `AppearanceVersion = 0`) and accept that the LL viewer stays unsupported — the web viewer could then bake in its own gateway with LibreMetaverse's `Baker` instead. (2) If a maintained third-party SSB service surfaces that Tranquillity could proxy the cap to (`caps.RegisterHandler("UpdateAvatarAppearance", url)` is the existing pattern for remote caps, `UploadBakedTextureModule.cs:108-109`), the in-region compositor becomes optional. Neither exists on this machine or in these trees today. + +--- + +## B. The compositor + +### B.1 Inputs + +Per avatar, from the sim's own data: the COF (links → wearable items → assets, exactly what `SetAppearanceAssets` walks, `AvatarFactoryModule.cs:839-1066`), each wearable's `LLWearable` asset (parameters + per-slot texture ids; parsed by `AssetWearable` from the linked libomv), the visual-param definitions and alpha-mask TGAs from `openmetaverse_data/avatar_lad.xml`, and the wearable textures via the asset service (J2K decode through CoreJ2K). + +### B.2 Layer model (what the reference does, `libomv-src/OpenMetaverse/Imaging/BakeLayer.cs`) + +Per bake type: canvas 512×512 (128×128 eyes) initialised to the base colour (`:118-125`); skin/body-paint and tattoo layers pulled out for special ordering on the head bake (`:130-150`, `:183-187`); built-in base layers `head_color.tga` / `upperbody_color.tga` / `lowerbody_color.tga`, head alpha and skin-grain multiply (`:153-167`); then each clothing texture in slot order, resized to the bake (nearest-neighbour, `:192-197`, with a `FIXME` to tile instead), tinted with the wearable colour (`ApplyTint`, `:222`, `:579`), masked by the wearable's alpha params (`VisualAlphaParam`, multiply vs non-multiply blends, `:239-270`), drawn with source alpha only for skirt/hair layer 0 (`:292-293`); finally the hair layer of the head bake multiplied by `head_hair.tga` (`:203-210`). `AppearanceManager.DecodeWearableParams` (`AppearanceManager.cs:1376-1478`) is the piece that turns a wearable's parameters into `AlphaMasks` and colour info — it must come across with the baker. + +### B.3 Library choice + +SkiaSharp is sufficient for every operation the bake needs — `SKCanvas.DrawBitmap` with `SKBlendMode.SrcOver`/`Multiply`/`DstIn` for layers, masks and skin grain, `SKColorFilter.CreateBlendMode` for tint, `SKBitmap.Resize` (bilinear or better, an improvement on the reference's nearest-neighbour) — and it is already loaded in the region process. The recommended shape is: **port `BakeLayer.cs` + `ManagedImage.cs` + `TGALoader.cs` (BSD-3, attribution header as done for PrimMesher in the web viewer) as the algorithm, replacing `ManagedImage` per-pixel loops with `SKBitmap` operations where they are the same operation**, decode/encode through `CoreJ2K` (`SkiaImageUtils.TryEncodeToJ2KLossless` exists; add a lossy preset at quality comparable to viewer uploads). Do *not* take a dependency on the LibreMetaverse NuGet inside the region: it would load a second copy of every type in the `LibreMetaverse` namespace next to `UtopiaSkye.OpenMetaverse` and the two `avatar_lad.xml` loaders would fight over `openmetaverse_data`. + +### B.4 Known fidelity gaps to plan for (the reference is a bot baker, not the viewer's `LLTexLayer`) + +1. **Resolution.** Reference bakes at 512 (viewer default for "medium/high" is also 512; 1024 is a viewer option). Start at 512. +2. **Resize.** Nearest-neighbour; use bilinear. The `FIXME: tile` case (texture smaller than the bake) is real for old content. +3. **Morph-driven masks.** The viewer evaluates alpha masks against the avatar's shape parameters; the reference applies `VisualAlphaParam` weights from the *wearable's* parameters only. Expect slight seam/length differences on gloves, sleeves, skirt length. +4. **Eleven-slot bakes.** The reference `BakeType` covers the six classic bakes; left-arm/left-leg/aux1–3 (BOM universal wearables) need adding — the layer rules for those are in the viewer's `avatar_lad.xml` (`bake` attributes) and are the same mechanism. +5. **Bakes-on-Mesh.** BOM does not change compositing — a BOM mesh body samples the *same* baked textures at the slots bit 63 already advertises. What BOM does change is *visibility of errors*: a mesh body shows the whole skin/tattoo/alpha bake, so gaps 1–3 are more visible than on the system body. The alpha-wearable layers (`LowerAlpha`…`HairAlpha`, skipped as colour layers at `:177-181` but applied as masks) matter most here. +6. **Materials/PBR.** Out of scope; bakes are diffuse only, as in SL. + +### B.5 Storage and serving + +- Bake output: J2K, stored through the asset service as `AssetType.Texture`, **not temporary**, creator = the avatar, name `Baked `, with a deterministic *cache key* recorded per avatar: `(avatarId, slot) → (textureId, cofVersion, inputHash)` where `inputHash` covers the wearable asset ids, colours and parameters that feed that slot. Re-baking a slot whose `inputHash` is unchanged is a no-op (the equivalent of the viewer's `AgentCachedTexture`). +- Serving: (1) the SL route `GET texture///` — a region-hosted HTTP handler that resolves `` through the asset service and answers `image/x-j2c` (Range requests welcome; the viewer's `FTT_SERVER_BAKE` uses the same fetcher as `GetTexture`); (2) `GetTexture` continues to serve the same asset id, which is what LibreMetaverse's normal texture pipeline and any Firestorm user in legacy mode will hit. `agent_appearance_service` should point at a grid-level URL that reverse-proxies to "the region the avatar is in", or, simpler for a single-host grid like this one, at a Robust handler that serves from the asset service directly — the asset is the same either way. Decision D-2. +- Old bakes: keep the last N per avatar/slot (viewers cache by texture id; a changed bake must have a new id) and let a sweeper delete assets that are no longer referenced by any `(avatarId, slot)` record. + +### B.6 Where it runs + +An `ISharedRegionModule` (`AppearanceBakeModule`) owning: the cap handler, a per-avatar bake queue (one bake job at a time per avatar, latest `cof_version` wins), the compositor, the cache-key table (SQLite/MySQL through the existing data layer; a region-local table is enough for milestone 1), and the bake URL route. A bake of six 512² slots from already-cached textures is tens of milliseconds of raster work plus J2K encode; the dominant cost is fetching wearable textures on first use. Concurrency limit per region (say 2 bakes in flight) protects the sim thread. + +--- + +## C. Wire changes, in one list + +| Area | Change | Site | +|---|---|---| +| Handshake | `RegionProtocols` bit 0 from a per-region flag, not a constant | `LLClientView.cs:995` | +| `AvatarAppearance` | emit `AppearanceData { AppearanceVersion, CofVersion }` (1 block): version 1 + COF version for server-baked avatars, version 0 otherwise | `LLClientView.cs:4532-4533`, callers `ScenePresence.cs:4338-4347` | +| Cap | `UpdateAvatarAppearance` (POST LLSD `cof_version` → `{ success, cof_version }` or `{ success:false, error, expected }`) | new module, pattern `UploadBakedTextureModule.cs:97-112` | +| Login | `agent_appearance_service` in the login response | `LLLoginResponse.cs:486-497` (+ `[LoginService]` config) | +| Bake route | `texture///` | new handler (region and/or Robust) | +| Visual params | param 11000 ("appearance version") set to 1 in the broadcast params for server-baked avatars (the viewer cross-checks it, `llvoavatar.cpp:10851-10863`) | `AvatarAppearance.SetVisualParams` consumers | +| Storage | bake assets persistent; cache-key table | new | +| Legacy path | `AgentSetAppearance` / `UploadBakedTexture` keep working (Firestorm legacy, bots); when bit 0 is set for a region, an incoming `AgentSetAppearance` from a viewer is accepted but the server bake wins for the broadcast | `AvatarFactoryModule.cs:168-215` | + +--- + +## D. Rollout hazards and the safe rollout + +**The hazard, precisely.** The moment a region's handshake carries bit 0, every viewer connecting to it (LL *and* Firestorm — `llagent.cpp:6369-6382` moves a legacy avatar to server bakes on entering a CBV>0 region) stops baking locally and asks the cap. If the cap or compositor fails, *every* avatar in that region is a cloud, including Firestorm users who were fine. There is no viewer-side fallback: `checkForUnsupportedServerBakeAppearance` only fires the other way (server-baked avatar entering a legacy region, `llvoavatarself.cpp:3636-3657`). + +**Gating design.** + +1. **Three switches, all default off.** `[Appearance] ServerSideBaking = false` (grid default in `OpenSimDefaults.ini`); per-region override in the region's own ini (`ServerSideBaking = true` under `[Appearance]` scoped by region name, using the existing `Region_` override convention); and a **runtime console command** `appearance ssb on|off` that flips the advertised bit for *new* handshakes without a restart (existing sessions keep their mode until they re-enter). The compositor module always loads; only the *advertisement* is gated. +2. **Bit 0 is advertised only when the module reports ready:** compositor self-test passed at startup (bake the default avatar from the library wearables and decode the result), the bake route answers a probe, and the login field is configured. If any check fails the region logs why and stays legacy even with the flag on. +3. **Dual-mode regions are legitimate.** With bit 0 clear the region still *accepts* `UpdateAvatarAppearance` requests (a viewer would not send them, but LibreMetaverse can be told to), which is how the web viewer and NPC re-baking can be exercised on a legacy region before any human-facing region flips. This is the key to testing on the live grid without touching Firestorm users. +4. **Fallback for a failed bake.** The cap answers `{ success:false, error:"…" }`; the viewer logs and retries with back-off (R4). The sim keeps broadcasting the *previous* good bake for that avatar (never the default cloud TE) and re-queues. If no bake has ever succeeded for an avatar, the broadcast falls back to the wearables' own textures where a slot has one (skin, eyes) — imperfect, visibly better than a cloud. +5. **Per-region enable order on the live grid:** (a) a new, empty test region (`SSB-Test`) with the flag on; verify with the web viewer, then an LL viewer, then Firestorm; (b) Elm or Transylvania off-peak with the console switch, watched, revert with the same command; (c) Ebony last; (d) grid default only after every region has run it. +6. **What to watch.** Per region: bake queue depth, bake failures per avatar, bake latency p95, bake-route 404s (a 404 means an `AvatarAppearance` referenced an id the store lost), and `AgentSetAppearance` arrivals on an SSB region (a viewer that has not switched). + +**How to test without breaking the live grid.** Milestone 1 (§F) runs entirely on a legacy-advertised region: the gateway asks the cap directly, the resulting `AvatarAppearance` (version 1) is broadcast — *and here is the one live-grid caveat*: Firestorm users in the same region would receive that version-1 appearance for the test avatar and fetch its bakes from the appearance service, which must therefore already be reachable. Do milestone 1 on the `SSB-Test` region only, or during a window with no other users on the region. + +--- + +## E. Dependencies and ordering + +- **Track-L order (login benefits → AgentProfile → AIS → SSB), as given in the brief.** No Track-L document was found in the trees on this machine (`Docs/feature/sl-parity-audit/` is not present in `/d/tranquillity-develop`, `/d/tranquillity-hypergrid` or the other checkouts; the closest house documents are `Docs/feature/trusted-hypergrid/` in `tranquillity-hypergrid` and `Docs/voice/`). The order is taken as accepted and recorded as ledger A-1. +- **AIS v3 is not a hard dependency.** The sim reads the COF and wearables through `IInventoryService` today (R2 step 4) and can read the COF folder's version the same way; AIS v3 is the *viewer's* HTTP inventory path. Without AIS the viewer still maintains its COF through legacy UDP inventory ops and the `FetchInventory2`/`FetchInventoryDescendents2` caps (both registered, R1), and the COF folder version still increments on the inventory service. What AIS buys SSB is *tighter agreement on `cof_version`* (the viewer comments in `llvoavatar.cpp:10936-10937` say the canonical COF version is "maintained by the AIS code"); without it expect more `expected`-mismatch retries after fast outfit changes. SSB can proceed independently; the retry loop is the viewer's own mitigation. +- **Login benefits / AgentProfile** have no code coupling to SSB other than the login response being touched for `agent_appearance_service` (one field; coordinate the edit). +- **Not built yet and needed:** the compositor (R6), the cap, the bake route, the login field, per-region flag, `AppearanceData` emission, bake persistence policy, a cache-key store. **Present and reusable:** J2K encode/decode, SkiaSharp, wearable parser, `avatar_lad.xml` + masks on disk, `XBakes` (optional persistent store), `GetTexture`, inventory reads. +- **Bots/NPC track** benefits (R10) and should consume the same bake service rather than its own path. + +--- + +## F. Build-plan skeleton (sizing only; not a BP) + +| # | Milestone | Content | Done when | +|---|---|---|---| +| 0 | Hygiene (independent, ship first) | emit `AppearanceData { 0, cof }` explicitly; per-region `RegionProtocols` from config (still 0); `agent_appearance_service` field plumbing behind a flag | LL viewer no longer logs "AgentAppearanceServiceURL not set" for legacy avatars; nothing else changes for Firestorm | +| 1 | **First demoable: one avatar dressed in both viewers** | port compositor (six classic bakes, 512²) into `AppearanceBakeModule`; `UpdateAvatarAppearance` cap; bakes stored persistent; bake route on the region; version-1 broadcast for baked avatars; console switch; `SSB-Test` region flagged | Truly logs in on `SSB-Test` with a system-body outfit: a stock LL viewer shows her dressed (no cloud, no orange), and the web viewer's gateway (LibreMetaverse) fetches the same six bakes and renders them on its avatar. Evidence: cap request/response log, six bake assets, bake-route 200s from both clients, viewer log free of bake fetch failures | +| 2 | Fidelity + BOM slots | eleven-slot bakes (left arm/leg, aux1–3), bilinear resize, tiling, alpha-wearable correctness on a BOM mesh body; side-by-side comparison against Firestorm's own bake of the same outfit | a BOM mesh body looks the same to within an agreed visual tolerance in LL and Firestorm-legacy | +| 3 | Robustness | failed-bake fallback, previous-good retention, `expected` COF handling, queue limits, sweeper, metrics, `appearance ssb` console surface | soak on `SSB-Test` with outfit churn; no cloud regressions | +| 4 | Persistence + grid | Robust-side bake route (or reverse proxy), `XBakes`/asset-service policy, NPC re-bake on rez, HG considerations (foreign avatars carry their own bakes; foreign regions may be legacy) | an avatar TPs between an SSB and a legacy region and back without clouding in either | +| 5 | Rollout | per-region enable per §D.5; Firestorm users observed; grid default flip | all regions SSB; `AgentSetAppearance` arrivals ≈ 0 | + +Dependencies inside the skeleton: 0 → 1 → 2/3 (parallel) → 4 → 5. Milestone 1 is the one to size first; its unknown is the compositor port, everything else is protocol plumbing with the viewer source as the spec. + +--- + +## G. Risk register + +| ID | Risk | Likelihood | Impact | Mitigation | +|---|---|---|---|---| +| RK-1 | Advertising bit 0 before the compositor is reliable clouds every avatar in the region, Firestorm included | high if unmanaged | high | §D gating: default off, readiness checks, console switch, test region first | +| RK-2 | Compositor fidelity below user expectation on BOM mesh bodies | medium | medium | milestone 2 comparison suite; keep per-region legacy as the fallback | +| RK-3 | `cof_version` drift without AIS causes retry storms after rapid outfit changes | medium | low–medium | honour `expected`; coalesce bake jobs per avatar; measure on `SSB-Test` | +| RK-4 | Bake assets bloat the asset store (a new id per rebake) | medium | medium | cache-key no-op rebakes; retention of last N; sweeper | +| RK-5 | Two libomv copies in one process (if LibreMetaverse were referenced for its Baker) | certain if done | medium | port the BSD code instead (§B.3) | +| RK-6 | Bake route reachability for foreign (HG) visitors and for the web gateway's host | medium | medium | grid-level `agent_appearance_service`; same asset served by `GetTexture` | +| RK-7 | Wearable assets missing/undecodable for old outfits | medium | low | per-slot fallback to base layers; log and continue | +| RK-8 | Login-response edit collides with the login-benefits work in Track-L | low | low | one field, coordinate | diff --git a/Docs/feature/ssb-appearance/S0a-VERIFICATION.md b/Docs/feature/ssb-appearance/S0a-VERIFICATION.md new file mode 100644 index 00000000000..c8a6042fc73 --- /dev/null +++ b/Docs/feature/ssb-appearance/S0a-VERIFICATION.md @@ -0,0 +1,76 @@ +# S0a — Verification pass + +Tree: `D:\tranq-ssb`, branch `feature/ssb-appearance`, HEAD `0f441a03df` (on deployed `db7c746248`). +Upstream comparison target: `upstream/develop` = `ee71b6951b`; merge-base with HEAD = `93765a999e`. +Method: rg/grep to locate, then the cited site was opened and read. Line numbers are at HEAD. + +Path correction versus the session brief: the UDP client stack lives at +`Source/OpenSim.Region.ClientStack.LindenUDP/` (no dot between `Linden` and `UDP`). +All LLClientView citations below use that path. + +## Summary table + +| Item | Verdict | file:line | Reasoning | +|---|---|---|---| +| V1 | **Yes, still count 0**; hover written as 1 block `(0, 0, hover)` | `Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs:4499` (method), `:4531-4532` (`// no AppearanceData` / `data[pos++] = 0;`), `:4533-4537` (AppearanceHover) | `SendAppearance` hand-packs the packet; the AppearanceData block count byte is a literal 0, and AppearanceHover is a literal count 1 followed by `Utils.FloatToBytesSafepos(0/0/hover)`. Line moved from 4521 to 4531 since 645b0f3. | +| V2 | **`1UL << 63` only**, bit 0 clear | `LLClientView.cs:995` inside `SendRegionHandshake()` (`:886`) | Exact expression: `zc.AddUInt64(1UL << 63);` preceded by the comments `// bit 0 signals server side texture baking` / `// bit 63 signals more than 6 baked textures support"`. No other write to RegionProtocols in the file. | +| V3 | **No hits** | — | `grep -rn 'UpdateAvatarAppearance\|agent_appearance_service'` over `Source/` and `Addons/` (`*.cs`, `*.ini*`, `*.csproj`, `*.xml`, excluding `bin/`, `obj/`, `AISv3`) returns nothing. Neither the cap nor the login-response key exists anywhere in the tree. | +| V4 | **Yes, both sides present (stock XBakes)**; stores a per-agent XML blob of `WearableCacheItem[]` including the full baked `AssetBase` | Region: `Source/OpenSim.Region.CoreModules/Avatar/BakedTextures/XBakesModule.cs:44` (`INonSharedRegionModule, IBakedTextureModule`), `:57` (config section `[XBakes]`, key `URL`), `:184-231` (serialises `{AssetBase}…`, indices >26 as ``), `:238-241` (POST `bakes/`). Robust: `Source/OpenSim.Server.Handlers/BakedTextures/XBakes.cs:37` (`ServiceBase, IBakedTextureService`), `:56` (`BaseDirectory`), `:82-90` (`Store` writes the raw POST body to `/aa/bb/cc/`), `:66-73` (`Get` reads it back). Connector: `Source/OpenSim.Server.Handlers/BakedTextures/XBakesHandler.cs:38` (`XBakesConnector`), `:62` (`/bakes` handler). Interface: `Source/OpenSim.Services.Interfaces/IBakedTextureService.cs`. Config: `Source/OpenSim.Server.GridServer/AppData/Robust.ini.example:130` (ServiceList entry, commented out by default), `:662-665` (`[BakedTextureService]` section). | Region module is a thin REST client; the Robust service is an opaque file store keyed by agent id. It stores the viewer-uploaded bakes (the whole `AssetBase`, base64 in XML), not wearables or params. It is a cache of client-side bakes, not a bake producer, so it does not conflict with ADR-004's asset-service persistence; it is a separate, optional, off-by-default path. | +| V5 | **Wipe pattern present — NOT fixed.** Starts from an empty wearable array and fills only what the viewer sent. **Hard gate open.** | `Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs:1205` — `AvatarAppearance avatAppearance = new AvatarAppearance(sp.Appearance, false);` → `Source/OpenSim.Framework/AvatarAppearance.cs:187` (`copyWearables=false`) → `:192` ctor → `:213-225` (`if (copyWearables …) {copy} else ClearWearables();`) → `:274-276` (`m_wearables = new AvatarWearable[LEGACY_VERSION_MAX_WEARABLES]`, all empty). Then `AvatarFactoryModule.cs:1207-1219` adds only `e.NowWearing`, `:1221` `GetAssetsFrom(sp.Appearance)` resolves asset ids only for items present, `:1228` `sp.Appearance.Wearables = avatAppearance.Wearables;` replaces the whole array, `:1231` `QueueAppearanceSave`. | Any `AgentIsNowWearing` that lists fewer than all worn types clears the unlisted slots and persists the result. This is stock OpenSim behaviour, and the Legion fix has not been ported to Tranquillity. Must be fixed before any server-initiated bake reads stored wearables. | +| V6 | **Yes on all three paths.** Service layer never increments; the data layer does, on every item store, delete and move and on folder store/move. Link create, item delete, item move and cap folder create all bump the parent folder's `version`. | Service: `Source/OpenSim.Services.InventoryService/XInventoryService.cs` has **no** increment site — only `:183` (`version = 1` on system-folder creation) and `:410-445` (`UpdateFolder` monotonic guard that refuses a lower version on typed folders). Data layer (MySQL; PGSQL and SQLite mirror it): `Source/OpenSim.Data.MySQL/MySQLXInventoryData.cs:239-244` (`Store(XInventoryItem)` → `IncrementFolderVersion(item.parentFolderID)`), `:148-162` (`Delete(string[],string[])` → increment each distinct parent), `:133-137` (`Delete(string,string)` delegates to the generic `:341-344`, which virtual-dispatches back to `:148`), `:167-188` (`MoveItem` → old and new parent), `:283-288` (`Store(XInventoryFolder)` → parent), `:258-278` (`MoveFolder` → both parents), `:303-316` (`update inventoryfolders set version=version+1`). PGSQL: `Source/OpenSim.Data.PGSQL/PGSQLXInventoryData.cs:158-159,232,276-277,287`. SQLite: `Source/OpenSim.Data.SQLite/SQLiteXInventoryData.cs:143,177,200-201,258,282-283`. | See traces below. | +| V7 | **CoreJ2K.Skia 2.3.3.91** (plain NuGet package, `nuget.org`), API `J2kImage.ToBytes(SKBitmap, J2KEncoderConfiguration)` / `SKBitmap.EncodeToJ2K(cfg)`. No OpenJPEG/CSJ2K code in the tree. | Encode sites: `Source/OpenSim.Framework/SkiaImageUtils.cs:27` (config `new J2KEncoderConfiguration().WithLossless().WithFileFormat(true)`), `:52` (`workingImage.EncodeToJ2K(encoderConfiguration)`); `Source/OpenSim.Region.CoreModules/World/LegacyMap/MapImageModule.cs:166` (`J2kImage.ToBytes(skBitmap, encoderConfig)`); `Source/OpenSim.Region.CoreModules/Scripting/VectorRender/VectorRenderModule.cs:410`. Decode: `Source/OpenSim.Region.CoreModules/Agent/TextureSender/J2KDecoderModule.cs:178`, `Source/OpenSim.Capabilities.Handlers/GetTexture/GetTextureRobustHandler.cs:324`. Package refs (no CPM, per-csproj): `Source/OpenSim.Framework/OpenSim.Framework.csproj:20`, `OpenSim.Capabilities.Handlers.csproj:27`, `OpenSim.Region.CoreModules.csproj:42`, `OpenSim.Region.Framework.csproj:31`, `OpenSim.Region.PhysicsModules.Meshing.csproj:11`, `OpenSim.Region.PhysicsModules.ubODEMeshing.csproj:24`, `OpenSim.Services.Connectors.csproj:28`, `OpenSim.Services.MapImageService.csproj:24`, `Warp3D.csproj:12`, all `Version="2.3.3.91"`. The only "OpenJPEG" string is a comment at `GetTextureRobustHandler.cs:329`. NuGet cache holds `corej2k.skia/2.3.3.91` and `2.1.2.38`. | A new library under `Source/` can `PackageReference` CoreJ2K.Skia directly with no region dependency; the plumbing in `OpenSim.Framework/SkiaImageUtils.cs` is a convenience, not a requirement. Note from V10: upstream #201 moved map tiles from fixed 256×256 J2K tiles to single-tile (`WithTiles(t => t.SetSize(width, height))`) because multi-tile output rendered blank in viewers; bake output should use the same single-tile setting. | +| V8 | **SkiaSharp 4.151.1**, per-csproj `PackageReference` (no `Directory.Packages.props`, so no central package management). Native assets only in the two host projects. | `Source/OpenSim.Framework/OpenSim.Framework.csproj:19`, `OpenSim.Capabilities.Handlers.csproj:26`, `OpenSim.Region.CoreModules.csproj:43`, `OpenSim.Region.Framework.csproj:30`, `OpenSim.Region.PhysicsModules.Meshing.csproj:12`, `OpenSim.Region.PhysicsModules.ubODEMeshing.csproj:25`, `OpenSim.Services.Connectors.csproj:27`, `OpenSim.Services.MapImageService.csproj:23`, `Warp3D.csproj:10`, `OpenSim.Server.RegionServer.csproj:86-88` and `OpenSim.Server.GridServer.csproj:62-64` (the latter two also `SkiaSharp.NativeAssets.Win32` / `.Linux` 4.151.1). NuGet cache also has 3.119.x and 4.150.1 from earlier builds. | Version is uniform at 4.151.1 across the tree; the library pins the same. | +| V9 | Pattern: `[ServiceList]` ini entry → `GridServiceConnectorLoader` → `ServerUtils.LoadPlugin(dll:Class, {config, server, configName})` → connector ctor loads the service via `LocalServiceModule`, builds `ServiceAuth`, calls `server.AddSimpleStreamHandler(new XHandler(...))`; the handler subclasses `SimpleStreamHandler("/path", auth)` and overrides `ProcessRequest`. | Loader: `Source/OpenSim.Server.GridServer/GridServiceConnectorLoader.cs:47-61` (reads `[Startup] ServiceConnectors` plus every key in `[ServiceList]`), `:64-90` (parses `[configName@]port/Dll.dll:Class`), `:110-118` (`ServerUtils.LoadPlugin(conn, new object[]{config, server, configName})`, falls back to `{config, server}`). LoadPlugin: `Source/OpenSim.Server.Base/ServerUtils.cs:186,234`. Base class: `Source/OpenSim.Server.Handlers/Base/ServerConnector.cs:38` (`ServiceConnector : IServiceConnector`). Worked example: `Source/OpenSim.Server.Handlers/BakedTextures/XBakesHandler.cs:38-63` (connector), `:66-77` (`BakesServerHandler : SimpleStreamHandler`, base `("/bakes", auth)`, `ProcessRequest(IOSHttpRequest, IOSHttpResponse)`), `:97-124` (GET/POST, `SplitParams(httpRequest.UriPath)`). Ini: `Source/OpenSim.Server.GridServer/AppData/Robust.ini.example:130` (`BakedTextureService = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:XBakesConnector"`) and `:662-665`. | S4 copies `XBakesConnector` + `BakesServerHandler` verbatim in shape: one connector class per service, one `SimpleStreamHandler` per URL prefix, `ServiceAuth.Create(config, sectionName)` for auth, and a `[ServiceList]` line plus a `[SectionName]` block with `LocalServiceModule`. | +| V10 | See "Upstream delta" below. libopenmetaverse: HEAD `1.1.6-ga897ffefd7`, upstream `1.1.7`. | `Directory.Build.props:13-17` (HEAD) vs `upstream/develop:Directory.Build.props:13-17`. | Only 7 of the ~25 files/dirs in scope differ; none of the appearance, inventory-version or AvatarFactory files differ at all. | + +## V6 — path traces + +**UDP `LinkInventoryItem` (link create):** +`LLClientView.cs:8473` (dispatch table) → `:10294` `HandleLinkInventoryItem` → `:10297` `OnLinkInventoryItem` → +`Source/OpenSim.Region.Framework/Scenes/Scene.cs:3460` (`client.OnLinkInventoryItem += HandleLinkInventoryItem`) → +`Source/OpenSim.Region.Framework/Scenes/Scene.Inventory.cs:1224` `HandleLinkInventoryItem` → `:1257-1262` `CreateNewInventoryItem(...)` → `:1195` `AddInventoryItem(item)` → `:123-125` `InventoryService.AddItem(item)` → +`XInventoryService.cs:507-512` `AddItem` → `m_Database.StoreItem(...)` → +`MySQLXInventoryData.cs:239-244` `Store(XInventoryItem)` → `IncrementFolderVersion(item.parentFolderID)`. +**Parent folder version is bumped on link create.** + +**UDP `RemoveInventoryItem` (item delete, including link delete):** +`LLClientView.cs:8481` → `:10412` `HandleRemoveInventoryItem` → `:10423` `OnRemoveInventoryItem` → +`Scene.cs:3471` → `Scene.Inventory.cs:1275` `RemoveInventoryItem` → `:1282` `InventoryService.DeleteItems(...)` → +`XInventoryService.cs:579-607` `DeleteItems` (both the `m_AllowDelete=false` link-only branch `:589-595` and the plain branch `:604` call `m_Database.DeleteItems`) → +`MySQLXInventoryData.cs:133` `Delete(string,string)` → `base.Delete` = `MySQLGenericTableHandler.cs:341-344` → virtual `Delete(string[],string[])` = `MySQLXInventoryData.cs:148-162` → `IncrementFolderVersion` for every distinct parent of the deleted rows. +**Parent folder version is bumped on item delete.** + +**UDP `MoveInventoryItem`:** +`LLClientView.cs:8480` → `:10390` → `:10409` `OnMoveInventoryItem` → `Scene.cs:3470` → `Scene.Inventory.cs:1119` `MoveInventoryItem` → `:1125` `InventoryService.MoveItems` → `XInventoryService.cs:567-573` → `m_Database.MoveItem` → `MySQLXInventoryData.cs:167-188` → `IncrementFolderVersion(oldParent)` and `(newParent)`. +**Both old and new parent versions are bumped on move.** + +**`CreateInventoryCategory` cap:** +`Source/OpenSim.Region.ClientStack.LindenCaps/BunchOfCaps/BunchOfCaps.cs:264-265` (registration) → `:1138` handler → `:1200` `new InventoryFolderBase(folderID, folderName, m_AgentID, (short)folderType, parentID, 1)` → `:1201` `m_Scene.InventoryService.AddFolder(folder)` → `XInventoryService.cs:369-407` `AddFolder` → `m_Database.StoreFolder` → `MySQLXInventoryData.cs:283-288` `Store(XInventoryFolder)` → `IncrementFolderVersion(folder.parentFolderID)`. +**Parent folder version is bumped on folder create; the new folder itself starts at version 1.** + +Caveat for ADR-006: the bump is a DB-side `version+1` with no read-back, and `XInventoryService.UpdateFolder` (`:423-426`) refuses a lower-than-stored version on typed folders (which includes COF, `FolderType.CurrentOutfit`), so a stale cached `InventoryFolderBase` written back by a region will not regress the counter. The sim must read `Version` fresh via `GetFolder`, not from a cached object. + +## V10 — Upstream delta (`HEAD..upstream/develop`, 22 commits upstream, 144 commits HEAD-only) + +`git diff --stat HEAD..upstream/develop --` over: `LLClientView.cs`, `Avatar/AvatarFactory/`, `OpenSim.Services.InventoryService/`, `OpenSim.Framework/SkiaImageUtils.cs`, `OpenSim.Framework/AvatarAppearance.cs`, `Scenes/Scene.Inventory.cs`, `OpenSim.Data.MySQL/MySQLXInventoryData.cs`, `World/LegacyMap/MapImageModule.cs`, `Capabilities.Handlers/GetTexture/`, `Directory.Build.props`, and the eleven csprojs cited in V7/V8. + +| File | What changed | +|---|---| +| `Directory.Build.props` | libopenmetaverse packages `1.1.6-ga897ffefd7` → `1.1.7` (upstream `58424056dd`). No SkiaSharp/CoreJ2K entries here on either side. | +| `Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs` | +15 lines: new overload `SendAlertMessage(string message, string info, byte[] extra)` at upstream `:3149`. **No change** to `SendAppearance`, `SendRegionHandshake`, or any appearance/cached-texture handler. | +| `Source/OpenSim.Capabilities.Handlers/GetTexture/GetTextureHandler.cs` | J2K area: decode changed from `j2k.As()` (+ `SKBitmap.FromImage`) to `j2k.As()`; comment: "CoreJ2K.Skia only registers an image creator for SKBitmap, not SKImage." Relevant to the library's decode path. | +| `Source/OpenSim.Capabilities.Handlers/GetTexture/GetTextureRobustHandler.cs` | Same `As` fix, then `SKImage.FromBitmap` for re-encode. | +| `Source/OpenSim.Region.CoreModules/World/LegacyMap/MapImageModule.cs` | J2K area (upstream `ae089ab7a7`/`301fc1421d`, #201): encoder config becomes `BuildEncoderConfig(w, h)` with `WithTiles(t => t.SetSize(w, h))` (single tile) instead of fixed 256×256 tiles; map tile sized to the region; terrain renderer selection simplified. Diverges both ways: HEAD carries `db7c746248` (legacy terrain restore) that upstream lacks. | +| `Source/OpenSim.Server.GridServer/OpenSim.Server.GridServer.csproj` | Removes `OpenSim.Services.MembershipService` ProjectReference. SkiaSharp lines unchanged. | +| `Source/OpenSim.Server.RegionServer/OpenSim.Server.RegionServer.csproj` | Adds Addons ProjectReferences (Gloebit, Groups, OfflineIM, Mutelist, Search) for publish; removes MembershipService and LegionJolt references. SkiaSharp lines unchanged. | +| `Source/Warp3D/Warp3D.csproj` | Adds `PackageReference OpenMetaverse.Rendering.Meshmerizer 1.0.6` (the non-UtopiaSkye package) alongside the global UtopiaSkye one — likely an upstream slip, not J2K/Skia related. | +| `Avatar/AvatarFactory/**`, `OpenSim.Services.InventoryService/**`, `AvatarAppearance.cs`, `Scene.Inventory.cs`, `MySQLXInventoryData.cs`, `SkiaImageUtils.cs`, `OpenSim.Framework.csproj`, `OpenSim.Region.CoreModules.csproj`, `OpenSim.Region.Framework.csproj`, `OpenSim.Capabilities.Handlers.csproj`, `OpenSim.Services.Connectors.csproj`, `OpenSim.Services.MapImageService.csproj`, `PhysicsModules.Meshing.csproj`, `PhysicsModules.ubODEMeshing.csproj` | **Identical** on both sides. | + +"#198" is not among the 22 upstream commits in this range, so no file in scope is logging-only from it. The only logging commit upstream (`ee71b6951b`) touches none of the files in scope. + +## Notes carried forward for S0b+ + +- `Directory.Build.props:3-5` sets `net10.0`, `ImplicitUsings=enable`, `Nullable=disable` tree-wide and injects all five `UtopiaSkye.OpenMetaverse*` packages into every project (`:13-17`). A new library must override `Nullable` locally and `Remove` the four non-Types packages if it wants only `OpenMetaverse.UUID`. `OpenMetaverse.UUID` is provided by `UtopiaSkye.OpenMetaverse.Types` (`lib/net10.0/OpenMetaverse.Types.dll`). +- Test framework in the tree is mixed: 19 test csprojs use NUnit, 7 use xunit (`xunit 2.9.3`, `xunit.runner.visualstudio 3.1.5`, `Microsoft.NET.Test.Sdk 18.8.1`, e.g. `Tests/OpenSim.Framework.PluginMigration.Tests/*.csproj`). The newer projects are xunit; the brief asks for xunit. +- Solution: `Tranquillity.sln` (94 projects, Tests in solution folder `{7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0}`). +- `F:\viewer-develop` is not a git checkout (no `.git`); `indra/newview/VIEWER_VERSION.txt` reads `26.1.1`, and `avatar_lad.xml` is 354,436 bytes, `wearable_definition_version="22"`, 11 `` elements. The addendum §3 names the viewer commit as `62033f2`; that could not be confirmed from the directory. diff --git a/Docs/feature/ssb-appearance/S1b-FIDELITY.md b/Docs/feature/ssb-appearance/S1b-FIDELITY.md new file mode 100644 index 00000000000..59137ece063 --- /dev/null +++ b/Docs/feature/ssb-appearance/S1b-FIDELITY.md @@ -0,0 +1,215 @@ +# S1b — The fidelity surface, measured against a second outfit + +**Date:** 2026-09-03 **Branch:** `feature/ssb-appearance` **Closes/updates:** Ledger Q-11 +**Sets:** `Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock`, `.../aleric-max` + +S1 shipped on one outfit. Ledger Q-11 recorded that this proved nothing about the layers that outfit never +touched. This session added a second reference set — Aleric Fenwood, Ebony, captured 2026-09-03 — and ran the +whole layer surface against it. + +**Result in one line: one real defect, in the upper channel's morph mask, and it is a parity gap the stock +outfit could not have shown.** No layer returned UNSUPPORTED on either outfit. + +--- + +## 1. What Aleric actually wears + +From `aleric-max/fixtures/avatar.json` (the live `Avatars` rows), 10 worn slots: + +| Type | Wearable | Index | Asset id | Parsed name | +|---|---|---|---|---| +| 0 | Shape | 0 | `8dca9b1a-9aeb-6580-c846-126d1cb5a280` | New Shape (82 params) | +| 1 | Skin | 0 | `045d0bb6-e055-6990-99e0-814936d03f9e` | Adam Skin (shaved) — 26 params, 3 textures | +| 2 | Hair | 0 | `e38631ab-a09a-3837-7965-59a485683402` | Professional Male 1-Hair base | +| 3 | Eyes | 0 | `cc5860c4-5651-0528-83ad-9dd95b25e9ee` | New Eyes | +| 4 | **Shirt** | 0 | `00000000-0000-0000-0000-000000000000` | **null asset id — slot worn but empty** | +| 5 | Pants | 0 | `5cc5ebc5-095e-328f-0435-13d2e0860531` | Professional Male 1-Pants | +| 6 | Shoes | 0 | `196eab6e-eae7-ca77-a752-0e97a2fcb7f3` | shoe base | +| 7 | **Socks** | 0 | `28cffb2a-557f-0cd8-5dee-089a915728ff` | mens black socks | +| 8 | **Jacket** | 0 | `ba106f90-f560-0ac3-868b-37088090b3a8` | Black Suede Blazer with blue shirt | +| 14 | **Tattoo** | 0 | `40294285-0ac8-3053-a459-acc66cc2101c` | Professional Male 1-Hair base | + +253 visual-param bytes. 11 distinct textures referenced and fetched. + +The Shirt row is worth its own line: the slot is **worn but carries the null asset id**. The orchestrator skips +it (`BakeOrchestrator.Resolve`, `assetId.IsZero()`), the harness skips it, and the compositor therefore reports +`upper_clothes skipped: no Shirt worn`. Section 4 argues this row may be the whole finding. + +## 2. Q-11 coverage: what this outfit reached and what it did not + +| Q-11 surface | Covered? | Evidence | +|---|---|---| +| **Tattoo** | **YES** | `head_tattoo drawn: Tattoo 40294285: HeadTattoo 512x512+alpha -> 1024`. `upper_tattoo` / `lower_tattoo` skipped: this tattoo wearable carries only a head texture. | +| **Socks** | **YES** | `lower_socks drawn: Socks 28cffb2a: LowerSocks 32x32 -> 1024; masks [Socks Length bump#1050=0.35+]` | +| **Jacket** | **YES** | `upper_jacket drawn` (5 param masks + texture alpha) and `lower_jacket drawn` (3) | +| Multi-wearables per slot | no | Every worn slot is index 0; no slot holds two wearables. The `LLTexLayerTemplate` multi-instance path (S0e) is still exercised only by the unit tests. | +| Universal | no | 6 layers report `no Universal worn` (`head_/upper_/lower_/eyes_/hair_universal_tattoo`, `*_tattoo` on eyes and hair). | +| Alpha | no | 5 layers report `no Alpha worn` (`head/upper/lower/eyes/hair alpha`). The `IMG_INVISIBLE` whole-region path is untouched. | +| Gloves | no | `upper_gloves skipped: no Gloves worn` | +| Skirt | no | No Skirt worn; the Skirt channel is not produced at all. | + +**Three of the eight covered; five remain untested against live content.** The five BoM aux channels +(`leftarm`, `leftleg`, `aux1`, `aux2`, `aux3`) are also unexercised — they need a Universal wearable, which is +the same gap. + +Q-11 therefore **narrows but does not close.** What it did buy: the first evidence that the compositor's +behaviour on a non-stock outfit is sound in RGB and alpha on every channel, and the discovery in §4. + +## 3. The layer surface, all 11 channels + +93 layers are declared across the 11 layer sets in `avatar_lad.xml`. 22 of them are `render_pass="bump"` +layers, which **the LL compositor never renders into a bake** (`lltexlayer.cpp:395`, S0d): they are listed +below as *not rendered by design*, not as gaps. + +| Channel | Layers | Produced for Aleric? | Drawn | Skipped | Bump (by design) | UNSUPPORTED | +|---|---|---|---|---|---|---| +| head | 25 | yes | 5 | 15 | 5 | **0** | +| upper_body | 23 | yes | 4 | 11 | 8 | **0** | +| lower_body | 24 | yes | 7 | 8 | 9 | **0** | +| eyes | 4 | yes | 2 | 2 | 0 | **0** | +| hair | 4 | yes | 2 | 2 | 0 | **0** | +| skirt | 3 | **no — no Skirt worn** | — | — | — | untested | +| leftarm | 2 | **no — no Universal worn** | — | — | — | untested | +| leftleg | 2 | **no — no Universal worn** | — | — | — | untested | +| aux1 | 2 | **no — no Universal worn** | — | — | — | untested | +| aux2 | 2 | **no — no Universal worn** | — | — | — | untested | +| aux3 | 2 | **no — no Universal worn** | — | — | — | untested | + +`unsupportedLayers=[]` on every produced channel, for both outfits. The compositor did not meet a mask file it +lacks, a resource it lacks, or a `local_texture` it does not know. + +### Why each layer was skipped (the five produced channels) + +Every skip falls into one of five reasons, none of which is a gap in the compositor: + +| Reason | Count | Example | +|---|---|---| +| `no worn` | 13 | `upper_gloves skipped: no Gloves worn` | +| `every mask parameter skipped` | 12 | `freckles skipped: every mask parameter skipped [Freckles#165=skip]` | +| `colour alpha 0` | 7 | `shadow skipped: colour alpha 0 (0.00,0.00,0.00,0.00)` | +| `no texture on the wearable` | 2 | `upper_tattoo skipped: Tattoo 40294285: no UpperTattoo texture on the wearable` | +| `no Shirt worn` (null-asset slot) | 3 | `upper_clothes skipped: no Shirt worn` — see §4 | + +## 4. THE FINDING — upper channel morph mask, `upper_clothes` + +**Channel:** `upper` (upper_body). **Layer:** `upper_clothes`, in its morph-mask role. +**Numbers, at all three bake sizes** (so this is structural, not a resampling artefact): + +| Size | mean abs dM | pixels dM > 8 | threshold | +|---|---|---|---| +| 512 | **82.25** | **33.27%** | 4.0 / 5% | +| 1024 | **82.24** | **33.21%** | 4.0 / 5% | +| 2048 | **82.25** | **33.22%** | 4.0 / 5% | + +The reference upper morph mask is a real mask — 31.4% of pixels near 0, 67.0% near 255. Ours is **uniform 255**: +we produce no upper morph mask at all. The compositor says why: + +``` +upper_clothes morph: no Shirt worn: mask left at 255 +``` + +**The machinery is not at fault.** On the same avatar, the lower channel's morph mask reproduces the reference +*exactly* — identical histograms, 29.3% near 0 and 69.2% near 255 in both, mean abs difference 0.47: + +``` +lower_pants morph: Pants 5cc5ebc5: morph mask *= layer mask (mean 178.3) +``` + +So the S0d/S0e gather is right where a declared morph-mask layer is worn. What is wrong is that in this outfit +the upper morph mask should not have been empty. `avatar_lad.xml`'s `` block names exactly one +upper_body layer, `upper_clothes` (for `Displace_Loose_Upperbody` and `Shirtsleeve_flair`), and that layer's +`local_texture` is the **Shirt** slot. Aleric wears no shirt asset — yet the LL viewer still produced a mask. + +**Two candidate causes; the evidence does not yet separate them.** + +1. *The gather should include `upper_jacket`.* The jacket is the only upper clothing worn, and it carries five + param masks (`jacket Sleeve Length#1020`, `jacket Collar Front#1022`, `Collar Back#1024`, + `bottom length upper#620`, `open upper#622`) that would produce a mask of roughly the observed shape. + *Against it:* if the gather simply took every alpha-param layer, the lower channel would pick up + `lower_socks` (`Socks Length bump#1050=0.35`) and `lower_shoes` (`Shoe Height#1052=0.10`) too, and our + pants-only result would then differ from the reference. It does not — it matches exactly. So the viewer's + gather is not "every layer", and any fix must explain that asymmetry. +2. *The null-asset Shirt row should still render `upper_clothes`.* Aleric's Shirt slot **is** worn, with the + null asset id. If the viewer treats that as a present-but-blank shirt wearable, it would run the + `upper_clothes` layer with the shirt's own param masks and produce a mask, while we skip the slot entirely. + This explains the asymmetry with the lower channel for free. + +Ruled out by measurement: the mask is **not** the jacket's texture alpha (that texture is 99.3% opaque, and +mean |refM − jacketAlpha| = 80.71), and **not** the bake's own alpha channel (mean |refM − refA| = 81.26). + +**Impact.** The 5th component drives the viewer's *clothing morphs* — how the mesh body is displaced to sit +under loose clothing. A missing upper morph mask means an avatar wearing a jacket and no shirt would not get +the loose-upper-body displacement it gets on SL. It does not affect the visible texture: RGB and alpha for the +same channel are well inside threshold (mean abs RGB 0.78, 0.22% of pixels over 8, at 1024). + +**Discriminating experiment** (not run here — it needs a code change and viewer-source access, neither in this +session's scope): re-bake Aleric's upper with (a) `upper_jacket` added to the gather and (b) a synthetic blank +Shirt wearable present, and see which reproduces the reference's 31.4%/67.0% histogram. + +## 5. Everything else passes, on both outfits + +At each set's manifest size, with S1's thresholds (mean abs RGB ≤ 4.0, pixels over 8 ≤ 5%, mean abs alpha ≤ 2.0, +mean abs morph ≤ 4.0): + +| Set | Channel | meanAbsRGB | meanAbsA | pctRGB>8 | meanAbsM | verdict | +|---|---|---|---|---|---|---| +| truly-stock @512 | head | 1.37 | 0.64 | 1.35% | 0.00 | pass | +| | upper | 1.41 | 1.00 | 0.64% | 1.00 | pass | +| | lower | 1.36 | 1.00 | 0.63% | 1.00 | pass | +| | eyes | 0.36 | 1.00 | 0.00% | 1.00 | pass | +| | hair | 0.00 | 0.00 | 0.00% | 1.00 | pass (RGB skipped: reference alpha all zero) | +| aleric-max @1024 | head | 0.80 | 0.84 | 0.00% | 0.00 | pass | +| | upper | 0.78 | 1.00 | 0.22% | **82.24** | **FAIL on morph mask** (§4) | +| | lower | 0.89 | 1.00 | 0.04% | 0.47 | pass | +| | eyes | 0.45 | 1.00 | 0.48% | 1.00 | pass | +| | hair | 0.33 | 0.53 | 0.00% | 1.00 | pass | + +The richer outfit is, if anything, **closer** in RGB than the stock one (0.78–0.89 against 1.36–1.41). Nothing +about jacket, socks or tattoo compositing degrades the visible bake. + +## 6. Bake size (S1b Part 2) — evidence for ADR-008 + +References as captured: head, upper, lower and hair are **2048×2048**; **eyes is 512×512** on both avatars. + +Fidelity against bake size (mean abs RGB; both images resampled to the compared size): + +| Set | Channel | 512 | 1024 | 2048 | +|---|---|---|---|---| +| truly-stock | head | 1.37 | 0.96 | 0.97 | +| | upper | 1.41 | 1.25 | 1.26 | +| | lower | 1.36 | 1.11 | 1.19 | +| | eyes | 0.36 | 0.32 | 0.33 | +| aleric-max | head | 1.21 | 0.80 | 0.82 | +| | upper | 1.07 | 0.78 | 0.84 | +| | lower | **2.29** | 0.89 | 0.94 | +| | eyes | 0.42 | 0.45 | 0.46 | +| | hair | 0.47 | 0.33 | 0.21 | + +Encoded bytes, all five channels summed: + +| Set | 512 | 1024 | 2048 | +|---|---|---|---| +| truly-stock | 213,431 | 310,333 | 574,645 | +| aleric-max | 227,143 | 347,324 | 634,332 | + +**Reading: 1024 is the knee.** Every channel improves from 512 to 1024 — and the richer outfit improves +dramatically (aleric lower 2.29 → 0.89, the single worst number in the whole matrix, and it is at 512). Going +1024 → 2048 buys nothing: five of the nine channel rows get *worse*, none improves materially, and the cost +rises ~1.8×. 512 costs ~0.69× of 1024, which is not much of a saving for a visible loss on a busy outfit. + +This is evidence, not a decision: **ADR-008 is not changed here.** But note the inconsistency it should +settle — ADR-008 and Ledger D-7 record the default as **512**, while S1 shipped +`[Appearance] BakeSize = 1024` in `OpenSimDefaults.ini` and the live sim is running 1024. The measurements +support 1024; the ADR should be updated to match reality rather than the ini changed to match the ADR. + +## 7. Harness changes + +`Golden/` now holds one subdirectory per reference set, each with its own committed `manifest.json` and +gitignored `fixtures/`. `fetch-fixtures.sh ` populates one set, reading the avatar name from that set's +manifest, with the S0b fallbacks unchanged (Robust first, then the region's Flotsam cache; the source is +reported per UUID). `GoldenTests` is a `[Theory]` over the sets present, plus `bake_size_sweep`, which reports +§6's numbers and asserts only that every channel encodes at the size asked for. + +For `aleric-max`: all 9 non-null wearables and all 11 textures came from **Robust**; all 5 reference bakes came +from the **region cache** (bakes are temporary assets — Robust 404s them). The fetch script now skips a worn +slot carrying the null asset id, as the orchestrator does, and says so in its table. From b1da081d1e3a8a5e02852650d2c384b6e149f6da Mon Sep 17 00:00:00 2001 From: JohnLegionH Date: Sun, 20 Sep 2026 12:47:07 -0500 Subject: [PATCH 5/8] fix(build): drop Warp3D's unresolvable OpenMetaverse.Rendering.Meshmerizer reference NOT PART OF THE TRACK L CARVE. Kept as the tip commit so it can be dropped with `git reset --hard HEAD~1` if your CI can resolve the package. Source/Warp3D/Warp3D.csproj carries which resolves from neither configured source: nuget.org returns 404 and the OpenSim-NGC GitHub Packages feed returns 403 for a token that reads every other package in the tree. NuGet fails the whole-solution restore on it, so `dotnet build Tranquillity.sln` produces 19 NU1301 errors on develop before any of this branch's code is considered. It is also redundant: Directory.Build.props already gives every project UtopiaSkye.OpenMetaverse.Rendering.Meshmerizer 1.1.7, the same component from the package this repo actually publishes. Docs/feature/ssb-appearance/ S0a-VERIFICATION.md recorded it at the time as "likely an upstream slip". With the line removed, restore succeeds and the solution builds 0 errors. --- Source/Warp3D/Warp3D.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Warp3D/Warp3D.csproj b/Source/Warp3D/Warp3D.csproj index ae7a665374c..ee7af4c646c 100755 --- a/Source/Warp3D/Warp3D.csproj +++ b/Source/Warp3D/Warp3D.csproj @@ -10,7 +10,6 @@ - From b5dbd2831397ef65722f0d645a8c2e758eb01201 Mon Sep 17 00:00:00 2001 From: JohnLegionH Date: Sun, 20 Sep 2026 13:06:57 -0500 Subject: [PATCH 6/8] test: neutral hostnames and connection strings in Track L tests Two Track L tests carried a live grid's identity as a literal. LoginResponseAdvertisementTests asserted on `http://legiongrid.ddns.net:8002/` as the advertised appearance-service URL; it is now `http://example.org:8002/`. The value is arbitrary to the test, which only checks that what was configured is what the login response carries, in both the XML-RPC and LLSD shapes. AisErrorHygieneTraversalTests injects a connector fault whose text stands in for a real one, and then proves none of it reaches the client. The injected string was a real host and database name; it is now `Server=db.example.org;Database=opensim;Uid=root;Pwd=hunter2`. The assertion that a host address does not reach the wire was updated to match. `Pwd=hunter2` is deliberate: it is the token the test greps for on the wire (must be absent) and in the log (must be present, so the operator still gets the real exception), and it is not a credential to anything. --- .../AIS/AisErrorHygieneTraversalTests.cs | 4 ++-- .../LoginResponseAdvertisementTests.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisErrorHygieneTraversalTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisErrorHygieneTraversalTests.cs index f20945afced..dde35f9227c 100644 --- a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisErrorHygieneTraversalTests.cs +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisErrorHygieneTraversalTests.cs @@ -53,7 +53,7 @@ public class AisErrorHygieneTraversalTests /// Text a real connector fault might carry. If this reaches the client, the fix is not working. private const string Secret = - "Server=10.44.0.9;Database=legiongrid;Uid=root;Pwd=hunter2 -- at MySql.Data.MySqlClient.NativeDriver.Open()"; + "Server=db.example.org;Database=opensim;Uid=root;Pwd=hunter2 -- at MySql.Data.MySqlClient.NativeDriver.Open()"; private sealed class HygRequest : OpenSim.Framework.Servers.HttpServer.IOSHttpRequest { @@ -136,7 +136,7 @@ public void an_unexpected_backend_exception_is_logged_and_not_echoed_to_the_clie // the whole point: nothing of the exception may travel Assert.That(wire, Does.Not.Contain("hunter2"), "a credential reached the client"); - Assert.That(wire, Does.Not.Contain("10.44.0.9"), "a host address reached the client"); + Assert.That(wire, Does.Not.Contain("db.example.org"), "a host address reached the client"); Assert.That(wire, Does.Not.Contain("MySql"), "an internal type name reached the client"); Assert.That(wire, Does.Not.Contain(Secret)); Assert.That(wire, Does.Not.Contain("InvalidOperationException")); diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/LoginResponseAdvertisementTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/LoginResponseAdvertisementTests.cs index 04540ba4c48..2056c6a34d6 100644 --- a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/LoginResponseAdvertisementTests.cs +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/LoginResponseAdvertisementTests.cs @@ -75,12 +75,12 @@ private static OSDMap Llsd(string url) [Fact] public void WhenConfiguredTheResponseCarriesTheUrlInBothEncodings() { - var (xml, llsd) = Build("http://legiongrid.ddns.net:8002/"); + var (xml, llsd) = Build("http://example.org:8002/"); Assert.True(xml.ContainsKey(Key)); - Assert.Equal("http://legiongrid.ddns.net:8002/", xml[Key]); + Assert.Equal("http://example.org:8002/", xml[Key]); Assert.True(llsd.ContainsKey(Key)); - Assert.Equal("http://legiongrid.ddns.net:8002/", llsd[Key].AsString()); + Assert.Equal("http://example.org:8002/", llsd[Key].AsString()); } [Fact] From c15129ec44c9a5ceff6cc82b22f8d60c93eb1769 Mon Sep 17 00:00:00 2001 From: JohnLegionH Date: Sun, 20 Sep 2026 13:12:13 -0500 Subject: [PATCH 7/8] test(appearance-baking): golden harness takes its grid from the environment; absent fixtures are a real skip Three things made the golden harness unusable by anyone but the grid it was written on, and made a checkout without fixtures look greener than it was. fetch-fixtures.sh had one grid's identity as its defaults: a database container name, a database name, a Windows path to the file holding the root password, the key name inside that file, a simulator asset-cache path and a localhost Robust URL. Every one is now a required variable (GOLDEN_DB_CONTAINER, GOLDEN_DB_NAME, GOLDEN_DB_ENV_FILE, GOLDEN_DB_PW_KEY, GOLDEN_ROBUST_ASSETS, GOLDEN_REGION_CACHE) declared with ${VAR:?...}, so the script names what is missing instead of silently reaching for a grid that is not there. A header block documents each one and gives a worked example with placeholder values. The logic is unchanged: exporting the six values makes it behave exactly as it did. Both manifest.json files named the capture grid by database container and Robust URL. That field now describes where the capture came from and points at the script's header for the reader's own grid. Avatar names, asset UUIDs and the reference bake ids stay: they are the fixtures' identity and the script reads them. Eight fixture-dependent tests across two projects returned early after printing a SKIPPED line, so a run with no fixtures reported eight passes that had asserted nothing. They are now skipped for real. xunit here is 2.9.3, which has no per-row skip for a [Theory], so each [Theory]/[MemberData] gate became one named method per reference set carrying a skip attribute whose constructor sets Skip when that set's fixtures/avatar.json is absent, with a reason telling an operator how to generate fixtures from their own grid. No new package dependency. Source/OpenSimNGC.Appearance.Baking.Tests GoldenFactAttribute Tests/...ServerSideBaking.Tests GoldenFixturesFactAttribute The two are duplicated rather than shared because the second project references the baking library, not its test project. Reference sets are now named rather than discovered, which is the point: a set whose fixtures are missing is visible as a skip instead of vanishing from the run. BackendTests' transparent-texture test asserted a synthetic case and then the same rule on real fixtures. It is split, so the synthetic half still runs everywhere and only the real-content half skips. Baking.Tests 35 passed / 0 skipped -> 31 passed / 5 skipped / 36 ServerSideBaking.Tests 118 passed / 0 skipped -> 115 passed / 3 skipped / 118 The 36 is the same 35 cases plus the split. --- .../BackendTests.cs | 21 ++++-- .../Golden/GoldenFactAttribute.cs | 43 ++++++++++++ .../Golden/GoldenTests.cs | 68 ++++++++++--------- .../Golden/aleric-max/manifest.json | 4 +- .../Golden/fetch-fixtures.sh | 66 ++++++++++++------ .../Golden/truly-stock/manifest.json | 4 +- .../BakeCostTests.cs | 16 ++--- .../BakeIndexSurvivesAppearanceSaveTests.cs | 6 +- .../GoldenFixturesFactAttribute.cs | 43 ++++++++++++ 9 files changed, 195 insertions(+), 76 deletions(-) create mode 100644 Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenFactAttribute.cs create mode 100644 Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/GoldenFixturesFactAttribute.cs diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/BackendTests.cs b/Source/OpenSimNGC.Appearance.Baking.Tests/BackendTests.cs index d7b19f81506..78af7e86de5 100644 --- a/Source/OpenSimNGC.Appearance.Baking.Tests/BackendTests.cs +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/BackendTests.cs @@ -362,10 +362,10 @@ public void a_channel_whose_every_layer_was_skipped_reports_nothing_drawn() } /// - /// Drawn-but-transparent is NOT undrawn. Truly Bazar's hair wearable carries a 4x4 fully transparent - /// texture: the base layer draws it, the bake is legitimately all-transparent, and it must still be stored. - /// Asserted on the real truly-stock fixtures, and on a synthetic equivalent so the rule is covered when the - /// fixtures are not fetched. + /// Drawn-but-transparent is NOT undrawn. A hair wearable carrying a fully transparent texture still draws: + /// the base layer draws it, the bake is legitimately all-transparent, and it must still be stored. This is + /// the synthetic half and needs no fixtures; the same rule on real content is + /// . /// [Fact] public void a_channel_that_drew_a_fully_transparent_texture_is_not_nothing_drawn() @@ -381,10 +381,17 @@ public void a_channel_that_drew_a_fully_transparent_texture_is_not_nothing_drawn Assert.Contains(synthetic.Fidelity.Notes, n => n.StartsWith("base drawn")); var img = J2kCodec.Decode(synthetic.J2kBytes); Assert.True(img.A.All(a => a <= 2), "the bald hair bake is legitimately all-transparent and must still be stored"); + } - // the real thing: Truly Bazar's stock outfit + /// + /// The same rule on real content: a captured outfit's hair draws even when its texture is fully transparent, + /// and no channel of an ordinary outfit reports nothing drawn. Needs the truly-stock reference set, so it is + /// skipped rather than silently passed when the fixtures have not been generated. + /// + [OpenSimNGC.Appearance.Baking.Tests.Golden.GoldenFact("truly-stock")] + public void a_real_outfit_reports_nothing_drawn_on_no_channel() + { var fx = TrulyFixtures(); - if (!File.Exists(Path.Combine(fx, "avatar.json"))) { Console.WriteLine("SKIPPED (truly-stock fixtures not fetched): synthetic case asserted above"); return; } using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(fx, "avatar.json"))); var worn = new List(); foreach (var w in doc.RootElement.GetProperty("wearables").EnumerateArray()) @@ -404,7 +411,7 @@ public void a_channel_that_drew_a_fully_transparent_texture_is_not_nothing_drawn } var real = new SkiaBakeBackend().Bake(new BakeRequest(worn, new Dictionary(), tex, 128)); var realHair = real.Single(r => r.Channel == BakeChannel.Hair); - Assert.False(realHair.NothingDrawn, "Truly's bald hair drew: " + string.Join(" | ", realHair.Fidelity.Notes)); + Assert.False(realHair.NothingDrawn, "the reference outfit bald hair drew: " + string.Join(" | ", realHair.Fidelity.Notes)); // and no channel of a normal outfit reports nothing drawn Assert.All(real, r => Assert.False(r.NothingDrawn, $"{r.Channel}: " + string.Join(" | ", r.Fidelity.Notes))); } diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenFactAttribute.cs b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenFactAttribute.cs new file mode 100644 index 00000000000..f6301776392 --- /dev/null +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenFactAttribute.cs @@ -0,0 +1,43 @@ +using System.Runtime.CompilerServices; +using Xunit; + +namespace OpenSimNGC.Appearance.Baking.Tests.Golden; + +/// +/// A for a test that needs a reference set's fixtures. +/// A reference set is a subdirectory of Golden/ holding a committed manifest.json and an +/// uncommitted fixtures/. The fixtures are one resident's real worn assets and the reference bakes +/// captured from them, so they are not in the repository and no checkout has them until an operator generates +/// them from their own grid. Without them the test cannot assert anything. +/// Rather than returning early and reporting a pass it never earned, a test marked with this attribute +/// is skipped when its set's fixtures/avatar.json is absent, and the skip reason says how to +/// produce them. xunit evaluates at discovery, which is why the check is in +/// the constructor. +/// Adding a new reference set means adding its directory, its manifest.json, and a test method +/// per gate naming it; sets are named here rather than discovered so that a set whose fixtures are missing is +/// still visible in the run as a skip. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class GoldenFactAttribute : FactAttribute +{ + /// The directory this source file sits in, which is Golden/. + private static string GoldenDir([CallerFilePath] string here = "") => Path.GetDirectoryName(here)!; + + /// The reference set's directory name, e.g. truly-stock. + public GoldenFactAttribute(string set) + { + Set = set; + var fixtures = Path.Combine(GoldenDir(), set, "fixtures"); + if (!File.Exists(Path.Combine(fixtures, "avatar.json"))) + { + Skip = $"no fixtures for reference set '{set}'. They are one resident's real assets and are not " + + $"committed. To generate them for your own grid: export the variables documented in the " + + $"header of Golden/fetch-fixtures.sh (grid database container and name, a file holding its " + + $"root password, the Robust asset service URL and a simulator asset cache), then run " + + $"Golden/fetch-fixtures.sh {set}. It writes {fixtures}. Nothing is asserted without them."; + } + } + + /// The reference set this test reads. + public string Set { get; } +} diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenTests.cs b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenTests.cs index f530489c09b..3c507422c82 100644 --- a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenTests.cs +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/GoldenTests.cs @@ -13,20 +13,21 @@ namespace OpenSimNGC.Appearance.Baking.Tests.Golden; /// an avatar's worn outfit plus the reference bakes (LL compositor output, captured via the client-bake path /// named in that manifest). The authority is the LL compositor, never the capturing client (Ledger P-1). /// -/// truly-stock/ — Truly Bazar, stock Library outfit (S0b). -/// aleric-max/ — Aleric Fenwood, a richer outfit: socks, jacket and a tattoo (S1b, Ledger Q-11). +/// truly-stock/ — a stock Library outfit. +/// aleric-max/ — a richer outfit: socks, jacket and a tattoo, added because the stock one exercised no unsupported layer. /// /// Fixtures are fetched per set by fetch-fixtures.sh <set> into <set>/fixtures/ (gitignored); -/// when they are absent the test reports that and returns without asserting anything. +/// when they are absent the test is skipped by , with the reason naming the +/// script and the environment it needs. Nothing here ever passes without asserting. /// -/// bakes at the manifest's bakeSize — which is the +/// The threshold gate bakes at the manifest's bakeSize — which is the /// shipped [Appearance] BakeSize (ADR-008: 1024), not the reference's own size — and asserts, per channel: /// RGB (mean |d| <= 4, at most 5% of pixels with |d| > 8 — both skipped when the reference alpha is entirely /// zero, as for a bald hair), alpha (mean |d| <= 2) and the 5th component, the morph mask (mean |d| <= 4 and, /// unless the reference's mask is uniform, at most 5% of pixels with |d| > 8). It writes the table and the full /// per-layer decision log to Golden/last-run-<set>.txt. The numbers came first (S0b), the thresholds after. /// -/// (S1b Part 2) repeats the comparison at 512, 1024 and 2048 and reports the +/// The size sweep repeats the comparison at 512, 1024 and 2048 and reports the /// encoded byte size per channel per size. It asserts only that every channel encodes and decodes at the size asked /// for: it is the evidence for ADR-008's default, not a gate on it. /// @@ -37,13 +38,13 @@ public class GoldenTests private static string GoldenDir([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!; - /// Every set present: a subdirectory holding a manifest.json. - public static IEnumerable Sets() - => Directory.EnumerateDirectories(GoldenDir()) - .Where(d => File.Exists(Path.Combine(d, "manifest.json"))) - .Select(d => Path.GetFileName(d)!) - .OrderBy(n => n, StringComparer.Ordinal) - .Select(n => new object[] { n }); + /// + /// The reference sets, named rather than discovered. Discovery would hide a set whose fixtures are absent; + /// naming it gives one skip per set per gate, with the reason on it. A new set gets a directory, a + /// manifest.json, and one method per gate below. + /// + private const string TrulyStock = "truly-stock"; + private const string AlericMax = "aleric-max"; private sealed record Manifest(string Avatar, string Outfit, string Captured, [property: System.Text.Json.Serialization.JsonPropertyName("captured_via")] string? CapturedVia, @@ -77,18 +78,17 @@ private sealed class SetContext public required Dictionary VisualParams; } - /// Loads a set, or returns null with a reason when its fixtures are not there. - private static SetContext? Load(string set, out string? reason) + /// + /// Loads a set. Callers are marked , which skips the test when the + /// fixtures are absent, so reaching here without them is a broken harness rather than a missing capture. + /// + private static SetContext Load(string set) { - reason = null; var dir = Path.Combine(GoldenDir(), set); var fixtures = Path.Combine(dir, "fixtures"); var manifest = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(dir, "manifest.json")), JsonOpts)!; - if (!Directory.Exists(fixtures) || !File.Exists(Path.Combine(fixtures, "avatar.json"))) - { - reason = $"SKIPPED [{set}]: no fixtures at {fixtures}. Run Golden/fetch-fixtures.sh {set} (needs the Legion grid DB and Robust) to populate them; nothing is asserted without them."; - return null; - } + if (!File.Exists(Path.Combine(fixtures, "avatar.json"))) + throw new FileNotFoundException($"fixtures for '{set}' are missing but the test was not skipped; see GoldenFactAttribute", Path.Combine(fixtures, "avatar.json")); var avatar = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(fixtures, "avatar.json")), JsonOpts)!; string Fixture(string uuid, params string[] exts) @@ -228,12 +228,15 @@ private static void Table(StringBuilder report, List rows) // ------------------------------------------------------------------ the threshold gate - [Theory] - [MemberData(nameof(Sets))] - public void reference_set_versus_library_bakes(string set) + [GoldenFact(TrulyStock)] + public void reference_set_versus_library_bakes_truly_stock() => ReferenceSetVersusLibraryBakes(TrulyStock); + + [GoldenFact(AlericMax)] + public void reference_set_versus_library_bakes_aleric_max() => ReferenceSetVersusLibraryBakes(AlericMax); + + private void ReferenceSetVersusLibraryBakes(string set) { - var c = Load(set, out var reason); - if (c is null) { _out.WriteLine(reason!); return; } + var c = Load(set); var size = c.Manifest.BakeSize; var (results, rows, failures) = Compare(c, size); @@ -265,14 +268,17 @@ public void reference_set_versus_library_bakes(string set) Assert.True(failures.Count == 0, string.Join("; ", failures)); } - // ------------------------------------------------------------------ S1b Part 2: bake size + // ------------------------------------------------------------------ bake size sweep + + [GoldenFact(TrulyStock)] + public void bake_size_sweep_truly_stock() => BakeSizeSweep(TrulyStock); + + [GoldenFact(AlericMax)] + public void bake_size_sweep_aleric_max() => BakeSizeSweep(AlericMax); - [Theory] - [MemberData(nameof(Sets))] - public void bake_size_sweep(string set) + private void BakeSizeSweep(string set) { - var c = Load(set, out var reason); - if (c is null) { _out.WriteLine(reason!); return; } + var c = Load(set); var report = new StringBuilder(); Header(report, c, 0); diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/aleric-max/manifest.json b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/aleric-max/manifest.json index 56821754e8b..384e542e1ce 100644 --- a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/aleric-max/manifest.json +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/aleric-max/manifest.json @@ -4,7 +4,7 @@ "captured": "2026-09-03", "captured_via": "Firestorm client-bake path", "authority": "LL viewer compositor (lltexlayer.cpp / llavatarappearance.cpp); the reference bakes are its output, captured through a client; nothing is keyed on the capturing client", - "grid": "Legion Grid (legiongrid_mysql / Robust localhost:8003)", + "grid": "the grid the capture was made on; fetch-fixtures.sh reads your own grid's database and asset service from the environment (see its header)", "region": "Ebony", "bakeSize": 1024, "goldens": { @@ -16,7 +16,7 @@ }, "notSet": ["skirt", "leftarm", "leftleg", "aux1", "aux2", "aux3"], "notes": [ - "Second reference set (S1b), added to close Ledger Q-11: Truly's stock-Library outfit exercised no unsupported layer, so the compositor's behaviour on a richer outfit was unproven.", + "Second reference set: the first set's stock-Library outfit exercised no unsupported layer, so the compositor's behaviour on a richer outfit was unproven.", "Goldens are reference bakes (LL compositor output) captured on 2026-09-03 via the Firestorm client-bake path.", "Bakes are temporary assets: Robust answers 404 for them; fetch-fixtures.sh falls back to the region's Flotsam asset cache, where the sim keeps them as the same AssetBase XML.", "Fixtures are not committed (binaries, resident content). Run fetch-fixtures.sh aleric-max to populate fixtures/." diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh index 30fcda37c6d..022ffc11dec 100644 --- a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh @@ -5,21 +5,48 @@ # # Usage: ./fetch-fixtures.sh [set-name] (default: truly-stock) # -# A "set" is a subdirectory here holding manifest.json (committed) and fixtures/ (not committed): -# truly-stock/ Truly Bazar, stock Library outfit (S0b) -# aleric-max/ Aleric Fenwood, richer outfit (S1b, Ledger Q-11) -# +# A "set" is a subdirectory here holding manifest.json (committed) and fixtures/ (not committed). # The avatar's name comes from the manifest's "avatar" field; the reference bake UUIDs from its -# "goldens" map. Steps: +# "goldens" map. +# +# --------------------------------------------------------------------------------------------- +# REQUIRED ENVIRONMENT +# +# This script reads from a running OpenSim grid. It has no defaults: every variable below must be +# exported for your own grid, and the script stops with a message naming any that is missing. +# +# GOLDEN_DB_CONTAINER Name of the running Docker container hosting the grid's MySQL/MariaDB +# server. The script runs `docker exec mysql ...` against it. +# GOLDEN_DB_NAME The grid's database, the one holding UserAccounts and Avatars. +# GOLDEN_DB_ENV_FILE Path to a file holding the database root password as KEY=value on its +# own line. The password is read from this file and is never written +# anywhere by this script. +# GOLDEN_DB_PW_KEY The key name to read out of GOLDEN_DB_ENV_FILE. +# GOLDEN_ROBUST_ASSETS Base URL of the grid's Robust asset service, without a trailing slash. +# The script GETs / and expects AssetBase XML. +# GOLDEN_REGION_CACHE Root of a simulator's Flotsam asset cache (the directory holding the +# three-character shard subdirectories). Bakes are temporary assets that +# Robust does not retain, so they are read from here instead. +# +# Worked example, with placeholder values only: +# +# export GOLDEN_DB_CONTAINER=mygrid_mysql +# export GOLDEN_DB_NAME=mygrid +# export GOLDEN_DB_ENV_FILE=/srv/mygrid/.env +# export GOLDEN_DB_PW_KEY=MYSQL_ROOT_PASSWORD +# export GOLDEN_ROBUST_ASSETS=http://robust.internal:8003/assets +# export GOLDEN_REGION_CACHE=/srv/mygrid/regionserver/assetcache +# ./fetch-fixtures.sh truly-stock +# +# --------------------------------------------------------------------------------------------- +# Steps: # -# 1. The avatar's PrincipalID from the live grid DB (container legiongrid_mysql, database legiongrid). -# The root password is read from D:\legiongrid-runtime\.env (key LEGIONGRID_DB_ROOT_PW); it is -# never written anywhere. +# 1. The avatar's PrincipalID from the grid database named by GOLDEN_DB_NAME. # 2. Their Avatars rows ('Wearable :' = itemID:assetID, and VisualParams) -> fixtures/avatar.json -# 3. Every wearable asset, every texture those wearables reference, and the reference bakes, from Robust -# (http://localhost:8003/assets/, AssetBase XML with base64 Data) -> fixtures/.. -# Bakes are temporary assets and Robust does not hold them; for those the region's Flotsam asset -# cache (same AssetBase XML on disk) is read instead, and the source column says so. +# 3. Every wearable asset, every texture those wearables reference, and the reference bakes, from +# the Robust asset service (AssetBase XML with base64 Data) -> fixtures/.. +# Bakes are temporary assets and Robust does not hold them; for those the simulator's Flotsam +# asset cache (same AssetBase XML on disk) is read instead, and the source column says so. # # Nothing is fabricated: any UUID that cannot be fetched from either source stops the script (exit 1). # @@ -31,11 +58,12 @@ SET="${1:-truly-stock}" SET_DIR="$HERE/$SET" MANIFEST="$SET_DIR/manifest.json" OUT="$SET_DIR/fixtures" -ENV_FILE="${LEGIONGRID_ENV:-D:/legiongrid-runtime/.env}" -ROBUST="${ROBUST_ASSETS:-http://localhost:8003/assets}" -REGION_CACHE="${LEGIONGRID_REGION_CACHE:-D:/legiongrid/regionserver/assetcache}" -DB_CONTAINER="${LEGIONGRID_DB_CONTAINER:-legiongrid_mysql}" -DB_NAME="${LEGIONGRID_DB_NAME:-legiongrid}" +ENV_FILE="${GOLDEN_DB_ENV_FILE:?set GOLDEN_DB_ENV_FILE to the file holding the grid database root password (KEY=value); see the header}" +PW_KEY="${GOLDEN_DB_PW_KEY:?set GOLDEN_DB_PW_KEY to the key name to read out of GOLDEN_DB_ENV_FILE; see the header}" +ROBUST="${GOLDEN_ROBUST_ASSETS:?set GOLDEN_ROBUST_ASSETS to the grid Robust asset service base URL, no trailing slash; see the header}" +REGION_CACHE="${GOLDEN_REGION_CACHE:?set GOLDEN_REGION_CACHE to a simulator Flotsam asset cache root; see the header}" +DB_CONTAINER="${GOLDEN_DB_CONTAINER:?set GOLDEN_DB_CONTAINER to the Docker container running the grid database; see the header}" +DB_NAME="${GOLDEN_DB_NAME:?set GOLDEN_DB_NAME to the grid database holding UserAccounts and Avatars; see the header}" die() { echo "FETCH FAILED: $*" >&2; exit 1; } @@ -57,8 +85,8 @@ echo "manifest $MANIFEST" echo "avatar $FIRST $LAST" [ -f "$ENV_FILE" ] || die "env file $ENV_FILE not found" -PW="$(grep -E '^LEGIONGRID_DB_ROOT_PW=' "$ENV_FILE" | cut -d= -f2- | tr -d '"\r')" -[ -n "$PW" ] || die "LEGIONGRID_DB_ROOT_PW not set in $ENV_FILE" +PW="$(grep -E "^${PW_KEY}=" "$ENV_FILE" | cut -d= -f2- | tr -d '"\r')" +[ -n "$PW" ] || die "$PW_KEY not set in $ENV_FILE" sql() { docker exec "$DB_CONTAINER" mysql -uroot -p"$PW" "$DB_NAME" -N -B -e "$1" 2>/dev/null | tr -d '\r'; } diff --git a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/manifest.json b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/manifest.json index 70d944ad3d8..d0025150e20 100644 --- a/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/manifest.json +++ b/Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/manifest.json @@ -4,7 +4,7 @@ "captured": "2026-09-03", "captured_via": "Firestorm client-bake path", "authority": "LL viewer compositor (lltexlayer.cpp / llavatarappearance.cpp); the reference bakes are its output, captured through a client; nothing is keyed on the capturing client", - "grid": "Legion Grid (legiongrid_mysql / Robust localhost:8003)", + "grid": "the grid the capture was made on; fetch-fixtures.sh reads your own grid's database and asset service from the environment (see its header)", "bakeSize": 1024, "goldens": { "head": "e7b03937-957c-435d-9a3b-3b516b489ed5", @@ -18,6 +18,6 @@ "Goldens are reference bakes (LL compositor output) captured on the wire on 2026-09-03 via the Firestorm client-bake path.", "Bakes are temporary assets: Robust answers 404 for them; fetch-fixtures.sh falls back to the region's Flotsam asset cache, where the sim keeps them as the same AssetBase XML.", "Fixtures are not committed (binaries, resident content). Run fetch-fixtures.sh to populate fixtures/.", - "bakeSize is the size the gate bakes at, not the reference's: it is the shipped [Appearance] BakeSize (ADR-008, 1024). It was 512 through S1; raised to 1024 in S2 Part 0 so the threshold gate diffs what the live sim actually stores. The references stay as captured (2048 for head/upper/lower/hair, 512 for eyes) and both images are resampled to the compared size." + "bakeSize is the size the gate bakes at, not the reference's: it is the shipped [Appearance] BakeSize (ADR-008, 1024), so the threshold gate diffs what a simulator running the shipped default actually stores. The references stay as captured (2048 for head/upper/lower/hair, 512 for eyes) and both images are resampled to the compared size." ] } diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeCostTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeCostTests.cs index 44dca1f7a20..84e4fb2de75 100644 --- a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeCostTests.cs +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeCostTests.cs @@ -43,19 +43,15 @@ public class BakeCostTests private static string GoldenDir([CallerFilePath] string here = "") => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "..", "..", "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden")); - public static IEnumerable Sets() - => new[] { new object[] { "truly-stock" }, new object[] { "aleric-max" } }; + [GoldenFixturesFact("truly-stock")] + public void where_the_bake_second_goes_truly_stock() => WhereTheBakeSecondGoes("truly-stock"); - [Theory] - [MemberData(nameof(Sets))] - public void where_the_bake_second_goes(string set) + [GoldenFixturesFact("aleric-max")] + public void where_the_bake_second_goes_aleric_max() => WhereTheBakeSecondGoes("aleric-max"); + + private void WhereTheBakeSecondGoes(string set) { var fixtures = Path.Combine(GoldenDir(), set, "fixtures"); - if (!File.Exists(Path.Combine(fixtures, "avatar.json"))) - { - _out.WriteLine($"SKIPPED [{set}]: no fixtures at {fixtures}; run Golden/fetch-fixtures.sh {set}"); - return; - } var assets = new FakeAssetService(); foreach (var f in Directory.GetFiles(fixtures)) diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeIndexSurvivesAppearanceSaveTests.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeIndexSurvivesAppearanceSaveTests.cs index 80b19ed61b4..4b8cff0731f 100644 --- a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeIndexSurvivesAppearanceSaveTests.cs +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/BakeIndexSurvivesAppearanceSaveTests.cs @@ -190,17 +190,13 @@ public void WithNoBakeIndexTheSaveIsTheSameSingleDeleteItAlwaysWas() private static string FixtureDir([CallerFilePath] string here = "") => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "..", "..", "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden", "truly-stock", "fixtures")); - private const string SkipNote = "SKIPPED: golden fixtures not fetched (Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock/fixtures)"; - /// /// The whole point of Q-14, end to end: bake, let an appearance save run through the real service, bake again. /// Before the fix the second run recomposited all five channels. Now it reuses all five. /// - [Fact] + [GoldenFixturesFact("truly-stock")] public void ABakeAfterAnAppearanceSaveStillReuses() { - if (!File.Exists(Path.Combine(FixtureDir(), "avatar.json"))) { Console.WriteLine(SkipNote); return; } - var dir = FixtureDir(); var assets = new FakeAssetService(); foreach (var f in Directory.GetFiles(dir)) diff --git a/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/GoldenFixturesFactAttribute.cs b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/GoldenFixturesFactAttribute.cs new file mode 100644 index 00000000000..71847026416 --- /dev/null +++ b/Tests/OpenSim.Region.OptionalModules.ServerSideBaking.Tests/GoldenFixturesFactAttribute.cs @@ -0,0 +1,43 @@ +using System.Runtime.CompilerServices; +using Xunit; + +namespace OpenSim.Region.OptionalModules.ServerSideBaking.Tests; + +/// +/// A for a test that needs a golden reference set's fixtures. +/// The sets live with the compositor's own harness, under +/// Source/OpenSimNGC.Appearance.Baking.Tests/Golden/<set>/fixtures/. They are one resident's real +/// worn assets, so they are not committed and no checkout has them until an operator generates them from their +/// own grid with Golden/fetch-fixtures.sh. +/// A test marked with this attribute is skipped when its set's fixtures are absent, rather than +/// returning early and reporting a pass it never earned. xunit reads at +/// discovery, which is why the check is in the constructor. This mirrors +/// OpenSimNGC.Appearance.Baking.Tests.Golden.GoldenFactAttribute; it is duplicated rather than shared +/// because this project references the baking library, not its test project. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class GoldenFixturesFactAttribute : FactAttribute +{ + /// The Golden/ directory of the baking library's test project, relative to this source file. + private static string GoldenDir([CallerFilePath] string here = "") + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(here)!, "..", "..", "Source", "OpenSimNGC.Appearance.Baking.Tests", "Golden")); + + /// The reference set's directory name, e.g. truly-stock. + public GoldenFixturesFactAttribute(string set) + { + Set = set; + var fixtures = Path.Combine(GoldenDir(), set, "fixtures"); + if (!File.Exists(Path.Combine(fixtures, "avatar.json"))) + { + Skip = $"no fixtures for golden reference set '{set}'. They are one resident's real assets and are " + + $"not committed. To generate them for your own grid: export the variables documented in the " + + $"header of Source/OpenSimNGC.Appearance.Baking.Tests/Golden/fetch-fixtures.sh (grid database " + + $"container and name, a file holding its root password, the Robust asset service URL and a " + + $"simulator asset cache), then run that script with the argument {set}. It writes {fixtures}. " + + $"Nothing is asserted without them."; + } + } + + /// The reference set this test reads. + public string Set { get; } +} From a79634bfb1dcc7ad2403d3979fe8efb02514b57a Mon Sep 17 00:00:00 2001 From: JohnLegionH Date: Sun, 20 Sep 2026 13:17:48 -0500 Subject: [PATCH 8/8] docs: carry the Track L reference set, drop session records The carve brought across every document the two lanes produced, including the ones that only make sense to the grid they were written on. The rule applied here: a document stays if it tells a maintainer or another operator how the feature works, why it is shaped the way it is, or how to verify a deployment. A document that narrates one grid's session history goes. Dropped (they remain on the branch this was carved from): A5-RUN-2026-09-04, A6-LIVE-FAILURE, A7-DUPLICATE-COF, A9-SUITCASE-NOT-DUPLICATE, A10-STEP10-REDIAGNOSIS, A12-ATTACHMENT-RECONCILIATION, A13-STEP10-CLOSED one grid's live runs and the diagnosis threads that closed them. A7 was itself corrected by A9, so it would have shipped a conclusion the next document withdraws. S0a-VERIFICATION, RECON-ssb-appearance, S1b-FIDELITY a branch-versus-upstream reconciliation pass, the first recon (its findings are restated in the addendum, which stays) and one session's fidelity measurements. BUILD-PLAN-ssb-appearance, BUILD-PLAN-track-L-combined schedules: session estimates, branch and worktree layout, deploy order. Neither describes the architecture; the design brief and the ADR set do. Kept: AIS-V3-SPEC, A5-LIVE-CHECKLIST, AUDIT-1-MALFORMED-LLSD, ADR-SET, DESIGN-BRIEF, RECON addendum (the V1-V7 viewer contract the whole design hangs off), FACT-SHEET, S8-WEARABLE-RESOLUTION, T1-TEST-FIXTURES, MORPH-MASK-PASS, THIRD-PARTY-NOTICES. Nine code and test comments cited a dropped document. None had its content deleted: each now states the substance it was pointing at. The A7 citations mattered most, because that document proposed a dedupe its own successor ruled out - the comments now say what an operator should actually do, and the XInventory warning no longer sends them to a file that is not there. Kept documents lost the paths of one machine: a grid root, two worktrees, a separate repository's checkout and the reference trees. The viewer source is named by version rather than by drive letter, which is the part a reader can act on. Section 7.4 of the design brief went with them - it recorded which worktree another repository's project reference pointed at, which is true of nobody else's checkout. --- Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md | 164 ----------- .../ais-v3/A12-ATTACHMENT-RECONCILIATION.md | 248 ----------------- Docs/feature/ais-v3/A13-STEP10-CLOSED.md | 126 --------- Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md | 9 +- Docs/feature/ais-v3/A5-RUN-2026-09-04.md | 155 ----------- Docs/feature/ais-v3/A6-LIVE-FAILURE.md | 117 -------- Docs/feature/ais-v3/A7-DUPLICATE-COF.md | 159 ----------- .../ais-v3/A9-SUITCASE-NOT-DUPLICATE.md | 108 ------- Docs/feature/ais-v3/AIS-V3-SPEC.md | 7 +- Docs/feature/repo-audit/T1-TEST-FIXTURES.md | 2 +- .../ssb-appearance/ADR-SET-ssb-appearance.md | 4 +- .../BUILD-PLAN-ssb-appearance.md | 104 ------- .../BUILD-PLAN-track-L-combined.md | 61 ---- .../DESIGN-BRIEF-ssb-appearance.md | 18 +- .../RECON-ssb-appearance-addendum.md | 9 +- .../ssb-appearance/RECON-ssb-appearance.md | 263 ------------------ .../ssb-appearance/S0a-VERIFICATION.md | 76 ----- Docs/feature/ssb-appearance/S1b-FIDELITY.md | 215 -------------- .../AIS/AISv3Module.cs | 4 +- .../AIS/AisInventory.cs | 14 +- .../AvatarFactory/AvatarFactoryModule.cs | 2 +- .../XInventoryService.cs | 16 +- .../Docs/MORPH-MASK-PASS.md | 2 +- .../THIRD-PARTY-NOTICES.md | 8 +- .../AIS/AisCapRegistrationTests.cs | 7 +- .../AIS/AisDuplicateSystemFolderTests.cs | 6 +- .../AIS/AisResponseLoggingTests.cs | 8 +- 27 files changed, 51 insertions(+), 1861 deletions(-) delete mode 100644 Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md delete mode 100644 Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md delete mode 100644 Docs/feature/ais-v3/A13-STEP10-CLOSED.md delete mode 100644 Docs/feature/ais-v3/A5-RUN-2026-09-04.md delete mode 100644 Docs/feature/ais-v3/A6-LIVE-FAILURE.md delete mode 100644 Docs/feature/ais-v3/A7-DUPLICATE-COF.md delete mode 100644 Docs/feature/ais-v3/A9-SUITCASE-NOT-DUPLICATE.md delete mode 100644 Docs/feature/ssb-appearance/BUILD-PLAN-ssb-appearance.md delete mode 100644 Docs/feature/ssb-appearance/BUILD-PLAN-track-L-combined.md delete mode 100644 Docs/feature/ssb-appearance/RECON-ssb-appearance.md delete mode 100644 Docs/feature/ssb-appearance/S0a-VERIFICATION.md delete mode 100644 Docs/feature/ssb-appearance/S1b-FIDELITY.md diff --git a/Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md b/Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md deleted file mode 100644 index 952c3600ece..00000000000 --- a/Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md +++ /dev/null @@ -1,164 +0,0 @@ -# A10 — re-diagnosing step 10, after A9 removed the original explanation - - -> **ANSWERED 2026-09-04 by A12/A13. Neither hypothesis in §5 was right.** -> -> - **H1 (a response delta the viewer rejected) is dead.** A11's response logging showed the deltas were correct -> and complete: `_removed_items` named the link and `_updated_category_versions` named the COF with its new -> version. Nothing was rejected because nothing was wrong. -> - **H2 (the appearance record never updated) was wrong as framed.** The record was not simply never written — -> the wearables were correct, 7 entries and no dress. H2 assumed a *missing server behaviour*; the truth is a -> *lost write*. -> -> **The cause:** the detach happened and the **deferred appearance save was dropped** because the agent left -> before the five-second timer fired — `AvatarFactoryModule.SaveAppearance`, `sp == null -> continue`. It predates -> AIS, hits the legacy path equally, and is symmetric: wear loses the same way. Diagnosed in -> `A12-ATTACHMENT-RECONCILIATION.md`, fixed in `dc4e417bb3`, closed in `A13-STEP10-CLOSED.md`. -> -> **What this document got right and should be kept for:** take-off is `DELETE /item` and not a slam; COF -> resolution was never wrong; and the passing re-run it examined was not a clean repetition. Those three findings -> stand. Its §5 hypotheses do not. - - -**Date:** 2026-09-04. **Region:** Ebony. **Avatar:** Truly Bazar -(`a7d2ff2e-dc32-44d8-aa61-3d22070a4964`). **Sources:** `OpenSim.Server.RegionServer20260904.log` and the live -database, read-only. - -**Conclusion up front: no single cause is established, and the passing re-run cannot be attributed to the A7 -fix. This session stops without a fix, as the brief requires.** What it does establish is that three of the four -lines of inquiry were aimed at the wrong operation, and it rules several things out with evidence. - -> **Timestamps.** Log times are local; `FROM_UNIXTIME` in the database is UTC, five hours ahead — the dump that -> finished at local 13:33 is stamped `18:33:26`. Both are quoted below in their own clock and labelled. - ---- - -## 1. The operation under test is not a slam - -**Take-off is `DELETE /item/{linkid}` → `RemoveItem`. It is not `SlamFolder`.** - -Every `SlamFolder` in the log is immediately preceded by `GET /category/5d7b7115-…/links`, and -`5d7b7115-edcb-4638-b5f9-196a1dd7aed3` is the asset id of the `AT_LINK_FOLDER` link named **"Truly Base"** — the -saved outfit folder. Fetching an outfit's links and then slamming COF is **wear-outfit** (step 9): - -| time (local) | request | -|---|---| -| 12:33:28,670 | `GET /category/5d7b7115-…/links` → FetchCategoryLinks | -| 12:33:28,720 | `PUT /category/71c3c184-…/links` → **SlamFolder** | -| 12:36:38,536 | `GET /category/5d7b7115-…/links` | -| 12:36:38,588 | `PUT /category/71c3c184-…/links` → **SlamFolder** | -| 12:37:33,205 | `GET /category/5d7b7115-…/links` | -| 12:37:33,239 | `PUT /category/71c3c184-…/links` → **SlamFolder** | - -The take-offs are the `RemoveItem` calls, each followed by a fetch of the garment's own item — and the garment is -the same one every time, `21ae19b0-75a8-41ba-b6d9-1f0472e39437`: - -| time (local) | request | -|---|---| -| 12:36:18,320 | `DELETE /item/dd6ac393-…` → **RemoveItem** | -| 12:37:26,116 | `DELETE /item/92873235-…` → **RemoveItem**, then `GET /item/21ae19b0-…` | -| 12:37:52,105 | `DELETE /item/f1137049-…` → **RemoveItem**, then `GET /item/21ae19b0-…` | - -**Consequence for the brief:** lines of inquiry (a), (b) and (c) all concern the slam's create-then-remove -ordering and its response deltas. That machinery was not on the failing path. It was examined anyway (§3) and is -sound. - -## 2. COF resolution was never wrong — confirmed on both sides of the fix - -Every mutation in the failing window addressed `71c3c184-…`, the correct root COF, never `52c327c4-…`. After the -fix the new WARN prints the resolution explicitly: - -``` -13:11:38,047 WARN [AIS]: agent "a7d2ff2e-…" has 2 folders of type CurrentOutfit - ("52c327c4-cb7d-4365-a7f0-62a6f7545265 v1, 71c3c184-410b-4dae-b20a-855741cf1faf v466"); - using "71c3c184-410b-4dae-b20a-855741cf1faf" version 466 -``` - -Same folder before and after. **The A7 fix changed which folder was chosen in exactly zero cases**, because the -suitcase COF was never a candidate the old code could return (A9). Whatever made step 10 pass on re-run, it was -not this. - -## 3. What was examined and cleared - -| Checked | Finding | -|---|---| -| Slam ordering (`AisSlam.Run :95-147`) | Correct. Creates all, then deletes the prior links by id; on a creation failure it rolls back and reports; the compensating `Rollback :150-156` only ever deletes ids it created in this call, so it **cannot** fire spuriously against a successful removal. | -| `AisEnvelope.IsLink :46-47` | Handles **both** `AT_LINK` (24) and `AT_LINK_FOLDER` (25), so the outfit-folder link is slammed like any other. An earlier suspicion that type 25 was being skipped and accumulating is wrong. | -| `RemoveItem` (`AisHandler.cs:366-382`) | Correct by inspection. Captures `item.Folder` **before** the delete, reports `_removed_items` with the item id and `_updated_category_versions` with the parent's freshly read version — which is exactly what spec §1d-bis requires for `DELETE /item/{id}`. | -| Does an item delete bump the folder version? | **Yes.** `MySqlItemHandler.Delete(string[], string[])` collects the parents and calls `IncrementFolderVersion` for each. (The single-field overload deliberately does not, because it delegates.) So the version the response reports really has moved. | -| `FetchCOF` shape (`FetchLinks :251-271`) | Conformant. Emits `_embedded.links` plus the resolved targets in `_embedded.items`; §1c says the viewer takes a COF's descendent count from `links` alone. | -| Current COF contents | 14 links, **no duplicates**, no surviving skirt link. Nothing was left behind by any removal. | - -Line of inquiry (a) asked for surviving links that should have been removed. **There are none** — but that is -weak evidence, because the current state postdates the re-run and a full slam has rewritten the folder since. - -## 4. The failure signature, and why the re-run does not clear it - -The failure is visible in the log as **`FetchCOF` immediately followed by `CreateInventory` into COF** — the -viewer reading the folder and instantly putting something back: - -| login | FetchCOF | next CreateInventory into COF | gap | -|---|---|---|---| -| **12:38:07 (failing)** | 12:38:10,905 | **12:38:10,944** | **39 ms — automatic repair** | -| 13:11:34 (post-deploy) | 13:11:38,002 | none before the next user action | — | -| 13:14:41 (the "pass") | 13:14:44,639 | 13:15:19,543 | 35 s — a deliberate wear | -| 13:28:13 | 13:28:16,911 | none | — | - -39 ms is not a user. The viewer read COF and repaired it against its own idea of what should be there. - -**And the re-run was not a repetition of the failing sequence.** Between the take-off and the relog that "passed", -a full wear-outfit slam intervened: - -| time (local) | event | -|---|---| -| 13:12:11,289 | `DELETE /item/7235d8bd-…` → RemoveItem (the take-off) | -| **13:13:53,919** | **`PUT /category/71c3c184-…/links` → SlamFolder** — rewrites every link in COF | -| 13:14:23,031 | `DELETE /item/18c41e76-…` → RemoveItem | -| 13:14:41,487 | relog | - -The database confirms the slam rewrote the folder wholesale: thirteen of the fourteen links carry creation -timestamps of `18:13:53`–`18:13:54` UTC (local 13:13:53–54), and only the dress added later differs at `18:15:19`. - -So any stale state left by the 13:12:11 take-off was **erased by a full slam** before the relog that was recorded -as a pass. The pass is therefore consistent with the fix working, with the slam having papered over the problem, -and with the problem being intermittent. **It does not discriminate between them, and an unexplained pass is not -a pass.** - -## 5. What remains, and what would settle it - -Two hypotheses survive. Neither can be confirmed from what exists. - -**H1 — a mutation response delta the viewer rejected.** If the `RemoveItem` response did not take effect in the -viewer's model, the viewer kept the link and restored it at the next login. Against it: the code is correct by -inspection, and checklist step 6 (delete an item, verified with *no duplicate left in the source folder*) exercises -the same route and passed. **Response bodies are not logged**, so the actual delta sent at 12:37:52 cannot be -recovered. - -**H2 — the avatar appearance record was never updated.** OpenSim keeps worn wearables in the avatar appearance -record, separate from the COF links. Under the legacy path a take-off travels as `AgentIsNowWearing` over UDP and -`AvatarFactoryModule` updates that record. Under AIS the viewer removed the COF link over HTTP, and **nothing in -the AIS handler touches appearance** — Ledger P-2 forbids it from taking a `Scene` or `ScenePresence` at all. If -the record still listed the garment, then at the next login the viewer would find itself wearing something with no -COF link and create one, which is exactly the 39 ms repair. This also fits the checklist's own note that Firestorm -has its own outfit machinery and may or may not send `AvatarNowWearing` alongside (SSB ledger A-Q6). Against it: -the region log contains **zero** `AVATAR FACTORY` lines for the whole day, so the module's activity is invisible -here and the hypothesis cannot be tested from this log either. - -**What would settle it, in order of cost:** - -1. **Log AIS response bodies at DEBUG for mutations** (or at least the delta keys and the reported version). The - single missing piece in both hypotheses is what we actually sent back. This is a small change and it is what - A6 taught: a request that arrives and misbehaves must not look the same as one that never arrived. -2. **Reproduce cleanly**: take off one garment, then relog **without** any intervening wear or slam, and watch for - `FetchCOF` → `CreateInventory` within a few tens of milliseconds. That is a definitive, one-minute test, and it - is the run the checklist should have recorded. -3. **Capture the appearance record** (the avatar's serialised wearables) immediately before and after a take-off. - If the garment is still listed after, H2 is confirmed and H1 is dead. - -## 6. Status - -- Step 10 is **not** diagnosed. The A7 explanation is withdrawn (A9) and nothing has replaced it. -- The A7 resolution fix (`6cd13a3645`) stays, on its own merits, but **must not be described as fixing step 10**. -- The A5 run record's step 10 row — "FAILED, then passed after the A7 fix" — is **misleading as to cause** and is - corrected by this document. -- No code was changed in this session. diff --git a/Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md b/Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md deleted file mode 100644 index e24900cf269..00000000000 --- a/Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md +++ /dev/null @@ -1,248 +0,0 @@ -# A12 — reconciling COF changes with attachment state: design brief - - -> **CONFIRMED and ACTED ON, 2026-09-04 (A13).** §2's finding — the detach happened and the deferred appearance -> save was lost — is the cause, and it is fixed: `dc4e417bb3` flushes a pending save on `OnRemovePresence`, -> deployed in merge `bfb50070d8`. Checklist step 10 then passed on a clean run and is closed -> (`A13-STEP10-CLOSED.md`). -> -> **Still outstanding from §4 and §5**, unaffected by that fix: the viewer skips its removal arm entirely when -> `isFullyLoaded()` is false and never retries, and an offline agent has no viewer to reconcile at all — the case -> Phase 2's Robust hosting makes normal. Option **B3, login-time reconciliation**, remains the standing -> recommendation. - - -**Design only. No behaviour changed this session.** - -> **The brief's premise does not survive the evidence, and the correction matters more than the design.** -> -> The brief states: *"AIS removed the COF link; nothing detached the object."* **The object was detached.** What -> was lost is the *appearance record write* that should have recorded the detach — dropped by a five-second -> deferred save that raced the logout. See §2. -> -> That failure has **nothing to do with AIS**. It reproduces on the legacy path, it reproduces on wear as well as -> take-off, and it has been in the tree since long before this branch. AIS made it visible because AIS made -> take-off fast and quiet. -> -> There is still a genuine AIS-shaped gap (§3) — it is just not what broke step 10. - ---- - -## 1. Part 1(a) — how a detach normally happens, and every store it writes - -The viewer sends UDP, and the region does the rest: - -| # | Step | Where | -|---|---|---| -| 1 | `DetachAttachmentIntoInv` / `ObjectDetach` packet arrives | `LLClientView.cs:8434`, `:8436` → `HandleDetachAttachmentIntoInv :9267`, `HandleObjectDetach :9289` | -| 2 | Raised as `OnDetachAttachmentIntoInv` / `OnObjectDetach` | `LLClientView.cs:89`, `:91` | -| 3 | Subscribed by the attachments module | `AttachmentsModule.cs:975-976` | -| 4 | Resolved to a `SceneObjectGroup` and dispatched | `Client_OnObjectDetach :1480-1491`, `Client_OnDetachAttachmentIntoInv :1493-1510` (matches on `group.FromItemID`) | -| 5 | The detach proper | `DetachSingleAttachmentToInv :892-952` | - -**The four stores a detach writes**, all inside `DetachSingleAttachmentToInv`: - -| Store | Call | Persisted by | -|---|---|---| -| **A. In-memory appearance** | `sp.Appearance.DetachAttachment(so.FromItemID)` (`:945`) | nothing on its own — memory only | -| **B. The `Avatars` table (`_ap_*` rows)** | `m_scene.AvatarFactory.QueueAppearanceSave(sp.UUID)` (`:947`) | **deferred**; see §2 | -| **C. The ScenePresence's attachment list** | `sp.RemoveAttachment(so)` (`:949`) | memory only | -| **D. The scene object + its inventory asset** | `UpdateDetachedObject(sp, so, scriptedState)` (`:950` → `:1217-1247`) | `m_scene.DeleteSceneObject` (`:1232`) then `UpdateKnownItem` (`:1246`) | - -Store **B** is the one that matters here, and it is the only one that is not written synchronously. - -## 2. Part 1(b) — what the viewer expects, and what actually failed - -### The viewer reconciles attachments itself. It is not asking the server to. - -Taking off an attachment does **not** send a detach directly: - -```cpp -// LLAppearanceMgr::removeItemsFromAvatar — llappearancemgr.cpp:4204-4232 -LLPointer cb = new LLUpdateAppearanceOnDestroy(true, true, post_update_func); // :4214 -... -if (item && item->getType() == LLAssetType::AT_OBJECT) - LL_DEBUGS("Avatar") << "ATT removing attachment " ... ; // :4220-4223 — logs only -... -removeCOFItemLinks(linked_item_id, cb); // :4228 -``` - -`removeCOFItemLinks` (`:3239-3266`) deletes the COF link — `remove_inventory_item(..., true)` for `AT_OBJECT` -(`:3253`), the immediate variant. **That is the `DELETE /item` we saw.** No detach message is sent here. - -The detach comes from the **callback**. When the link deletes complete, `LLUpdateAppearanceOnDestroy` runs -`updateAppearanceFromCOF`, which diffs the COF against what is actually worn and acts: - -```cpp -// llappearancemgr.cpp:2631-2673 -LLAgentWearables::findAttachmentsAddRemoveInfo(obj_items, objects_to_remove, objects_to_retain, items_to_add); -... -// (don't remove attachments until avatar is fully loaded - reduces random attaching/detaching/reattaching at log-on) -if (gAgentAvatarp->isFullyLoaded()) // :2654 -{ - LLAgentWearables::userRemoveMultipleAttachments(objects_to_remove); // :2656 <- the detach -} -... -LLAgentWearables::userAttachMultipleAttachments(items_to_add); // :2673 <- the attach -``` - -So the contract is: **COF is the source of truth, and the viewer — not the server — reconciles the objects to -it.** The server's job is only to accept the link change and then to handle the ordinary detach/attach messages -the viewer sends afterwards. - -The opposite direction confirms the same model. When the *server* detaches something, the viewer tidies COF -itself — `unregisterAttachment` (`:4459-4479`) calls `onDetachCompleted` and then `removeCOFItemLinks(item_id)` -(`:4471`). And `getIsProtectedCOFItem` (`:4502-4531`) refuses raw deletion of a COF link — *"force users to -choose 'Detach' or 'Take Off'"* — precisely so the removal goes through `removeItemFromAvatar`, which is the -function above. - -**Answer to the question the brief poses:** it is **neither** a missing server behaviour nor a viewer message we -failed to handle. The viewer sent the detach, we handled it, and we then lost the write. - -### What actually failed, from the log - -| time | event | source | -|---|---|---| -| 14:09:35,236 | `DELETE /item/dfcc1be2-…` → RemoveItem | AIS | -| 14:09:35,267 | `RemoveItem -> 200 _removed_items=[dfcc1be2-…] _updated_category_versions={71c3c184-…:501}` | AIS — **correct** | -| ~14:09:35,3xx | viewer's `updateAppearanceFromCOF` → `userRemoveMultipleAttachments` → UDP detach | viewer | -| 14:09:35,408 | `[ATTACHMENTS MODULE]: Updating asset for attachment c91d9878-…, attachpoint 18` | `UpdateKnownItem` (`AttachmentsModule.cs:1031`) — **the detach's own object save** | -| ~14:09:40 | **logout** (`[JANUS PLUGIN] Detach. Detached` 14:09:40,012) | region | -| 14:09:40,0–,2 | `Updating asset for attachment` for attachpoints **11, 40, 7, 8** | `DeRezAttachments :533-561` | - -**Attachpoint 18 is absent from the logout batch.** The dress was no longer attached in the scene — because it -had already been detached five seconds earlier. Yet the database still holds `_ap_18 = 9bec8993-…`. - -The reason: - -```csharp -public void QueueAppearanceSave(UUID agentid) // AvatarFactoryModule.cs:334-342 -{ - long timestamp = DateTime.Now.Ticks + Convert.ToInt64(m_savetime * 1000 * 10000); // m_savetime = 5 (:51) - m_savequeue[agentid] = timestamp; - m_updateTimer.Start(); -} - -private void SaveAppearance(List ids) // :811-830 -{ - foreach (UUID id in ids) - { - ScenePresence sp = m_scene.GetScenePresence(id); - if (sp == null) - continue; // :818-819 <- the write is DROPPED, silently - SetAppearanceAssets(id, sp.Appearance); - m_scene.AvatarService.SetAppearance(id, sp.Appearance); // :828 <- the ONLY persist for a live avatar - } -} -``` - -The detach queued a save for ~14:09:40.4. The avatar logged out at ~14:09:40.0. When the timer fired, -`GetScenePresence` returned null, the loop `continue`d, and **the only write that would have cleared `_ap_18` -never happened**. - -**Nothing flushes it at logout.** `DeRezAttachments` (`:533-561`) saves each attachment *object* through -`UpdateDetachedObject`, and never touches `sp.Appearance` or `AvatarService`. `AvatarFactoryModule.cs:828` is the -only appearance persist for a live avatar in the whole tree — the other `SetAppearance` callers are -`RemoteAdminPlugin` and account creation. - -**So: a five-second deferred write, dropped without a log line, with no flush on the path that ends the session.** - -### Why it looked like an AIS problem - -Under AIS, take-off is one small HTTP call and the user is free to log out immediately. The legacy path had the -same race, but a UDP take-off is usually followed by more UDP traffic and a slower user. It is a pre-existing -defect that AIS exposed, in the same way A7's `folders[0]` was a pre-existing fragility. - -## 3. Part 1(c) — SL parity - -SL has no separate attachment store to fall out of step: the COF *is* the record, and the simulator's attachment -state is derived from it. That is why the viewer is written to reconcile against COF and why LL never needed a -"COF changed → detach" server behaviour. - -**Parity therefore does not require us to reconcile at mutation time.** It requires that our derived stores — -`Avatars._ap_*`, `ScenePresence`, the scene object — never disagree with COF once the dust settles. Today they -can, in two ways: - -1. the write that records agreement is lost (§2); and -2. nothing reconciles when no viewer is there to do it (§4). - -## 4. Part 1(d) — the same gap on wear, on slam, and when nobody is reconciling - -**Wear is symmetric.** `AttachmentsModule.cs:1389` sets `sp.Appearance.SetAttachment(...)` and `:1396` queues the -same deferred save. Attach something and log out within five seconds and the record is lost the same way — the -avatar comes back *without* the attachment, the mirror of this bug. - -**Slam is the same code path, multiplied.** `PUT /category/current/links` replaces every link, so -`updateAppearanceFromCOF` computes a large `objects_to_remove` / `items_to_add` and issues many detaches and -attaches, each queueing a save that collapses to one timestamp. One logout inside the window loses the lot. - -**Two holes the viewer cannot cover at all:** - -- **`isFullyLoaded()` is false** (`llappearancemgr.cpp:2654`). The removal arm is skipped entirely and **never - retried** — the COF link is gone, the object stays attached, and no message is ever sent. This is a real - second failure mode with the same visible symptom, and no amount of fixing the save will address it. -- **The agent is not logged in.** There is no viewer to reconcile. Today AIS only serves a region-hosted, - logged-in agent, so this is latent — but **Phase 2 hosts the handler on Robust, where an offline agent is the - normal case, not the exception**. A COF mutation arriving for an offline agent must still leave the stored - appearance consistent, or the avatar rezzes wrong on next login and "repairs" itself back to the old outfit. - ---- - -## 5. Part 2 — options - -**These are two problems and they want different answers. Do not let the second pay for the first.** - -### Problem A — the lost appearance write (the actual cause of step 10) - -| Option | Cost | Forecloses | -|---|---|---| -| **A1. Flush the save queue when the presence closes.** Drain `m_savequeue` for that agent on `OnRemovePresence` / client logout, before the ScenePresence is torn down. | Small and local to `AvatarFactoryModule`. One event subscription plus a synchronous save. | Nothing. | -| **A2. Persist synchronously on detach/attach** instead of queueing. | A DB write per attachment operation; a slam becomes N writes. Was presumably why the queue exists. | The batching the queue buys. | -| **A3. Make `SaveAppearance` not need the presence** — capture the `AvatarAppearance` at queue time rather than dereferencing `sp` at fire time. | Small, but changes save semantics: it would persist a snapshot rather than the latest state. | Coalescing later changes into one write. | -| **A4. Log the drop and do nothing else.** | Trivial. | Nothing — but it fixes nothing either. | - -**Recommendation: A1, plus the WARN from A4.** It closes the race at the exact point the race exists, keeps the -batching, and needs no new architecture. A3 is a reasonable belt-and-braces addition later. - -**What breaks if A1 is wrong:** a flush on logout writes appearance one extra time per session. If the in-memory -appearance were somehow *worse* than the stored one, we would persist the worse one — so the flush must write -only when the queue actually holds an entry for that agent, i.e. only when something really did change. - -### Problem B — reconciling COF when no viewer will - -This is the one P-2 constrains. **P-2 says the AIS handler may take only an agent id, an `IAisInventoryBackend` -and the request.** Every option below preserves that; they differ in where the knowledge lives. - -**First, what the tree already has: nothing.** There is no listener for COF changes anywhere. -`grep FolderType.CurrentOutfit` across `OpenSim.Region.CoreModules` returns two hits, both in -`AvatarFactoryModule` (`:1097`, `:1112`), and both are *writes* — the "Failed Wearable Replacement" path creating -a link. Nothing subscribes to, or notices, a COF change. There is no existing seam to reuse. - -| Option | Cost | Forecloses | -|---|---|---| -| **B1. Event/queue the region subscribes to.** The handler publishes "COF changed for agent X" to an abstraction it is given; the region module subscribes and reconciles against the live `ScenePresence`. | A new interface plus a region-side consumer. In-process it is an event; on Robust it needs a real transport. | Nothing structurally — this is the option that survives Phase 2 intact. Handler stays Scene-free. | -| **B2. Narrow seam: one capability, detach/attach by item id for a present agent.** | Smallest code. | **A lot.** It only works for a present agent, so Phase 2 gains nothing, and the moment the interface exists it will grow. It also re-introduces, in spirit, the coupling P-2 exists to prevent. | -| **B3. Reconcile at login.** On presence creation, diff stored appearance against COF and correct. | Cheap, entirely region-side, no handler change at all, and **it is the only option that fixes an offline mutation**. | Leaves a present avatar visibly wrong until relog. | -| **B4. Do nothing; rely on the viewer.** | Free. | Accepts both holes in §4 — the `isFullyLoaded` skip and the offline agent. | - -**Recommendation: B3 now, B1 when Phase 2 lands. Not B2.** - -- **B3 first** because it is the only option that covers the offline case, which is Phase 2's normal case, and - because it is a pure region-side addition with no AIS or P-2 impact. It also happens to be a safety net for - the `isFullyLoaded` skip and for anything else that leaves the stores disagreeing: at every login, COF wins. -- **B1 when the handler actually needs to reach a live region**, i.e. when Phase 2 makes "the region that has - this agent" a different process. Designing it before then risks building the wrong transport. -- **Not B2.** A capability that only works for a present agent buys nothing for the case that is about to become - normal, and it spends P-2 to get there. - -**What breaks if B3 is wrong:** login-time reconciliation makes COF authoritative over the appearance record. -If COF is ever *itself* wrong — a partial slam, a failed create — we would faithfully reproduce the wrong -outfit and, worse, overwrite a correct appearance record with it. Two guards: reconcile only when the two -disagree, and never strip on an empty or unreadable COF (the same "never trust an empty result" rule that S1e -applied to bake channels). An avatar that logs in with an unreadable COF must keep what it had. - -## 6. What this session did not do - -No behaviour changed, no test written, nothing deployed, the database untouched. `A-Q17` should be closed with -the §2 finding, and two new items opened — the lost appearance write (Problem A) and the reconciliation gap -(Problem B) — but the brief asked for a design and a stop, so those edits are left for the session that acts. diff --git a/Docs/feature/ais-v3/A13-STEP10-CLOSED.md b/Docs/feature/ais-v3/A13-STEP10-CLOSED.md deleted file mode 100644 index 1ac930d71f8..00000000000 --- a/Docs/feature/ais-v3/A13-STEP10-CLOSED.md +++ /dev/null @@ -1,126 +0,0 @@ -# A13 — step 10 closed, A-Q17 answered, T-2 deployed - -**Date:** 2026-09-04. Docs only. - ---- - -## 1. Step 10 passes, on a clean run, with the cause fixed - -**Build `1.1.202-alpha+bfb50070d8`** — the merged AIS + appearance-flush tree. No slam between the take-off and -the relog, and ~20 seconds in-world before logging out. - -| time | line | -|---|---| -| 17:15:18,402 | `DELETE /item/a4cb683b-…` → RemoveItem | -| 17:15:18,432 | `RemoveItem -> 200 _removed_items=[a4cb683b-…] _updated_category_versions={71c3c184-…:503}` | -| ~17:15:23 | the 5 s timer fires and writes — **inferred**: no drop WARN, and the change persisted | -| 17:15:38 | logout; attachpoints 40, 7, 8 saved; **no flush line** | -| relog | **the dress stayed off** | - -**The absent flush line is the cost guarantee, not a failure.** `FlushAppearanceSaveOnClose` writes only when the -queue actually holds an entry — *a close with nothing queued writes nothing*, which has a test of its own. The -timer had already drained the queue at ~17:15:23, fifteen seconds before logout, so there was nothing to flush. -The flush exists for the other case: a logout **inside** the five-second window. That is what happened at -14:09:40 on the failing run, and that is what lost the dress. - -`A5-RUN-2026-09-04.md` is updated: step 10 **pass**. *(Tally revised again by A15, once step 7 was found to be -unreachable through the viewer: **12 pass, 1 not reachable (7), 2 not run (14, 15)**.)* - -## 2. A-Q17 answered — and neither hypothesis was right - -A10 left two hypotheses. Both are now dead. - -| | Hypothesis | Verdict | -|---|---|---| -| **H1** | a mutation response delta the viewer rejected | **Dead.** A11's logging shows the deltas were correct and complete: `_removed_items` named the link, `_updated_category_versions` named the COF with its new version. Nothing was rejected because nothing was wrong. | -| **H2** | the avatar appearance record was never updated | **Wrong as framed.** The record was not simply never written — the wearables were correct (7 entries, no dress). H2 assumed a *missing server behaviour*; the truth is a *lost write*. | - -**The actual cause (A12):** the detach happened — the viewer reconciles attachments itself in -`updateAppearanceFromCOF` (`llappearancemgr.cpp:2656`) and sent it, the region handled it, and attachpoint 18 was -absent from the logout save batch. What was lost is the **deferred appearance save**: -`AvatarFactoryModule.QueueAppearanceSave` defers by `m_savetime` (5 s) and `SaveAppearance` then does -`sp == null -> continue`, dropping the write silently when the agent has already left. Nothing flushed it — -`DeRezAttachments` never touches appearance. - -It **predates AIS**, hits the **legacy path equally**, and is **symmetric**: wear queues the same deferred save -and loses it the same way, so an attachment worn just before logout comes back missing. - -### The working lesson - -**Three successive diagnoses were aimed at the wrong store, and each was corrected by one read-only query or one -log line.** - -| # | Diagnosis | Store blamed | What corrected it | Cost | -|---|---|---|---|---| -| 1 | A7 | COF resolution — the agent has two type-46 folders and we pick the wrong one | one `SELECT` showing the version-1 folder is parented to `My Suitcase`, so the query could never have returned it (A9) | a fix, a deploy, and a dedupe plan that would have deleted live suitcase skeletons | -| 2 | A10 | the response delta, then the appearance record | the A11 response logging, which showed the deltas were correct (A13) | a session | -| 3 | A12 | attachment reconciliation — "nothing detached the object" | the logout save batch, which listed attachpoints 11/40/7/8 but **not** 18, proving the object *had* been detached | caught in-session, before any code | - -Each wrong turn rested on **one unverified observation** carried in as fact: "AIS returned the version-1 folder", -"the response must have been wrong", "nothing detached the object". None was checked; each was cheap to check. - -**The rule worth keeping: verify the single observation the argument rests on, before building on it.** The tell -is an argument whose whole structure depends on one premise nobody has measured — especially when that premise -arrived in the framing of the problem rather than from the evidence. - -A related habit that paid for itself: when a diagnosis cannot be separated from its alternatives, **add the -missing evidence rather than another theory**. A11 was a session spent only on logging, and it settled a question -two prior sessions could not. - -## 3. T-2 — the fix and its deploy - -**`dc4e417bb3`** on `fix/appearance-save-flush`: - -- flushes a pending appearance save on `EventManager.OnRemovePresence`, raised at `Scene.cs:3866` while the - presence is still resolvable — it is not removed until `:3898` nor disposed until `:3905`; -- **skipped for child agents**, so a teleport cannot publish a stale outfit over a newer one the destination has - since saved; on a teleport the source's root is converted by `MakeChildAgent`, not `RemoveClient`, and the - appearance travels in the agent data; -- **WARN on both drop paths**, so this cannot fail silently again; -- **6 tests, 4 of which fail without it**; the other 2 assert the cost guarantee. - -It also added the `DisableTestParallelization` declaration `OpenSim.Region.CoreModules.Tests` was missing — the -same one, for the same reason, already in `OpenSim.Region.Framework.Tests`. That project was order-dependent and -flaky: a full run failed **9 tests, but not the same 9**, with failures appearing inside `SceneHelpers.SetupScene` -rather than in any assertion. It is now deterministic at **5 failed / 84 passed**. Those 5 are pre-existing and -are **T-3's job**. - -**Deployed** as merge **`bfb50070d8`** on `integration/ais-appearance` — merge-base `11a2456833`, clean, zero -conflicts, no file overlap between the branches. Backup at -`D:\legiongrid\_backup\regionserver-20260904-1703\`. Both features verified present in the **deployed** binaries: -`InventoryAPIv3` and the mutation-delta line in `LindenCaps.dll`, the flush and both WARN lines in -`CoreModules.dll`. - -## 4. Open items — enough context to pick up cold - -~~**1. The live region runs a commit reachable from only one branch.**~~ **DONE 2026-09-04 (A14).** -`fix/appearance-save-flush` merged into `feature/ais-v3`; the merged tree differs from the deployed -`bfb50070d8` only in `Docs/`, so the live binaries are reproducible from the branch. `integration/ais-appearance` -deleted and `D:\tranq-integration` removed. The deployed merge is not an ancestor of the new HEAD, so it is -tagged `deployed/region-2026-09-04` to keep the binaries' `+bfb50070d8` stamp resolvable. - -~~**2. Robust redeploy, still blocked.**~~ **DONE 2026-09-04 (R1).** Merged as `a2c8fb63f3` and deployed; -Robust runs one commit where it ran four, and restarted cleanly at 19:03:58 with trusted-hypergrid loaded. -`ONLYIFTRASH` and `EnsureSystemFolder` are live. **Checklist step 7 was never actually blocked by this** — it is -not reachable through any viewer (A15). - -**3. A12's remaining holes.** The viewer skips its removal arm entirely when `isFullyLoaded()` is false -(`llappearancemgr.cpp:2654`) and **never retries**; and an offline agent has no viewer to reconcile at all, which -Phase 2's Robust hosting makes the normal case. A12 option **B3 — login-time reconciliation** — is the standing -recommendation, with the guard that it must reconcile only on disagreement and **never strip on an empty or -unreadable COF**. - -**4. A-Q16 open.** For a local user, AIS's system-folder resolution scans every type-46 folder including the -suitcase's. Suitcase COFs sit at version 1 so the root always wins today, but nothing enforces it. - -**5. Checklist steps 14 and 15 unrun** — HG folder deletion, and folder thumbnail/favourite. Both are documented -limitations rather than suspected defects. - -**6. T-3: the 5 residual `CoreModules` failures**, now stable and attributable — 2 asset-store assertions in -`AvatarFactoryModuleTests`, 3 IAR loader tests. They were hidden behind the flakiness until T-2 made the project -deterministic. - -**7. Backup integrity, unaudited.** Any `.sql` in `D:\legiongrid\_backup\` written through a PowerShell text -pipeline is **corrupt and unrestorable** — `Set-Content` re-encodes the byte stream and replaces every byte that -is not valid text, which mangles binary column data. `legiongrid-predupe-20260904-1332.sql` was re-taken by shell -redirection and is good (2,685,971,589 bytes, no BOM). **An audit of the rest of that folder has not been run.** diff --git a/Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md b/Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md index 1ea0765bf15..6d19c001893 100644 --- a/Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md +++ b/Docs/feature/ais-v3/A5-LIVE-CHECKLIST.md @@ -401,10 +401,9 @@ impossible on a single Robust. ### 22. Two slams at once on Truly's COF: one outfit wins, never both -**Do:** as **Truly**, run `two-slam-race.sh` from the AIS-SEC-3 handoff -(`D:\legiongrid\_ops\handoffs\HANDOFF-AIS-SEC-3-20260912.md`). Fill in only the **cap path** and the **two -link sets**; the script targets `category/current/links`, so the COF id is resolved server-side and is not a -placeholder. It fires both `PUT`s concurrently and prints both status codes. +**Do:** as the test avatar, fire two `PUT`s at `category/current/links` concurrently, each with a different +link set, and print both status codes. Any two-process shell will do; the route resolves the COF id +server-side, so only the cap path and the two link sets have to be filled in. **Expected:** @@ -506,7 +505,7 @@ and there is no control.** Firestorm remains a test client and never an authority (Ledger P-1). What changes is the reading of a green result: it means Firestorm is satisfied, not that the protocol is right. Anything observed only in Firestorm must be checked against the LL viewer source before it is relied on — step 13's legacy fallback is the live -example (`A5-RUN-2026-09-04.md`). These are the steps where Firestorm's own machinery differs most, so they +example. These are the steps where Firestorm's own machinery differs most, so they carry the least transferable evidence: - **1** (full load) — Firestorm's fetch pacing differs; diff --git a/Docs/feature/ais-v3/A5-RUN-2026-09-04.md b/Docs/feature/ais-v3/A5-RUN-2026-09-04.md deleted file mode 100644 index 80a8ea7628a..00000000000 --- a/Docs/feature/ais-v3/A5-RUN-2026-09-04.md +++ /dev/null @@ -1,155 +0,0 @@ -# A5 checklist — live run of 2026-09-04 - -**Region:** Ebony, `[Ebony] AIS_Enabled = true` (`config/OpenSim.ini:163`, section opens `:162`). -**Build:** `1.1.191-alpha+e800eb61b6` for the passing re-run of step 10; steps 1–9 and 11–13 were run against -`1.1.188-alpha+ee6d2a3554`, which differs only in the A7 resolution fix. -**Viewer:** Firestorm 7.2.5. **Avatar:** Truly Bazar (`a7d2ff2e-dc32-44d8-aa61-3d22070a4964`). - -**This is the first end-to-end run in which AIS actually served traffic.** The run before it (A6) never reached -the handler: every request 404'd inside the HTTP server. - ---- - -## Standing constraint — the stock LL viewer cannot be used on this grid - -**Every live checklist from here on is a Firestorm result, and there is no stock-viewer control.** The stock -Second Life viewer will not start against this grid: its Vivox voice component refuses to initialise outside -SL, and that stops the viewer coming up at all. It is not a matter of preference or convenience — the control -run is unavailable. - -This does not change Ledger **P-1**: the LL viewer *source* remains the authority for the AIS v3 protocol, and -Firestorm remains a test client only. What changes is what a green checklist is worth. A pass here means "Firestorm -is satisfied", not "the protocol is right": - -- Where Firestorm and the source disagree, **the source wins** and the difference is recorded, never fixed - against Firestorm. -- Any behaviour observed **only** in Firestorm — step 13's fallback is the important one — must be checked against - the LL viewer source before it is relied on, because Firestorm may have its own path where the LL viewer has - none. -- Behaviour the LL viewer has and Firestorm does not will simply never be exercised here. That is an accepted, - permanent gap in this grid's coverage, not a to-do. - ---- - -## Results - -| # | Step | Result | Observed | Evidence | -|---|---|---|---|---| -| 1 | Full inventory load after cache clear | **pass** | The tree filled in and the item count settled. **Each folder was fetched exactly once** — no folder re-requested. | The A-R3 signature (a folder requested over and over because it was returned without all three `_embedded` collections) did **not** appear. | -| 2 | Open a deep folder | **pass** | Nested folders four levels deep both create and open: `Objects/Test/Test2/Test3`. | Created and opened in-world during the run. | -| 3 | Current outfit reads back | **pass** | Appearance → Wearing lists every worn item under its real name, not "(loading)". | `GET /category/current/links` carried the link targets in `_embedded.items`. | -| 4 | Rename an item | **pass** | A linkset was renamed; the new name survived a relog. | `PATCH /item`. | -| 5 | Rename a folder | **pass** | Folder rename via `UpdateCategory`; survived a relog. | `PATCH /category`. See the double-send note below — it is not a fault. | -| 6 | Delete an item | **pass** | The item was in Trash after a relog, **and no duplicate was left behind in the source folder** — the removal delta was correct. | The stale-copy-in-place symptom would be a missing `_category_items_removed` entry (spec §1d-bis). It did not occur. | -| 7 | Delete a folder **outside** Trash | **not reachable** | **A resident cannot request this.** The viewer's only folder-removal routes are move-to-Trash (a `PATCH`, not a delete), purge a single item in Trash, and Empty Trash. There is no shift-delete for folders. | Verified in-world 2026-09-04: two folders deleted, one nested and one at root — **both moved to Trash** and were still there after a restart. The day's AIS log shows `CreateInventory` and `UpdateCategory`, and **no `RemoveCategory` at any point**. A2b's `ONLYIFTRASH` remains correct and is now live, but was never gating a resident-visible operation. | -| 8 | Empty Trash | **pass** | Trash emptied **completely, including the contents of a subfolder**, and stayed empty through a relog. | Confirms the purge enumeration against real content: unlike a folder delete, nothing viewer-side sweeps the children, so the response has to **enumerate** the direct children itself (spec §1d-bis). A partial empty would have left survivors named in the response; none were left. | -| 9 | Wear an outfit (slam) | **pass** | An outfit was saved and then worn. The slam fired and the outfit applied. The avatar was gray for a few seconds while **Firestorm rebaked client-side**. | The sim did **not** bake, and there is no `[SSB]` line — **correct**: SSB's own triggers are not built yet (that is the L-2 track), so a client-side rebake is the expected path today. Not a defect and not evidence about SSB either way. | -| 10 | Take off a garment | **pass** — on the third attempt, with the cause fixed | Clean run on `1.1.202-alpha+bfb50070d8`: garment removed, ~20 s in-world, logout, relog — **it stayed off**. No slam in between. | Cause established and fixed: not COF resolution (A7, withdrawn by A9/A10) and not a rejected delta, but a **deferred appearance save dropped when the agent left inside the 5 s window** (`A12`, fixed in `dc4e417bb3`). See the clean-run trace below and `A13-STEP10-CLOSED.md`. | -| 11 | Create a folder | **pass** | The folder appeared, kept its name and survived a relog. | `POST /category`. | -| 12 | Copy a library outfit | **pass** | A Library item copied into inventory, is **wearable**, and its permissions came across intact. | `COPY /category` with the destination in the `Destination` header. Permissions intact means the copy carried the source's own masks rather than `NextPermissions`. | -| 13 | Create an inventory **item** | **pass**, and see below | New Notecard: created normally over the legacy UDP path; a resident sees nothing wrong. **AIS was never asked** — corrected by A11, see below. | The outcome the checklist called "the good outcome and worth recording", reached by a route the checklist did not anticipate. | -| 14 | Hypergrid folder deletion — expected refusal | **not run** | Not exercised; no HG visitor in the run. | Known limitation: `HGInventoryService` / `HGSuitcaseInventoryService` answer NOGO for folder deletion whatever the flag says. | -| 15 | Folder thumbnail and favourite — silently dropped | **not run** | Not exercised. | Known limitation: `InventoryFolderBase` has no column for either, so both are accepted and dropped. | - -**Tally:** 12 pass, **1 not reachable (7)**, 2 not run (14, 15). - -### Step 10, closed — the clean run - -Two earlier "passes" were not clean: the first was attributed to a COF-resolution fix that turned out to fix -nothing here, and the second had a full wear-outfit slam between the take-off and the relog, which rewrote COF and -masked the state. This one has neither. - -**Build `1.1.202-alpha+bfb50070d8`** (AIS plus the appearance-save flush), no slam, ~20 s in-world before logout: - -| time | line | -|---|---| -| 17:15:18,402 | `DELETE /item/a4cb683b-…` → RemoveItem | -| 17:15:18,432 | `RemoveItem -> 200 _removed_items=[a4cb683b-…] _updated_category_versions={71c3c184-…:503}` | -| ~17:15:23 | the 5 s timer fires and writes — inferred: no drop WARN, and the change persisted | -| 17:15:38 | logout; attachpoints 40, 7, 8 saved; **no flush line** | -| relog | **the dress stayed off** | - -**The absent flush line is the fix working, not the fix missing.** `FlushAppearanceSaveOnClose` only writes when the -queue actually holds an entry — "a close with nothing queued writes nothing" is its cost guarantee, and it has a -test of its own. Here the timer had already drained the queue at ~17:15:23, fifteen seconds before logout, so there -was nothing left to flush. The flush exists for the *other* case: a logout **inside** the five-second window, which -is exactly what happened at 14:09:40 on the failing run and is what lost the dress. - ---- - -## Step 13 removes the main argument against widening the flag - -The A5 checklist named step 13 as **"the single biggest argument against flipping this flag more widely"**: the -viewer's own AIS path for notecard creation is compiled out (`USE_AIS_FOR_NC`) and expects the server to mint the -asset, which this region does not do, so the route answers 501. The open worry was that the viewer would treat the -501 as a hard failure and the notecard would simply never appear. - -**It does not — and the reason is better than a fallback.** The item is created over the legacy -`CreateInventoryItem` UDP path and the resident sees an ordinary notecard and no error. - -**Corrected by A11: AIS is never asked.** This section previously said "AIS answered 501, and the viewer fell back". -That was an inference, and it is wrong. In `create_inventory_item` -(`F:\viewer-develop\indra\newview\llviewerinventory.cpp`) the entire AIS arm is inside -`#ifdef USE_AIS_FOR_NC` (`:1120`-`:1166`), and that macro is **not defined**. The arm is compiled out, so control -falls unconditionally to `msg->newMessageFast(_PREHASH_CreateInventoryItem)` at **`:1169`**. There is no runtime -decision and therefore no fallback: item creation simply never goes through AIS. - -**The log confirms it.** At the notecard creation there is exactly one AIS line, and it is not a create: - -| time | line | -|---|---| -| 13:19:56,389 | `[INVENTORY ACCESS MODULE]: Received request to create inventory item "New Note" in folder "bb7d5f74-…", transactionID "00000000-…"` | -| 13:19:56,467 | AIS **`FetchItem`** on the newly created item — the viewer syncing the result | - -No `POST /category/{id}` → `CreateInventory` appears anywhere in that window. Our 501 route was never reached, so -the "AIS answered 501" claim had no evidence behind it and has been removed. - -So: the legacy UDP path really did create the item server-side, and the viewer then pulled it back through AIS. - -So the 501 is a **degraded path, not a broken one**, and the specific objection recorded in the checklist is -withdrawn. Two limits on that conclusion, both real: - -- ~~Whose fallback it is remains unconfirmed~~ — **closed by A11 (A-Q15).** The code is in the **LL tree**, not - Firestorm's: `llviewerinventory.cpp:1120` opens the `#ifdef USE_AIS_FOR_NC` arm and `:1169` is the legacy send - that follows it. Because the macro is undefined the arm never compiles, so **every** viewer built from this - source — stock included — creates items over UDP and never asks AIS. This is stronger than a fallback holding - under test: there is no branch to take. Stock-viewer residents get exactly the behaviour observed here, and - P-3's "no control run" caveat does not bite, because the answer came from the source rather than from a run. -- It removes *this* objection **fully**, for every viewer rather than just for Firestorm. It still does not make - the case for widening on its own — steps 7, 14 and 15 are unrun, and A-R1 (no - fallback for deletes, purges and slams — those really are AIS-only) is untouched by it. - ---- - -## Firestorm sends `UpdateCategory` twice for one folder rename - -A single folder rename produced **two identical `PATCH /category` requests**, milliseconds apart: - -| | | -|---|---| -| first | 11:52:32 | -| second | 11:52:41 | - -**Harmless, and not a loop.** The operation is idempotent: the second request sets the folder to the name it -already has and produces the same delta. It is recorded here and as ledger **A-Q14** for one reason — so that a -later reader looking at a log full of doubled `PATCH /category` lines does not mistake it for the A-R3 re-fetch -loop, which looks superficially similar and is a genuine fault. The distinguishing feature is that A-R3 repeats -*without bound* while this repeats exactly once. - -No action taken. It is a client behaviour, not a server one, and nothing on the server should be keyed to it -(Ledger P-1). - ---- - -## What this run does and does not establish - -**Establishes:** AIS serves real inventory traffic to a real viewer for reads, renames, deletes, purges, outfit -slams, folder creation and library copies, with correct deltas and no re-fetch loop. Step 8 in particular confirms -the purge enumeration against real nested content, and step 13 confirms server-side that a 501 falls back to a -real legacy creation rather than a silent loss. - -**Does not establish:** anything about the stock LL viewer, including **whose** fallback step 13 exercised (A-Q15); -anything about steps 7, 14, 15; anything about -behaviour under concurrent users — the run was one avatar; or that the A7 duplicate-COF data fault is gone. It is -not. Resolution now picks correctly, but seven accounts still hold two Current Outfit folders each and will keep -logging the A7 WARN until the dedupe is run. diff --git a/Docs/feature/ais-v3/A6-LIVE-FAILURE.md b/Docs/feature/ais-v3/A6-LIVE-FAILURE.md deleted file mode 100644 index e37abd641f9..00000000000 --- a/Docs/feature/ais-v3/A6-LIVE-FAILURE.md +++ /dev/null @@ -1,117 +0,0 @@ -# A6 — the first live run failed, and why - -**Date:** 2026-09-04. **Region:** Ebony. **Viewer:** Firestorm 7.2.5. **Avatar:** Truly Bazar. -**Symptom:** every inventory folder empty and stayed empty; "worn folder could not be found so clothing could not -be downloaded"; avatar a cloud. The region log shows the startup line once and then **no AIS lines at all**. - -## The short version - -**The caps were advertised correctly. Every request to them then 404'd inside the HTTP server, before the handler -was ever entered.** AIS is the first capability in this tree whose URLs carry sub-paths, and it was registered in -the dictionary that only matches a path *exactly*. - -## Why "no AIS lines" proved nothing - -The request path had **no logging whatsoever** — not at registration, not at handler entry, not on error. So -"zero AIS lines" was equally consistent with "no request arrived" and with "every request arrived and was -rejected before reaching us". It could not distinguish them, which is why item (d) of the brief exists and why -the fix adds both log points. - -The in-world symptom does distinguish them, and it points the other way from the brief's leading hypothesis. **If -the caps had been missing from the seed response, nothing would have broken**: `AISAPI::isAvailable()` would have -returned false and Firestorm would have used the legacy `FetchInventoryDescendents2` path exactly as it did the -day before. Inventory came up empty *because* the viewer got the caps, switched everything to AIS, and then got -nothing back. - -## (a) Where AIS registers, and the side-by-side that matters - -Registration itself is correct and identical to a cap that works. - -| | `FetchInventory2Module` (works) | `AISv3Module` (failed) | -|---|---|---| -| Subscribes | `s.EventManager.OnRegisterCaps += RegisterCaps` in `RegionLoaded` (`FetchInventory2Module.cs:112`) | same, in `RegionLoaded` (`AISv3Module.cs`) | -| Registers | `caps.RegisterSimpleHandler("FetchInventory2", new SimpleOSDMapHandler("POST", "/" + UUID.Random(), …))` (`:141-149`) | `caps.RegisterSimpleHandler(CapName, new AisHandler("/" + UUID.Random(), …))` | -| Reaches the seed? | yes | **yes — this was never the problem** | -| **URL shape the viewer uses** | the cap URL **exactly**: `POST ` | the cap URL **plus a sub-path**: `GET /category/{id}/children`, `/item/{id}`, `/orphans` | - -That last row is the whole bug. - -Both go `Caps.RegisterSimpleHandler` (`Source/OpenSim.Capabilities/Caps.cs:196-200`) → -`CapsHandlers.AddSimpleHandler` (`CapsHandlers.cs:94-100`) → `m_httpListener.AddSimpleStreamHandler(handler)`. -That call takes a second parameter which neither `Caps` nor `CapsHandlers` exposes: - -```csharp -public void AddSimpleStreamHandler(ISimpleStreamHandler handler, bool varPath = false) // BaseHttpServer.cs:358-364 -{ - if (varPath) - m_simpleStreamVarPath.TryAdd(handler.Path, handler); - else - m_simpleStreamHandlers.TryAdd(handler.Path, handler); -} -``` - -It defaults to **false**, so AIS landed in `m_simpleStreamHandlers`. And that dictionary is matched **exactly**: - -```csharp -private bool TryGetSimpleStreamHandler(string uripath, out ISimpleStreamHandler handler) // BaseHttpServer.cs:1109-1123 -{ - if (m_simpleStreamHandlers.TryGetValue(uripath, out handler)) // exact match only - return true; - - // look only for keyword before second slash ( /keyword/someparameter/... ) - handler = null; - if (uripath.Length < 3) return false; - int indx = uripath.IndexOf('/', 2); - if (indx < 0 || indx == uripath.Length - 1) return false; - return m_simpleStreamVarPath.TryGetValue(uripath[..indx], out handler); // sub-paths live HERE -} -``` - -The dispatcher matches on `Util.TrimEndSlash(request.UriPath)` (`BaseHttpServer.cs:702-704`), i.e. the path -without the query string. For a cap registered at `/`: - -- `POST /` — FetchInventory2's shape — hits the exact-match branch. Works. -- `GET //category//children` — AIS's shape — misses the exact match, falls to the var-path branch, - where `uripath.IndexOf('/', 2)` lands on the slash after the 36-character UUID and looks up `/` in - `m_simpleStreamVarPath` — **which is empty for us**. Returns false. The server answers 404 and the handler is - never entered. - -So the var-path branch is exactly the mechanism AIS needs, and the key it would look up is exactly the path AIS -registered. Only the dictionary was wrong. - -**Precedent:** every handler in this tree that serves sub-paths already passes `varPath: true` — -`GatekeeperServerConnector.cs:70`, `UserAgentServerConnector.cs:107`, `NeighbourServiceInConnector.cs:61`, -`SimulationServiceInConnector.cs:50-51`, `XBakesHandler.cs:62`. None of them is a **cap**, which is why -`Caps.RegisterSimpleHandler` never needed the parameter until now. AIS is the first sub-path cap in the tree. - -## (b) Timing — not the cause - -`RegionLoaded` ran: the startup line printed once, and it is emitted *after* `OnRegisterCaps += Handler`. The -subscription is taken on the same `Scene` object the log line names (`scene.EventManager`, with the scene captured -in the closure that `RemoveRegion` later unsubscribes). Truly logged in 18 minutes later, so there is no race. -`TriggerOnRegisterCaps` (`EventManager.cs:2119-2138`) invokes each delegate in a try/catch and logs -`[EVENT MANAGER]: Delegate for TriggerOnRegisterCaps failed` on a throw — no such line appeared either. - -## (c) Cap names — not the cause - -`AISv3Module.CapName = "InventoryAPIv3"` and `LibraryCapName = "LibraryAPIv3"`, matching `llaisapi.cpp:48-49` -character for character, and pinned by a test. `SeedCapRequest` adds every requested name to `validCaps` with no -whitelist (`BunchOfCaps.cs:340-376`), and `GetCapsDetailsLLSDxml` emits a URL for any name present in either -handler dictionary (`CapsHandlers.cs`), so a registered name is advertised. - -## (d) Why nothing could be seen - -Two log points were missing and are added by the fix: - -- **at registration**, DEBUG, naming the agent and the URL produced — so a live run shows registration per agent - rather than only the once-per-region startup line; -- **at handler entry**, DEBUG, naming the verb, path and resolved operation — so a request that arrives and fails - is distinguishable from one that never arrives. - -Had either existed, this would have been a one-minute diagnosis instead of a code read. - -## What this says about the test suite - -114 tests passed while this was completely broken. They drive `AisHandler.Handle(request, response)` directly, so -they exercise routing, envelopes and every operation — and never touch how the handler is bound to a URL. The bug -lived entirely in the two lines between `RegisterCaps` and the HTTP server, which no test observed. diff --git a/Docs/feature/ais-v3/A7-DUPLICATE-COF.md b/Docs/feature/ais-v3/A7-DUPLICATE-COF.md deleted file mode 100644 index 3bf12327db3..00000000000 --- a/Docs/feature/ais-v3/A7-DUPLICATE-COF.md +++ /dev/null @@ -1,159 +0,0 @@ -# A7 — two Current Outfit folders, and which one wins - -> **CORRECTED 2026-09-04 by A9. The central claim of this document was wrong.** -> -> There are **no duplicate Current Outfit folders on this grid**. The version-1 folders are the Current Outfit -> folders of the **HG suitcase skeleton** — parented to `My Suitcase` (type 100) and created by -> `HGSuitcaseInventoryService.CreateSystemFolders` (`:172-186`). Sixteen system folder types show the same -> pattern across the same seven accounts, which are exactly the seven accounts that have a suitcase. Counting -> agents with more than one type-46 folder *outside* any suitcase returns **zero**. -> -> Evidence and the full before-state table: `A9-SUITCASE-NOT-DUPLICATE.md`. -> -> **Also corrected:** the failure this document was written to explain was **not** a resolution failure. The -> region log shows every slam went to `71c3c184…`, the correct root COF. Step 10's real cause is re-diagnosed in -> `A10-STEP10-REDIAGNOSIS.md`. -> -> **What still stands:** the resolution in `AisInventory.GetSystemFolder` (`6cd13a3645`) is sound and worth -> keeping — `folders[0]` over an unordered query genuinely is non-deterministic, and a deterministic rule with a -> WARN is better than a coin flip. It simply was not fixing the bug we thought it was. See **A-Q16** for a latent -> risk the correct picture exposes. -> -> The original text follows, kept because the reasoning error in it is worth reading. **Sections (a)–(c) below -> are superseded.** - ---- - -**Date:** 2026-09-04. **Region:** Ebony, `AIS_Enabled = true`. **Avatar:** Truly Bazar -(`a7d2ff2e-dc32-44d8-aa61-3d22070a4964`). **Checklist step:** 10, take off a garment. - -**Symptom:** the skirt came off and was back after a relog. The viewer slammed -`71c3c184-410b-4dae-b20a-855741cf1faf` twice (12:36, 12:37); at login 12:38 it fetched `/category/current/links` -and immediately rebuilt links in `71c3c184…`. The avatar has two type-46 folders: - -| folder | name | type | version | ~~claim~~ **actual (A9)** | -|---|---|---|---|---| -| `71c3c184-410b-4dae-b20a-855741cf1faf` | Current Outfit | 46 | 457 (now 500) | the real COF, under `My Inventory` | -| `52c327c4-cb7d-4365-a7f0-62a6f7545265` | Current Outfit | 46 | 1 | ~~the one we returned~~ **the suitcase's COF, under `My Suitcase`** | - -## ~~The short version~~ — SUPERSEDED - -> ~~Our `"current"` alias resolves to an arbitrary one of the agent's type-46 folders, and it picked the wrong -> one. The take-off did happen — it was written to a folder no viewer reads.~~ -> -> **It did not pick the wrong one.** The log shows the slam went to `71c3c184…` every time. - -## (a) What the resolution actually promises: nothing — STILL TRUE, but not the cause - -This section's *description of the code* is accurate and unchanged. What was wrong was the inference drawn from it. - -`AisInventory.GetCurrentOutfit` (`AisInventory.cs:106-107`) → `IAisInventoryBackend.GetFolderForType` -(`IAisInventoryBackend.cs:20`) → `InventoryServiceBackend` (`AISv3Module.cs:172`) → `IInventoryService`. On Legion -Grid inventory is remote (`RemoteXInventoryServiceConnector.cs:175-178` → `XInventoryServicesConnector.cs:186-195`, -`METHOD=GETFOLDERFORTYPE`) → Robust `XInventoryInConnector.cs:260` → `XInventoryService.GetFolderForType`. - -Robust runs the plain service for the region-facing port (`Robust.ini:107-108`), so this is the code that answers: - -```csharp -private InventoryFolderBase GetSystemFolderForType(InventoryFolderBase rootFolder, FolderType type) // XInventoryService.cs:272-294 -{ - if (type == FolderType.Root) - return rootFolder; - - XInventoryFolder[] folders = m_Database.GetFolders( - ["agentID", "parentFolderID", "type"], - [rootFolder.Owner.ToString(), rootFolder.ID.ToString(), ((int)type).ToString()]); - - if (folders.Length == 0) - return null; - - return ConvertToOpenSim(folders[0]); // first row wins. No ordering, no tie-break, no warning. -} -``` - -`folders[0]`, and the query behind it has **no `ORDER BY` and no `LIMIT`**: `MySQLXInventoryData.GetFolders` -(`:56-59`) → `MySqlFolderHandler` (no `Get` override, `:250-256`) → `MySQLGenericTableHandler.Get(string[], -string[])` (`:154-157`), which delegates with `options = String.Empty` (`:159-185`). The schema permits duplicates: -`inventoryfolders` (`InventoryStore.migrations:30-40`) has `PRIMARY KEY (folderID)` and non-unique keys only. - -**All of that remains true.** It is a real latent fragility and the reason `6cd13a3645` is worth keeping. It was -simply not what broke step 10. - -### The reasoning error, stated plainly - -> **A consequence worth stating:** this query filters on `parentFolderID = rootFolder.ID`. Two rows can only both -> match if **both COFs are direct children of the same root folder**. `52c327c4…` therefore certainly is. - -The first sentence is correct. **The last sentence does not follow, and it is where this went wrong.** - -The filter means `GetSystemFolderForType` can only ever return a folder parented to the root. I combined that with -an unverified premise — "AIS returned `52c327c4…`" — and concluded `52c327c4…` must be under root. The premise was -never checked; it came from the brief's own reading of the symptom and I adopted it as fact. - -Run the other way, the same filter refutes the premise: `52c327c4…` is under `My Suitcase`, so -`GetSystemFolderForType` **could not have returned it**, so AIS was never resolving to it. One read-only query -would have caught this before a line of code was written. The lesson is not "check parentage" but: **when a -diagnosis rests on an assumed observation, verify the observation first, especially when it is the one fact that -makes the rest of the argument work.** - -## (b) Every site that creates a type-46 folder — table still correct, conclusion inverted - -| # | Site | When it fires | Guarded? | Parents to | -|---|---|---|---|---| -| 1 | `XInventoryService.CreateUserInventory` `:132-133` | account creation `UserAccountService.cs:817`; `RemoteAdminPlugin.cs:2630`; every Direct Delivery `DirectDeliveryPostHandler.cs:134, :217`; `XInventoryInConnector.cs:205` | yes — `GetSystemFolders` `:194-208` scans all root children | the real root | -| 2 | `HGSuitcaseInventoryService.CreateSystemFolders` `:185-186` | once per user, from `GetRootFolder` `:136-170` when they have no suitcase yet | yes, same shape | **the suitcase** | -| 3 | `HGInventoryService` `:103-115` | HG visitor with no suitcase | — | creates only a suitcase | -| 4 | IAR load | — | — | creates no type-46 | - -~~Sites 2, 3 and 4 are excluded by the parentage consequence… That leaves site 1 as the only in-tree path that can -put a second type-46 under the root.~~ - -**Inverted by A9. Site 2 is the answer, and it is not a bug.** Site 2 created the version-1 folders, on purpose, -as part of the suitcase skeleton. Site 1 created nothing extra — the `EnsureSystemFolder` narrowing committed in -`68bfa60735` is defensible hardening of a real read-then-write race, but **no observed duplicate is attributable to -it**, and it should not be described as fixing one. - -## (c) Does this predate AIS? — the question was moot - -The section argued that every `GetFolderForType` caller inherits the same coin flip and that AIS merely made a -latent data fault visible. The first half stands as a statement about the code. The second half does not: **there -was no data fault.** - -## The rule chosen, and why — STILL THE RULE, with a caveat - -Highest `Version`, ties on lowest folder id. The justification from the tree is unchanged and still holds: - -- version is bumped on every child add/remove (`MySqlFolderHandler.Store :283-291`, `MoveFolder :258-281` → - `IncrementFolderVersion`, `MySQLXInventoryData.cs:303-317`) and never decreases (`UpdateFolder :423-427`, - `:439-440`); -- creation order is not available — `XInventoryFolder` has six fields (`IXInventoryData.cs:32-45`) and the table - has no timestamp; -- descendant count is unusable — a legitimately emptied COF has none. - -**Caveat added by A9, tracked as A-Q16.** Resolution scans the skeleton, which for a local user includes the -suitcase subtree. Suitcase COFs sit at version 1 today, so the root COF always wins — but nothing enforces that. -If a suitcase COF ever overtook the root COF, a local user's outfit would resolve into their suitcase. - -## ~~What would confirm the creation site~~ — RUN, AND IT REFUTED THIS DOCUMENT - -The query this section proposed was run in A9. Its own stated failure branch was the one that came true: - -> If the second row's parent is the suitcase, the parentage argument in (a) is wrong and the diagnosis must be -> reopened. - -It is, it was, and it has been. - -## ~~What the dedupe should do~~ — DO NOT RUN A DEDUPE - -There is nothing to deduplicate. Running the proposed dedupe would have deleted a live part of the HG suitcase -skeleton for seven accounts, and it would **not** have grown back: `CreateSystemFolders` is only called when the -suitcase itself is missing (`GetRootFolder :152-165`). - -## Ledger items - -- **A-R8** — corrected. A unique index on `(agentID, type)` **must not be added**: it would reject the legitimate - suitcase skeleton and break suitcase creation grid-wide. See the ledger for the candidate shape, which is not - settled. -- **A-Q13** — unchanged. `InventoryFolderBase.Version` is `ushort` (`InventoryFolderBase.cs:67`) against an - `int(11)` column. -- **A-Q16** — new, the suitcase-overtakes-root risk described above. diff --git a/Docs/feature/ais-v3/A9-SUITCASE-NOT-DUPLICATE.md b/Docs/feature/ais-v3/A9-SUITCASE-NOT-DUPLICATE.md deleted file mode 100644 index 8a249ebf5f1..00000000000 --- a/Docs/feature/ais-v3/A9-SUITCASE-NOT-DUPLICATE.md +++ /dev/null @@ -1,108 +0,0 @@ -# A9 — the "duplicate Current Outfit folders" are the HG suitcase skeleton - -**Date:** 2026-09-04. **Purpose of the session:** deduplicate the seven accounts believed to hold two Current -Outfit folders each, per the proposal in `A7-DUPLICATE-COF.md`. - -**Outcome: stopped at the plan step with the database untouched. There is nothing to deduplicate.** The session -issued only `SELECT`s and one `mysqldump`. No `UPDATE`, `DELETE` or `INSERT` was executed. - -**Backup taken before any inspection:** `D:\legiongrid\_backup\legiongrid-predupe-20260904-1332.sql`, -2,685,971,589 bytes, no BOM, terminating in `-- Dump completed on 2026-09-04 18:34:21`. - -> **Backup hygiene, worth carrying forward.** The first attempt piped `mysqldump` through PowerShell's -> `Set-Content -Encoding utf8`. It produced a 4.08 GB file with a UTF-8 BOM, and it was **corrupt**: PowerShell -> decodes and re-encodes the byte stream, replacing every byte that is not valid text with U+FFFD, which mangles -> binary column data. The 1.4 GB of inflation over the correct 2.50 GB was that damage. It was deleted and the -> dump re-taken with byte-exact shell redirection. **Any `.sql` in `_backup\` produced through a PowerShell text -> pipeline should be assumed unrestorable.** - ---- - -## Before-state survey - -Every agent with more than one type-46 folder, with each folder's id, version, parent and child counts. Keeper -(by the A7 rule: highest version) in **bold**. - -| agentID | folderID | version | parentFolderID | subFolders | items | -|---|---|---|---|---|---| -| 0f62cf39-71b8-49e1-94ea-ebdf54be01e2 | **2b74a4cd-e384-4778-86c4-80f057b713d1** | **115** | 0f62cf39-71b8-49e1-94ea-ebdf54be01e2 | 0 | 12 | -| 0f62cf39-71b8-49e1-94ea-ebdf54be01e2 | 2eb36833-915b-4076-9226-671ec914bf96 | 1 | 8ee583b3-9259-40e4-989a-431492d85768 | 0 | 0 | -| 47dd39a8-1261-45d2-9fc9-986def3a97b6 | **d2d5a12b-b2a0-412b-8c40-9a8e222dda12** | **9** | 97a4d413-5be4-4551-b977-470bd1f45e1b | 0 | 6 | -| 47dd39a8-1261-45d2-9fc9-986def3a97b6 | 08f89f73-f572-4382-bf09-1196f810ef7a | 1 | b36b7761-4da1-47d9-b138-1e15e20cb936 | 0 | 0 | -| 4dc144cb-4335-4d5f-ac2d-b2c87d0f67e9 | **509aa3ff-15be-4cc6-be8b-5c9aa8398a42** | **36** | 4dc144cb-4335-4d5f-ac2d-b2c87d0f67e9 | 0 | 15 | -| 4dc144cb-4335-4d5f-ac2d-b2c87d0f67e9 | 88028d53-4a08-473c-ac52-fb301727edb8 | 1 | 36b2d277-a69e-47ec-b835-140e20f42e09 | 0 | 0 | -| 4fbdfd2a-e0c6-4003-b2f8-8714fcc7b968 | **4161565b-b08b-490e-80d6-a5a61227bc0f** | **716** | dd748992-4298-4dc6-88b3-d5d72e00226c | 0 | 11 | -| 4fbdfd2a-e0c6-4003-b2f8-8714fcc7b968 | 2acd261c-af0b-4393-a183-5f2cfec6271d | 1 | aa02d15b-78d1-4c2e-9630-226db8a0f36e | 0 | 0 | -| 5266d93e-d723-4317-a653-227bd676dddd | **7853c313-6d58-4816-a1dc-66f7e9fb6d1b** | **9** | 88dcd9ee-7f7b-45b8-a323-8c055054a00a | 0 | 6 | -| 5266d93e-d723-4317-a653-227bd676dddd | 856550ad-84d5-4316-9898-67b020ab347a | 1 | 4f89c68d-8c50-4ddc-8ee3-0bf193dca988 | 0 | 0 | -| a7d2ff2e-dc32-44d8-aa61-3d22070a4964 | **71c3c184-410b-4dae-b20a-855741cf1faf** | **500** | bb7d5f74-a4cf-47cf-9f1d-96f60c1cd954 | 0 | 14 | -| a7d2ff2e-dc32-44d8-aa61-3d22070a4964 | 52c327c4-cb7d-4365-a7f0-62a6f7545265 | 1 | ec7a4f10-2307-4c23-857e-af0550216ea1 | 0 | 0 | -| c0b98d62-9705-4ca5-8f8a-902ad6ee9083 | **fb4e5690-4305-4863-b236-cb594aed5655** | **41** | c0b98d62-9705-4ca5-8f8a-902ad6ee9083 | 0 | 10 | -| c0b98d62-9705-4ca5-8f8a-902ad6ee9083 | 52f5dcdb-7ea7-4bbf-a015-78499f7ba46f | 1 | 40a7c741-4dc5-46fc-8df2-b9811d4ff7d5 | 0 | 0 | - -Seven accounts, as expected, and every version-1 folder is empty. **But the two folders in each pair have -different parents** — which `A7-DUPLICATE-COF.md` had asserted was impossible. That is what stopped the run. - -## Resolving the parents - -| agent | keeper's parent | loser's parent | -|---|---|---| -| all seven | `My Inventory`, **type 8**, `parentFolderID = 00000000-…` | `My Suitcase`, **type 100** | - -For Truly Bazar specifically: - -| folderID | version | parent | parentName | parentType | -|---|---|---|---|---| -| 52c327c4-cb7d-4365-a7f0-62a6f7545265 | 1 | ec7a4f10-2307-4c23-857e-af0550216ea1 | **My Suitcase** | **100** | -| 71c3c184-410b-4dae-b20a-855741cf1faf | 500 | bb7d5f74-a4cf-47cf-9f1d-96f60c1cd954 | **My Inventory** | **8** | - -The version-1 folders are the Current Outfit folders of the **HG suitcase skeleton**, created deliberately by -`HGSuitcaseInventoryService.CreateSystemFolders` (`:172-186`) when the suitcase is made (`GetRootFolder :152-165`). - -## The three confirmations - -**1. The affected accounts are exactly the accounts with a suitcase.** - -| query | result | -|---|---| -| accounts with a type-100 folder | **7** | -| overlap between "accounts with >1 type-46" and "accounts with a suitcase" | **7 of 7** | - -**2. It is not Current-Outfit-specific — sixteen system types show the same pattern, across the same seven -accounts.** That is the whole suitcase skeleton as `CreateSystemFolders` builds it. - -| type | accounts affected | | type | accounts affected | -|---|---|---|---|---| -| 1 | 7 | | 15 | 7 | -| 2 | **14** | | 16 | 7 | -| 3 | 7 | | 20 | 7 | -| 5 | 7 | | 21 | 7 | -| 6 | 7 | | 23 | 7 | -| 7 | 7 | | **46** | **7** | -| 10 | 7 | | 56 | 7 | -| 13 | 7 | | | | -| 14 | 7 | | | | - -Type 2 (Calling Cards) shows 14 because the skeleton nests `Friends` and `All` beneath it — three per tree, as -`CreateUserInventory :122-127` and `CreateSystemFolders` both do. - -**3. Genuine duplicates: zero.** Counting agents with more than one type-46 folder whose parent is *not* a -suitcase returns **0**. - -## What executing the dedupe would have done - -Deleted a live part of the HG suitcase skeleton for seven accounts, permanently: `CreateSystemFolders` is called -only when the *suitcase itself* is missing (`GetRootFolder :152-165`), so a deleted suitcase COF does not grow -back. Applied consistently the same rule would have taken the other fifteen types too — and the plan's own -"report any other duplicated type" step ran *after* the deletions, so it would have reported the damage rather -than prevented it. - -## Consequences for the record - -- `A7-DUPLICATE-COF.md` is corrected in place, with the superseded reasoning kept visible. -- **A-R8** is corrected: no unique index on `(agentID, type)`. It would reject the suitcase skeleton and break - suitcase creation grid-wide. -- **A-Q16** is opened: for a local user the A7 rule scans every type-46 folder including the suitcase's. -- The resolution fix `6cd13a3645` stands on its own merits — `folders[0]` over an unordered query really is - non-deterministic — but it did not fix step 10, because COF resolution was never wrong. Step 10 is - re-diagnosed in `A10-STEP10-REDIAGNOSIS.md`. diff --git a/Docs/feature/ais-v3/AIS-V3-SPEC.md b/Docs/feature/ais-v3/AIS-V3-SPEC.md index d0048d7f33b..4323db5ae67 100644 --- a/Docs/feature/ais-v3/AIS-V3-SPEC.md +++ b/Docs/feature/ais-v3/AIS-V3-SPEC.md @@ -1,6 +1,6 @@ # AIS v3 — the surface the LL viewer drives -**Authority (Ledger P-1):** the LL viewer source at `F:\viewer-develop` (viewer 26.1.1). Every row below cites +**Authority:** the Linden Lab viewer source, version 26.1.1. Every row below cites the file and line it was read from. Files read, read-only: `indra/newview/llaisapi.h` (167 lines), `indra/newview/llaisapi.cpp` (1798 lines), the AIS call site in `indra/newview/llinventorymodel.cpp` (`:1025-1058`), and the AIS call sites `remove_inventory_item`, `remove_inventory_category`, @@ -477,8 +477,9 @@ stops refusing four classes of folder the viewer never considered protected. The RemoveCategory → the removed object's parent; PurgeDescendents → the purged folder; UpdateItem / UpdateCategory → the parent when the update moves the object, otherwise the object's own folder is listed with delta 0 (`:1245`, `:1298`, `:1427`); CopyLibraryCategory → the destination. Those are derived from the viewer's - accounting, not from an explicit table; the server rule in OpenSim is the data-layer increment recorded in - `S0a-VERIFICATION.md` V6. + accounting, not from an explicit table; the server rule in OpenSim is the data layer's own increment, which + fires on item store, delete and move and on folder store, and which the cap's category create also applies + to the parent. The handler must therefore re-read the folder after a write rather than compute the version. ## 1f. HTTP status handling (`InvokeAISCommandCoro`, `:851-1011`) diff --git a/Docs/feature/repo-audit/T1-TEST-FIXTURES.md b/Docs/feature/repo-audit/T1-TEST-FIXTURES.md index 56f0265eb74..3af7aff3f23 100644 --- a/Docs/feature/repo-audit/T1-TEST-FIXTURES.md +++ b/Docs/feature/repo-audit/T1-TEST-FIXTURES.md @@ -1,6 +1,6 @@ # T1 — test-infrastructure defects at the deployed tree -Tree: `D:\tranq-fix`, branch `fix/test-fixtures`, HEAD `db7c746248` (deployed). Runs are serial +Measured on branch `fix/test-fixtures`, HEAD `db7c746248`. Runs are serial (`-- xUnit.ParallelizeTestCollections=false`) so order-flaky tests do not blur the count. ## Part 1 — `OpenSim.Region.CoreModules.Tests`: 35 failures at clean HEAD diff --git a/Docs/feature/ssb-appearance/ADR-SET-ssb-appearance.md b/Docs/feature/ssb-appearance/ADR-SET-ssb-appearance.md index 230e44a5cb9..bad7fd27445 100644 --- a/Docs/feature/ssb-appearance/ADR-SET-ssb-appearance.md +++ b/Docs/feature/ssb-appearance/ADR-SET-ssb-appearance.md @@ -84,8 +84,8 @@ because `AvatarService.SetAvatar` deletes every row for the principal first — **Status:** Accepted, revised 2026-09-03 by measurement (supersedes the 512 default carried from D-7) **Decision:** Sim default **1024** px per channel; `[Appearance] BakeSize` accepts 512, 1024 or 2048. Hash includes size so a config change invalidates stored bakes on next login rather than serving mixed sizes. -**Why 1024 and not the original 512.** S1b ran both reference sets at all three sizes against the LL -compositor references (`S1b-FIDELITY.md` §6). 1024 is the knee: every channel improves from 512 to 1024, and on +**Why 1024 and not the original 512.** Both reference sets were run at all three sizes against the LL +compositor references; the bake-size sweep in the golden harness reproduces the measurement. 1024 is the knee: every channel improves from 512 to 1024, and on the richer of the two outfits the improvement is large — Aleric's lower channel goes from mean abs RGB **2.29** at 512 to **0.89** at 1024, the single worst number in the matrix and the only one that would have failed a tighter threshold. Going on to 2048 buys nothing: five of the nine channel rows get *worse*, none improves diff --git a/Docs/feature/ssb-appearance/BUILD-PLAN-ssb-appearance.md b/Docs/feature/ssb-appearance/BUILD-PLAN-ssb-appearance.md deleted file mode 100644 index 5239eb934b4..00000000000 --- a/Docs/feature/ssb-appearance/BUILD-PLAN-ssb-appearance.md +++ /dev/null @@ -1,104 +0,0 @@ -# Build Plan — Server-Side Baking (L-2) - -**Status:** IN PROGRESS — S0a–S1 done and deployed; S2 next. Still gated on Ledger D-1/D-3 + ADR-003 for the later sessions. **Date:** 2026-09-03 (S1-close) -**Estimating convention:** wall-clock Claude Code minutes per session, anchored to measured web-viewer sessions (S4 21 min, S10 20 min, S6 28 min, S11 39 min, S9 55 min). A session past ~2× its estimate is stuck: stop it, report, re-scope. One feature per session. -**Branch:** `feature/ssb-appearance` off `develop` HEAD (`cb141dd61d` + maptile fix). Commit locally at each session's DoD; push is John's call. -**Repos:** T = `/d/tranquillity-develop` (worktree for this branch — see §0), W = `D:\web-viewer`. - -## 0. Setup (John, ~3 min, no CC) - -```bash -cd /d/tranquillity-develop -git fetch --all -git worktree add /d/tranq-ssb -b feature/ssb-appearance develop -cd /d/tranq-ssb -mkdir -p Docs/feature/ssb-appearance -cp /d/_TO_REVIEW/ssb-appearance/*.md Docs/feature/ssb-appearance/ -# then copy the five docs from this delivery into the same folder -git add Docs/feature/ssb-appearance -git commit -m "docs(ssb): recon + addendum, design brief, ADR set, build plan, ledger" -``` - -**Needs your attention:** the `cp` from `_TO_REVIEW` assumes the CC recon's filenames are `.md` at the top level of that folder — check `ls /d/_TO_REVIEW/ssb-appearance/` first. - -## 1. Session table - -| S | Repo | Feature | DoD (harness green, not "looks right") | Est. min | Verify loop (yours) | -|---|---|---|---|---|---| -| **S0a** ✅ | T | **Verification grep pass + library project skeleton.** Resolve every `[UNVERIFIED]` in the RECON addendum §2/§6 with file:line at HEAD; create `Source/OpenSimNGC.Appearance.Baking` (net10.0, SkiaSharp, tree's J2K encoder, embedded `avatar_lad.xml` per ADR-007) with an empty public API + test project. | Report table with file:line for all 6 items; solution builds; `dotnet test` runs 0 tests green. **Done:** `29105ccc44`, `7dbc092d2e`. | 25 → **?** | none | -| **S0b** ✅ | W→T | **Extract compositor.** Move `gateway/src/Gateway/Baking/` into the library; port its existing unit tests; golden-fixture harness: given a wearables+params fixture and Firestorm's bake assets for Truly's stock-Library outfit, pixel-diff per channel with a threshold (report SSIM/abs-diff per channel). Gateway switches to `ProjectReference` (NuGet later, ADR-003) and its `Baking/` dir is deleted. | Library tests green; gateway builds and its 9 S11 tests + S12 tests still green against the library; diff numbers printed for 6 legacy channels. **Done:** `303d2b39c1`, `8a245aa286`, `cbf3284e06`, `e3b969c9ab` (T); `be67e2d` (W). | 40 → **34** | **You:** produce the golden fixtures — log Truly in via Firestorm on the stock outfit, note the 6 bake UUIDs from Appearance debug (or from `AvatarAppearance` via the harness), pull the assets. This is the step that can't be automated and blocks S0b's last third. | -| **S1** ✅ | T | **Bake orchestrator + console trigger.** `AppearanceBakeModule` (Addons or OptionalModules per S0a finding): COF → wearables → textures → 11-channel composite via library → store assets → update ScenePresence TE → `SendAppearance` to all. Trigger: console `appearance bake `. No cap, no flag, no persistence keys yet. | Console command on Ebony bakes Truly; Firestorm observer (you) sees the sim's bake replace hers; harness diff of the stored assets vs goldens ≤ threshold. **Done:** `bbc065bc5f`, `99118ea1ab`; deployed to the live region server 2026-09-03 19:14 and verified in-world 19:46 (first server-composited avatar on the grid). **DoD closed in S2 Part 0** (commit `test(ssb): close S1 DoD — golden diff at the shipped bake size`): the caveat was that the harness-diff clause had been met only at 512 while the bakes stored on the live sim were made at 1024. The gate now bakes at the shipped `[Appearance] BakeSize` (1024, ADR-008) — `truly-stock/manifest.json` `bakeSize` raised 512 → 1024, `aleric-max` was already 1024 — and both sets pass every threshold against the 2048/512 references. Worst numbers at 1024: mean |dRGB| 1.25 (truly upper) against 4.0, pctRGB>8 0.75% against 5%, mean |dA| 1.00 against 2.0, mean |dM| 1.00 against 4.0, pctM>8 1.59% (aleric upper) against 5%. Every RGB mean is *better* at 1024 than it was at 512. | 35 → **15** | Firestorm side-by-side, 1 loop. | -| **S2** (part) | T | **Persistence + supersede + reaper** (ADR-004). Avatar-service keys, per-channel input hash, skip-compute on match, synchronous supersede-delete, Robust reaper with `BakeTTLDays`, off by default. | Unit tests: hash stability, supersede deletes old UUID, reaper deletes only past-TTL-and-not-logged-in; console bake twice → second run logs "reused 11/11". **Done for the index, the skip and supersede:** keys read/written through the existing `GetAvatar`/`SetItems`/`RemoveItems` (no service change, no schema change); reuse is per channel and decided before any texture is fetched; a stored hash whose asset has vanished is not trusted; `BakeSize` invalidates; supersede deletes only after the new asset is stored and never something a face points at. The one-line summary now ends `reused N/M`. **Still open in S2:** the Robust TTL reaper with `BakeTTLDays`. New finding: Ledger Q-14 (any appearance save wipes the index). Cost instrumentation and Q-10's answer landed here too. | 30 | none | -| **S3** ✅ | T | **Wire: flag, bit 0, `AppearanceData`, cap.** `[Appearance] ServerSideBaking` per region; `RegionProtocols |= 1` when set; `SendAppearance` emits `AppearanceData{1, CofVersion}` for sim-baked avatars only; `UpdateAvatarAppearance` cap with the §4.3 handshake + anti-livelock; login-time bake trigger on `MakeRootAgent`. Flag stays **false** in every shipped ini. | Unit tests for the handshake (equal / less / greater / livelock cap); with the flag on for a **test region only**, LL viewer logs in and is textured to itself; Firestorm on the same region POSTs and is textured. **Done:** `1e78b9a706` (Part 0, Q-14), `7554bf9b51` (Part 1); deployed to the live region 2026-09-04 at `1.1.216-alpha+7554bf9b51`, flag left false. Handshake tests cover all four branches plus a throwing re-read, window expiry, per-agent counters and the clear on success; the flag resolves per region in five configurations; the ADR-001 gate asserts a flag-off region's `AvatarAppearance` body is byte-identical to the pre-S3 form at three sizes. `cof_version` proven identical to AIS's folder version. **The DoD's two live clauses are not met and cannot be met by this session** — they need the flag on and a viewer in-world, which is John's loop. | 40 → **?** | **You:** stock LL viewer + Firestorm on the test region, 1 loop each. First moment the LL viewer isn't a cloud. **Also settles Q-6**, which gates S5's shape. | -| **S4** ✅ | T | **Appearance service on Robust** (ADR-002). `agent_appearance_service` in the login response; `GET texture///` resolving via avatar-service keys and streaming the asset; standalone registration. | curl the URL for Truly's `head` returns the J2K bytes; LL viewer sees **other** avatars textured on the test region. **Done:** `59b12538b9` (handler), `51194ac754` (login response); deployed to both roots 2026-09-05 at `1.1.219-alpha+51194ac754`, with the connector and the URL left unconfigured. Channel token established from `llvoavatar.cpp:5912` (a name — `head`, `upper`, `lower`, `eyes`, `skirt`, `hair`, `leftarm`, `leftleg`, `aux1..3`), not assumed. 404 on every miss including a UUID that disagrees with the index. **The DoD's two clauses are John's loop and cannot be met here** — both need the service configured and a viewer in-world. | 30 → **?** | LL viewer observing Firestorm-and-sim-baked Legion, 1 loop. **Prerequisite for any flag flip**: S3's flip without S4 produced a correct cloud. | -| **S5** ✅ | T | **Change triggers + BoM aux channels.** Rebake on `AvatarNowWearing` (Firestorm on a bit-0 region) and on cap POST with a newer COF version; the 5 BoM aux channels produced when universal wearables are present. | Firestorm on the test region changes a shirt → new bake within one POST; fixture with a universal wearable yields 11 stored channels. **Done:** `d7ac58d187` (trigger), `9417a09402` (aux channels). Trigger is the save-completion event, not the arrival of the change (Q-16); both signal routes converge on it after the cap was changed to queue a save rather than bake on arrival; 2 s debounce sized against the 5 s save delay (the signal spread is unmeasured — Q-6). Aux channels exercised end to end with a synthetic Universal — composited, stored, faces 40-44, served by the Robust route. **The DoD's live clause is John's loop**; the aux clause is met by fixture, not by real content, and the fidelity gap is recorded rather than closed. | 30 → **?** | Firestorm outfit change, 1 loop. Watch for `reason=CofChanged` and one bake per change. | -| S6 | W | **Gateway SSB-aware mode** (ADR-009). Detect bit 0 per region; `server` appearance mode; accept `AppearanceData` for self; no bake path reachable on bit-0 regions (structural, like the S11 invariant). | Unit test proving the bake step is unreachable when bit 0 is set; live: Truly logs in via the web viewer on the test region and is textured with **zero** gateway bakes logged; on Transylvania (flag off) the S12 path still runs. | 25 | Web-viewer login on both regions, 1 loop. | -| S7 | T+W | **Soak + fidelity sign-off.** Harness against all three test avatars' outfits; 30-minute soak with LL viewer + Firestorm + web viewer on the test region; region restart with flag on → no rebake (persistence); flag off → Firestorm reverts, LL viewer clouds, nothing deleted. | All harness diffs ≤ threshold; no `AppearanceData` regressions on the flag-off region; report lists every unsupported layer seen. | 30 | Your call on flipping Ebony/Transylvania/Elm. | - -**Total:** ~4.75 h CC across 9 sessions; 5 short verify loops of yours. Comparable to two web-viewer working days at the measured pace. - -## 1a. Unplanned slices added mid-programme - -Five sessions below were **not in the original table** — they were cut out of S0b/S1 when the work turned out to be -a separate concern, and one (T-1) was pure repo hygiene that S1 tripped over. They are listed here so the estimate -column above stays honest about what the programme actually cost. - -| S | Feature | Commit(s) | Est. → actual (min) | Why it was added | -|---|---|---|---|---| -| **S0c** ✅ | `Client_OnAvatarNowWearing` merges instead of wiping unlisted wearable slots; 4 xunit tests | `a5e88d72f1` (now `11a2456833` on the deployed branch) | 20 → **9** | Ledger Q-3 / R-4 found the wipe bug present in S0a; it is a hard gate before any production flag flip, so it could not wait for S3. | -| **S0d** ✅ | 5th J2C component is the **morph mask**, not a bump pass; compositor + encoder + decoder | `30c82d472b` | 30 → **14** | Q-8 opened by S0b's golden diff: viewer bakes carry a component the server did not produce. Parity gap, had to close before S3. | -| **S0e** ✅ | Plain vs template layer semantics (`isUserSettable`); morph gather corrected to match the colour pass; doc renamed `MORPH-MASK-PASS.md` | `916dc35d00`, `22b3695389` (T); `0a6acff` (W) | 25 → **8** | S0d shipped on a wrong premise about per-instance layers; caught reading `lltexlayer.cpp` for the packing-order citation. | -| **A0** ✅ | AIS v3 spec + `AISv3Module` skeleton (separate worktree `D:\tranq-ais`) | 2 commits on `feature/ais-v3` | 30 → **21** | Track L's other half; sequenced after SSB per D-1 but started early because it is independent. | -| **T-1** ✅ | NUnit lifecycle hooks orphaned by the xunit migration — `CoreModules.Tests` 35 → 5 failing; `LindenCaps.Tests` restored to the solution | `c1fc7fff3e`, `d43f8cb362` | 30 → **18** | S0c reported 35 pre-existing `CoreModules.Tests` failures and flagged them as needing a separate owner; S1's test work needed a trustworthy baseline. | - -## 1b. Estimate vs actual - -| S | Est. | Actual | Note | -|---|---|---|---| -| S0a | 25 | ? | Not recorded at the time. | -| S0b | 40 | 34 | The one session that ran near estimate; the golden harness carried most of it. | -| S0c | 20 | 9 | Unplanned. | -| S0d | 30 | 14 | Unplanned. | -| S0e | 25 | 8 | Unplanned. | -| A0 | 30 | 21 | Unplanned (AIS worktree). | -| T-1 | 30 | 18 | Unplanned (repo hygiene). | -| S1 | 35 | 15 | Plus deploy and in-world verify. | - -Across the seven sessions with a recorded actual, **estimates are running roughly 2:1 over actuals** (210 est. → 119 actual). -Two readings, and they are not exclusive: the estimating convention was anchored to web-viewer sessions that involved -more unknown-shape exploration than this programme has needed, and four of the seven were narrow slices carved out -of a session already scoped and understood. The ratio is **not** a reason to re-estimate S2–S7 downward: those -sessions carry the wire protocol, the cap handshake and the live flag flip, which is where the web-viewer sessions -overran too. Treat the "past ~2× its estimate means stuck" rule as unchanged. - - -## 2. Order and gates - -``` -S0a ──► S0b ──► S1 ──► S2 ──► S3 ──► S4 ──► S5 ──► S7 - ▲ │ - goldens (you) ┘ └──► S6 (any time after S3) -``` - -Gates: -- **Before S0a:** D-1, D-3, ADR-003 ruled (Ledger). -- **Before S1:** golden fixtures exist (your step in S0b). If they lag, S1 can proceed and S0b's diff numbers land in S1's DoD instead. -- **Before S3:** S0b diff ≤ threshold on the stock outfit. This is the rule that keeps a worse-than-Firestorm bake from ever reaching a bit-0 region. -- **Before flipping any production region (after S7):** the wipe-loop check from S0a is resolved on Tranquillity. - -## 3. Each session prompt carries - -Per the standing prompt structure: the S0a grep results and file:line anchors; the library's public API; what *not* to read (no LibreMetaverse Baker, no Halcyon wire code, no `appearance-utility-bin`); test avatars Truly/Aleric only, never Legion; reporting contract (done / VERIFY-resolved with file:line / decisions needed). Prompts are written here per session, in a code block, when you say go. - -## 4. Deploy notes - -**Branch state as of 2026-09-03 (S1-close).** The deployed branch `fix/maptile-legacy-renderer` is at `11a2456833` -and now carries, besides the maptile fix itself (`db7c746248`), the **S0c wearable-wipe fix** (`11a2456833` — the -rebased form of `a5e88d72f1`) and the **T-1 fixture repair** (`c1fc7fff3e`, `d43f8cb362`). Both reached the live -region server with the S1 deploy. Both feature worktrees — `D:\tranq-ssb` (`feature/ssb-appearance`) and -`D:\tranq-ais` (`feature/ais-v3`) — are rebased onto `11a2456833`, so neither carries a stale copy of the -wipe fix or of the test fixtures. Nothing is pushed. - -- Test region = one region only, flag on in its own ini section. Recommended: Transylvania (currently loads 0 objects anyway — separate issue — so nothing to disturb). -- Both servers down before deploy (your practice). Publish path is `bin\Release\net10.0\win-x64\publish\` per project (BUILDING.md is wrong on this; noted in [[repo-audit]]). -- Robust must be redeployed at S4 (appearance service) and S2 if the reaper is enabled; region-only for the rest. diff --git a/Docs/feature/ssb-appearance/BUILD-PLAN-track-L-combined.md b/Docs/feature/ssb-appearance/BUILD-PLAN-track-L-combined.md deleted file mode 100644 index da8c4fe775c..00000000000 --- a/Docs/feature/ssb-appearance/BUILD-PLAN-track-L-combined.md +++ /dev/null @@ -1,61 +0,0 @@ -# Build Plan — Track L combined: AIS v3 (L-1) + SSB (L-2) together - -**Status:** DRAFT, prompted by Mike's 2026-09-03 feedback. **Supersedes** nothing yet: `BUILD-PLAN-ssb-appearance.md` stays the SSB lane's detail; this document adds the AIS lane and the interleave. -**Estimating convention:** wall-clock Claude Code minutes per session, anchored to measured sessions (20–55 min). A session past ~2× its estimate is stuck: stop, report, re-scope. One feature per session. -**Branches:** `feature/ssb-appearance` (worktree `/d/tranq-ssb`) and `feature/ais-v3` (worktree `/d/tranq-ais`), both off `develop` HEAD. They touch disjoint files except the caps registration switch and the inventory folder-version path (see §3). Merge order: AIS first (it owns folder versions), then SSB. - -## 1. Why together - -- The LL viewer needs **both** to be a usable viewer: SSB makes the avatar visible, AIS makes it changeable. Either alone ships a half-viewer. -- They share one invariant — the COF folder `Version` — and SSB's change-trigger session (S5) can only be tested properly with the LL viewer once AIS's SlamFolder exists. -- Verify loops are the scarce resource (yours), not CC minutes. A joint soak on one test region with LL viewer + Firestorm + web viewer replaces two soaks. - -## 2. AIS lane - -Spec source: RECON-02 §3 (routes and `else`-branch table from `llaisapi.cpp` / `llviewerinventory.cpp` at `62033f2`). The prompt for A0 carries the extracted route/verb/envelope table so CC never opens the viewer tree. - -| A | Feature | DoD | Est. min | -|---|---|---|---| -| A0 | **Verification + harness skeleton.** Grep pass at HEAD: `BunchOfCaps` switch and `validCaps` path, `XInventoryService` folder-version bump sites, `CreateInventoryCategory` cap, COF folder type resolution. Create `OpenSim.Region.ClientStack.Linden.Caps/AIS/` (or `Addons/`, per finding) module skeleton with `[AIS] Enabled = false` gate, plus the acceptance harness: HTTP client + LLSD envelope fixtures (`_embedded{categories,items,links}`, `_links`, `_updated_items`, `_created_items`, `_removed_items`, `_updated_categories`, `_category_items_removed`) as golden files. | Report with file:line; harness runs 0 tests green; **decision A-D1** answered (region-side proxy vs Robust-hosted, §4). | 30 | -| A1 | **Fetch surface.** `GET /item/`, `GET /category/`, `/children?depth=n`, `/children?depth=*&children=…` (subset), `/categories`, `/links`, `/category/current/links` (COF alias), `/orphans`. Links are a separate collection, never items. | Harness green on all fetch routes against a fake `IInventoryService` and against a real region for Truly. | 45 | -| A2 | **Item/category mutations.** `PATCH /item`, `PATCH /category` (name, desc, thumbnail, sale info), `DELETE /item`, `DELETE /category` (folder + descendents), `tid` echo, per-operation version-bump rule (parent bump on item ops, self+parent on moves), delta envelopes. | Harness asserts exact delta sets and version numbers per op. | 40 | -| A3 | **SlamFolder + create.** `PUT /category//links?tid=` atomic replace-all-links under the folder lock (all-or-nothing, proven by a fault-injection test), `POST /category//children` creating items/categories/links. | Fault-injection test: failure mid-slam leaves the COF unchanged; LL viewer on the test region changes outfit and it persists across relogin. | 40 | -| A4 | **Purge, library copy, simulate.** `DELETE /category//children` (Empty Trash / Lost and Found), `COPY /category/?tid=` (CopyLibraryCategory), `simulate` dry-run on mutations, HTTP status codes the viewer branches on. | Harness green; Empty Trash works in the LL viewer. | 35 | -| A5 | **Advertise.** `InventoryAPIv3` in the caps seed **only** when `[AIS] Enabled` — through `validCaps`, not just the flag switch (RECON-01 rule). Firestorm-on-cap behaviour check (RECON-02 UNVERIFIED). | Cap absent by default; present on the test region; Firestorm on that region runs inventory through AIS without regressions on a scripted checklist. | 25 | - -AIS lane total: **~3.6 h CC, 6 sessions.** Verify loops of yours: A3 (LL viewer outfit change), A5 (Firestorm checklist). - -## 3. Interleave - -``` -week-view (CC sessions, left→right; ≈ your verify loop) - -AIS : A0 ──► A1 ──► A2 ──► A3≈ ──► A4 ──► A5≈ ─────────────┐ - ├──► J1 joint soak ≈ -SSB : S0a ──► S0b≈ ──► S1≈ ──► S2 ──► S3≈ ──► S4≈ ──► S5≈ ─┘ - ▲ - A3 must land before S5 ──────┘ S6 (web viewer) any time after S3 -``` - -Rules: -- **Alternate, don't overlap.** Two worktrees, but one CC session at a time, so a bug is attributable to one session. Order that respects the dependencies: `S0a, A0, S0b, A1, S1, A2, S2, A3, S3, A4, S4, A5, S5, S6, J1`. -- Both lanes gated on their region flags; both default off in every shipped ini. -- **J1 — joint soak (T+W, ~35 min):** replaces SSB S7 and an AIS soak. Test region with both flags on: LL viewer logs in textured, changes outfit → SlamFolder → `UpdateAvatarAppearance` → rebake → `AvatarAppearance` with new `CofVersion`; Firestorm on the same region does the same; web viewer consumes. Flags off → all three revert cleanly. Harness diffs ≤ threshold on all three test avatars' outfits. - -## 4. Decisions this plan adds - -| ID | Decision | Recommendation | -|---|---|---| -| A-D1 | AIS hosting: region-side caps module translating to `IInventoryService` (Phase 1) vs Robust-hosted service with a per-agent tokenized URL (Mike's "inventory out of the simulator") | **Phase 1 region-side, behind an interface so Phase 2 can lift the same handler onto a Robust connector.** Region-side gets auth free from the caps seed and needs no new wire trust; the translation layer is identical either way. Phase 2 is a single session later, not a redesign. | -| A-D2 | Merge order | AIS branch merges first (owns folder-version semantics); SSB rebases on it before S5. | -| A-D3 | Shared test region | Transylvania (D-5), both flags on. | - -## 5. Timeline answer - -| | CC wall-clock | Sessions | Your verify loops | -|---|---|---|---| -| SSB alone | ~4.75 h | 9 | 5 | -| AIS alone | ~3.6 h | 6 | 2 | -| **Both, interleaved** | **~8.5 h** (S7 folded into J1) | **15** | **~7** | - -At the web-viewer cadence (12 sessions over two working days, verify loops between), that is **three working days**, four if the golden fixtures (Q-7) or the wipe-loop check (Q-3) turn up work. Running the two lanes strictly one-at-a-time costs nothing in CC time versus running them concurrently — the CC minutes are the same — and keeps bugs attributable, which is what made the web-viewer sessions cheap to verify. diff --git a/Docs/feature/ssb-appearance/DESIGN-BRIEF-ssb-appearance.md b/Docs/feature/ssb-appearance/DESIGN-BRIEF-ssb-appearance.md index ab8453a0288..730b51fff87 100644 --- a/Docs/feature/ssb-appearance/DESIGN-BRIEF-ssb-appearance.md +++ b/Docs/feature/ssb-appearance/DESIGN-BRIEF-ssb-appearance.md @@ -52,7 +52,7 @@ G6. Ordinary OpenSim grid owners can run the web viewer without any of this; SSB / --->| (proxy to AssetService) | | +------------------------------+ | | - Web-viewer gateway (D:\web-viewer) ---- on non-SSB grids only ----------------+ + Web-viewer gateway (separate repository) ---- on non-SSB grids only ----------+ on SSB regions: appearance-passive, consumes AvatarAppearance + asset route ``` @@ -60,13 +60,13 @@ G6. Ordinary OpenSim grid owners can run the web viewer without any of this; SSB | # | Component | Repo / location | New or changed | |---|---|---|---| -| C1 | `OpenSimNGC.Appearance.Baking` — shared compositor library | Tranquillity tree (placement: ADR-003) | **new project**, code lifted from `D:\web-viewer\gateway\src\Gateway\Baking\` | +| C1 | `OpenSimNGC.Appearance.Baking` — shared compositor library | Tranquillity tree (placement: ADR-003) | **new project**, code lifted from the web-viewer gateway's `Gateway/Baking/` | | C2 | `AppearanceBakeModule` — region module: orchestration, cap, triggers, sender | `Addons/` or `Source/OpenSim.Region.OptionalModules` (ADR-003) | new | | C3 | `BakeStore` — persist bakes as assets, record channel→UUID + input hash + COF version in the avatar service, expiry reaper | region module + Robust reaper | new | | C4 | `AppearanceServiceConnector` — Robust HTTP handler for `texture///` and login-response `agent_appearance_service` | Robust | new (ADR-002) | | C5 | `LLClientView.SendAppearance` — emit `AppearanceData{AppearanceVersion=1, CofVersion}` when the avatar is server-baked; unchanged otherwise | `OpenSim.Region.ClientStack.Linden.UDP` | changed, add-only | | C6 | `RegionHandshake` — set bit 0 of `RegionProtocols` when `[Appearance] ServerSideBaking = true` | ClientStack | changed, flag-gated | -| C7 | Gateway SSB-aware mode | `D:\web-viewer` | changed (Build Plan S6) | +| C7 | Gateway SSB-aware mode | the web-viewer gateway, a separate repository | changed there, not here | ### 4.2 Bake pipeline (C2 → C1 → C3) @@ -300,18 +300,6 @@ So the gateway gets `cof_version` and `appearance_version` for self and for othe `LibreMetaverse.LoginResponseData.AgentAppearanceServiceURL` (get/set) and `LibreMetaverse.NetworkManager.AgentAppearanceServiceURL` (get). The gateway does not need to read the raw login LLSD. This is the value S4 taught Robust to advertise and `llstartup.cpp` adopts only when non-empty. -### 7.4 Where the compositor project reference points — **stale branch, current content** - -`gateway/src/Gateway/Gateway.csproj` references - - D:\tranq-ssb\Source\OpenSimNGC.Appearance.Baking\OpenSimNGC.Appearance.Baking.csproj - -`D:\tranq-ssb` is a worktree on **`feature/ssb-appearance` at `162bfadcc3`** ("perf(ssb): instrument the bake phases and answer Q-10", S2 Part 2), not the integration branch. It is 20+ commits behind `feature/ais-v3`. - -**It does not currently matter for correctness.** `git diff 162bfadcc3 b13f15add3 -- Source/OpenSimNGC.Appearance.Baking/` is empty and no commit in that range touches the library: S3, S4 and S5 changed the region module, the services and the wire, never the compositor. The gateway is therefore building a byte-identical library to the one at `b13f15add3`. - -It is still a hazard rather than a fact to file away: the next change to the library will land on `feature/ais-v3` and the gateway will silently keep building the old one. **Not repointed in this session — John's decision.** - ### 7.5 The gateway's self-appearance flow — every route to the bake/send step Three entry points, all funnelling into one method: diff --git a/Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md b/Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md index 4c116928ca2..a9f549fbacd 100644 --- a/Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md +++ b/Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md @@ -1,15 +1,14 @@ # RECON Addendum — Server-Side Baking (SSB) for NGC-Tranquillity -**Programme:** Track L, item L-2 (BUILD-PLAN-sl-parity-v2) -**Supplements:** `RECON-ssb-appearance.md` (Claude Code recon, delivered 2026-09-02 to `D:\_TO_REVIEW\ssb-appearance\`) -**Tree pin:** the parity audit's findings are pinned to `645b0f3`; live grid runs `cb141dd61d` + `db7c746248` (maptile fix). Nothing appearance-related has changed between those commits as far as chat history shows — **VERIFY at S0** (Build Plan). +**Supplements:** an earlier reconnaissance pass over the appearance surface, not carried here; everything this +document relies on is restated below. **Date:** 2026-09-03 ## 1. Why an addendum The CC recon was written before the web-viewer Sessions 11 and 12 and the appearance wire spike. Those three pieces of work changed the SSB picture materially: -1. A **working, data-driven bake compositor now exists in C# on .NET 10** — `gateway/src/Gateway/Baking/` in `D:\web-viewer`. It interprets `avatar_lad.xml` layer sets with the viewer's `LLTexLayerSet` semantics, has a fidelity gate, and has been compared against Firestorm bakes on real avatars. The recon's "port BakeLayer.cs onto SkiaSharp+CoreJ2K" recommendation is therefore **already ~done, in the wrong repo**. SSB on the sim is no longer a compositor project; it is a *plumbing* project plus a *library extraction*. +1. A **working, data-driven bake compositor now exists in C# on .NET 10** — `Gateway/Baking/` in the web-viewer gateway, a separate repository. It interprets `avatar_lad.xml` layer sets with the viewer's `LLTexLayerSet` semantics, has a fidelity gate, and has been compared against Firestorm bakes on real avatars. The recon's "port BakeLayer.cs onto SkiaSharp+CoreJ2K" recommendation is therefore **already ~done, in the wrong repo**. SSB on the sim is no longer a compositor project; it is a *plumbing* project plus a *library extraction*. 2. **LibreMetaverse 3.1.4's `Baker` is disqualified** as a backend for anything that persists (decompile-confirmed: tiles sub-1024 layers into a 2×2 mosaic; earlier: skips layers). The recon's `IBakeBackend` seam stays, but "managed baker as default" now means *our* compositor, not LibreMetaverse's. 3. The wire spike established what the **sim already delivers** with zero grid changes: other avatars' baked-texture UUIDs (5 legacy slots) in `AvatarAppearance`, fetchable as ordinary assets; `VisualParams` present; `AppearanceData` block still omitted (count 0). The only reason a passive client stays a cloud is that nothing bakes for it. @@ -65,7 +64,7 @@ Consequence of V7: **SSB without AIS gives the LL viewer "log in as yourself, ca ## 5. Halcyon reference — what to take, what not -From `/d/halcyon-reference-fresh/` (read-only). The recon recommended "Halcyon's persistent-bake rule". Restating it precisely so it is not over-applied: +From a read-only checkout of the Halcyon source tree. The recon recommended "Halcyon's persistent-bake rule". Restating it precisely so it is not over-applied: - **Take:** bakes are first-class persisted assets tied to the avatar record; a login does not force a rebake if the stored bakes match the stored wearables; a change to wearables/params invalidates them. - **Take:** hash-of-inputs as the invalidation key (wearable asset IDs + visual params + texture IDs per bake channel), so the compositor is skipped when nothing changed. diff --git a/Docs/feature/ssb-appearance/RECON-ssb-appearance.md b/Docs/feature/ssb-appearance/RECON-ssb-appearance.md deleted file mode 100644 index 72f30f95ccc..00000000000 --- a/Docs/feature/ssb-appearance/RECON-ssb-appearance.md +++ /dev/null @@ -1,263 +0,0 @@ -# Recon Report — Server-Side Baking (SSB) / Server-Side Appearance - -**Status:** DRAFT for review — recon + design brief. No code, no branch, no build. Supersedes the appearance findings of `RECON-03-avatar-appearance.md` (Pass 3, pinned `645b0f3`) for this subject. -**Scope:** what the code does today, the two lineages that could deliver SSB, a recommendation, the compositor design, a safe rollout, dependencies, and a build-plan skeleton sized to a first demoable milestone. - -**Tree:** `JohnLegionH/OpenSim-Tranquillity` at `/d/tranquillity-develop` -**Commit:** `a68d59f232340b62b3e12ee4c9f62f4f2374e60d` — "fix(map): restore legacy MapImageModule terrain rendering after Skia rework", 2026-09-02, checkout branch `feature/voice-visibility-matrix`. 167 commits after Pass 3's `645b0f3bb3` (2026-07-31). -**Target framework:** `net10.0` -**References read (read-only):** Halcyon at `/d/halcyon-reference-fresh` ("Copyright (c) InWorldz Halcyon Developers"); Firestorm viewer source at `/d/phoenix-firestorm/indra/newview` (the LL-upstream code with Firestorm's `[Legacy Bake]` additions marked, which is how the stock-LL behaviour was isolated — no separate LL checkout exists on this machine, `/d/SLViewer-Source` is empty); stock OpenSim 0.9.3 at `/d/opensim - Use this december 2025`; libopenmetaverse source at `/d/libomv-src`; LibreMetaverse 3.1.4 (the web viewer gateway's library) from the NuGet cache; the live grid's configuration under `/d/legiongrid`. -**Method:** direct inspection with `grep`/`sed`; every file:line below is to the commit above unless another tree is named. - ---- - -## R0. Delta against Pass 3 (`645b0f3`) - -None of the 167 commits between `645b0f3` and HEAD touch the appearance path. `AvatarFactoryModule.cs`, `LLClientView.cs` (handshake and `SendAppearance`), `SimulatorFeaturesModule.cs`, `UploadBakedTextureModule.cs`, `XBakesModule.cs` and `AvatarAppearance.cs` carry no SSB-related change. **Pass 3's findings stand at HEAD.** Two of them are *refined* below rather than corrected: the missing `AppearanceData` block (R3) explains why a stock LL viewer shows *other* avatars as clouds too, not only itself; and the web-viewer side turns out to already speak the SL SSB client contract (R9), which changes the sizing of the "serve both viewers" requirement. - -## R1. No bake cap is registered; the SSB advertisement bit is clear - -**Caps.** The complete set of cap names registered anywhere under `Source/OpenSim.Region.ClientStack.LindenCaps` and `Source/OpenSim.Region.CoreModules/Framework` was enumerated (`RegisterHandler(`, `RegisterSimpleHandler(`). It contains `UploadBakedTexture` and `GetTexture`; it does **not** contain `UpdateAvatarAppearance`, nor any appearance-service cap. The `UploadBakedTexture` cap is the *client-bake* upload path: `Source/OpenSim.Region.ClientStack.LindenCaps/UploadBakedTextureModule.cs:97-112` registers it (locally when `Cap_UploadBakedTexture = "localhost"`, `OpenSimDefaults.ini:867`) and `:189-209` stores whatever the viewer uploads as a `Temporary = true`, `Local = true` texture asset (`:207-208`). - -**Where a bake cap would register.** The pattern to copy is `UploadBakedTextureModule.RegisterCaps` (`:97-112`): an `ISharedRegionModule` hooking `Scene.EventManager.OnRegisterCaps` and calling `caps.RegisterSimpleHandler("UpdateAvatarAppearance", …)`. `SimulatorFeaturesModule.cs:188` shows the same hook for `SimulatorFeatures`. - -**RegionProtocols.** The RegionHandshake writer sets the `RegionInfo4` block explicitly, `Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs:986-995`: - -``` -//RegionInfo4 block -//RegionFlagsExtended -zc.AddByte(1); -zc.AddUInt64(regionFlags); -//RegionProtocols - // bit 0 signals server side texture baking - // bit 63 signals more than 6 baked textures support" -zc.AddUInt64(1UL << 63); -``` - -So **bit 0 (SSB) is 0 and bit 63 (eleven bake slots / Bakes-on-Mesh) is 1**, hard-coded, for every region. There is no configuration switch; `grep RegionProtocols` over `Source` finds only this site. `SimulatorFeatures` separately advertises `BakesOnMeshEnabled = true` (`SimulatorFeaturesModule.cs:136`). - -**Who reads it.** Viewer side: `llviewerregion.cpp:3264-3277` reads `RegionInfo4.RegionProtocols` from the handshake and `:3316` derives `mCentralBakeVersion = region_protocols & 1`. That single value gates the whole SSB request path: `llappearancemgr.cpp:4268-4272` returns "Region does not support baking" when it is 0, before the `UpdateAvatarAppearance` cap is even looked up (`:4274-4278`). LibreMetaverse 3.1.4 exposes the same bit as `RegionProtocols.AgentAppearanceService` (R9). Nothing on the sim side reads the bit back. - -## R2. What the appearance path does today: client-bake pass-through plus an optional bake cache - -The sim never composes a texture. It receives the viewer's own bakes and relays them. - -1. **Inbound.** `LLClientView.cs:8430` maps `AgentSetAppearance` to `HandlerAgentSetAppearance` (`:9196-9221`), which decodes the TextureEntry, visual params, avatar size and the `WearableData` cache items and raises `OnSetAppearance`. `AvatarFactoryModule.Client_OnSetAppearance` (`Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs:1179-1186`) → `SetAppearance` (`:168-215`): `SetVisualParams` (`:185`), `SetTextureEntries` (`:195`), `UpdateBakedTextureCache` (`:199`), then `QueueAppearanceSave` / `QueueAppearanceSend` (`:214-215`). The TextureEntry the viewer sends already *contains the baked texture UUIDs* it uploaded through `UploadBakedTexture`; the sim stores the ids, never the layers. -2. **Cache validation.** `AgentCachedTexture` (`LLClientView.cs:8368`, `:12389`) → `Client_OnCachedTextureRequest` (`AvatarFactoryModule.cs:1243-1263`) answers from `sp.Appearance.WearableCacheItems`; `ValidateBakedTextureCache` (`:485-670`) checks the region asset cache and, when `IBakedTextureModule` is present, the external bake store (`:589-640`, assets re-flagged `Temporary`/`Local` at `:621-622`). A miss ends in `RequestRebake` (`:674-718`) → `SendRebakeAvatarTextures` (`:715`), i.e. *the viewer is asked to bake again*. `ScenePresence.cs:2291-2295` runs this validation on region entry. -3. **Outbound.** `ScenePresence.SendAppearanceToAgent` (`ScenePresence.cs:4338-4347`) → `LLClientView.SendAppearance` (`:4499-4543`), the `AvatarAppearance` packet: TextureEntry, visual params, **no `AppearanceData` block** (`:4532-4533`, literally `// no AppearanceData` then a zero count) and an `AppearanceHover` block. -4. **Wearables and COF.** `SetAppearanceAssets` (`:839-1066`) resolves wearable item ids to asset ids through `IInventoryService` at save time; `TryAndRepairBrokenWearable` (`:1067-1116`) rebuilds Current Outfit Folder links. `Client_OnRequestWearables` (`:1158`) and `Client_OnAvatarNowWearing` (`:1194`) are the legacy UDP wearable paths. The sim reads COF and wearables today without AIS (relevant to §E). -5. **Bake storage.** `IBakedTextureModule` (`Source/OpenSim.Region.Framework/Interfaces/IBakedTextureModule.cs`: `Get(UUID)`, `Store(UUID, WearableCacheItem[])`, `UpdateMeshAvatar`) is implemented by `XBakesModule` (`Source/OpenSim.Region.CoreModules/Avatar/BakedTextures/XBakesModule.cs`), a REST client to Robust's `XBakes` file store (`Source/OpenSim.Server.Handlers/BakedTextures/XBakes.cs:56-107`, `BaseDirectory`). It is inert unless `[XBakes] URL` is set (`XBakesModule.cs:55-67`). On the live grid Robust *does* host the service (`/d/legiongrid/gridserver/config/Robust.ini:281-282`, `[BakedTextureService] LocalServiceModule = "OpenSim.Server.Handlers.dll:XBakes"`) but no region config sets `[XBakes] URL`, so the regions never talk to it; `PersistBakedTextures = false` (`OpenSimDefaults.ini:947`) keeps uploaded bakes temporary. - -**Net effect for a stock LL viewer.** It never sends `AgentSetAppearance` or uploads bakes (those code paths exist in Firestorm only inside `// [Legacy Bake]` blocks, e.g. `llagentwearables.cpp:510`, `:529`, `llagent.cpp:6369-6703`), never asks `UpdateAvatarAppearance` (R1), so the sim holds the default TextureEntry and everyone sees a cloud. Only Firestorm's legacy client-bake path (enabled on OpenSim grids) masks this — and it is Firestorm-only. - -## R3. Refinement: the missing `AppearanceData` block makes *other* avatars fail too - -The viewer resolves an avatar's appearance version from the `AvatarAppearance` packet (`llvoavatar.cpp:10729-10732` reads `AppearanceData.AppearanceVersion` and `CofVersion`; `:10838-10846` reads visual param 11000). LL-upstream's resolution, preserved as commented-out lines at `llvoavatar.cpp:10864-10874`, is: param if present, else field if > 0, **else 1** ("still not set, go with 1"). Firestorm's replacement (`:10875-10886`) resolves the same missing data to **0** (legacy). `:11015` then calls `setIsUsingServerBakes(appearance_version > 0)`. - -Because Tranquillity sends no `AppearanceData` (R2 step 3), a **stock LL viewer treats every avatar it sees as server-baked**, builds bake URLs from the appearance-service URL (`llvoavatar.cpp:6815-6836`: `texture///`), finds the URL empty ("`AgentAppearanceServiceURL not set - Baked texture requests will fail`", `:6825`) and never fetches. The fix for that is part of SSB anyway (R7), but note it: **once the sim emits `AppearanceVersion = 1` it must also serve bake URLs, and until then it must emit `AppearanceVersion = 0` explicitly** rather than omit the block. Emitting `0` is a zero-risk, viewer-visible improvement independent of SSB (Firestorm ignores it; LL viewers stop trying the bake service for legacy avatars). Recorded as ledger F-3 / D-4. - -## R4. The SL server-bake contract, as the viewer implements it - -This is the contract Tranquillity must meet; it is small and precisely observable in the viewer source. - -| Step | Viewer behaviour | Source | -|---|---|---| -| Advertise | `RegionProtocols` bit 0 → `mCentralBakeVersion` | `llviewerregion.cpp:3316` | -| Locate the bake server | login response field `agent_appearance_service` (URL, trailing slash expected) | `llstartup.cpp:5161-5166` | -| Request a bake | `POST ` with LLSD `{ "cof_version": N }` (a debug setting can send the whole COF instead, `:4343-4351`) after every outfit change; skipped while editing appearance | `llappearancemgr.cpp:4243-4351` | -| Reply | LLSD map: `success` (bool) required; on failure `error` (string) and optionally `expected` (int) — a COF-version mismatch makes the viewer re-request its own `AvatarAppearance` and retry with back-off up to `BAKE_RETRY_MAX_COUNT` | `:4359-4400` | -| Result delivery | the sim broadcasts `AvatarAppearance` with `AppearanceData.AppearanceVersion = 1` and `CofVersion = N`; visual param 11000 must agree | `llvoavatar.cpp:10729-10732`, `:10851-10863` | -| Fetch bakes | per baked slot: `GET texture///` (`FTT_SERVER_BAKE`, not written to the viewer's texture cache, expects J2C) | `llvoavatar.cpp:6831`, `lltexturefetch.cpp:1818`, `:2811` | -| Sanity | if the avatar is server-baked but the region says CBV 0, the viewer probes the bake URL and may force an update | `llvoavatarself.cpp:3636-3657` | - -`bakeName` is the texture entry's default image name from the viewer's avatar dictionary (`head`, `upper`, `lower`, `eyes`, `skirt`, `hair`, `leftarm`, `leftleg`, `aux1`…`aux3`); the sim can treat it as opaque and key on ``. - -The LLSD *success* payload's texture and visual-param contents are not consumed by the viewer in this code path (the viewer waits for the UDP `AvatarAppearance` instead — `:4380-4386` "the message will return through the UDP"); returning `{ success: true, cof_version: N }` is sufficient, with `textures`/`visual_params` optional for diagnostics. - -## R5. The appearance model already has the slots SSB needs - -`Source/OpenSim.Framework/AvatarAppearance.cs`: `VISUALPARAM_COUNT = 218` (`:54`), `TEXTURE_COUNT = 45` (`:57`), `BAKE_INDICES = { 8, 9, 10, 11, 19, 20, 40, 41, 42, 43, 44 }` (`:63`) — the six classic bakes plus left-arm, left-leg and aux1–3 for Bakes-on-Mesh — and `WearableCacheItems` (`:126`). `WearableCacheItem` (`Source/OpenSim.Framework/WearableCacheItem.cs:34-39`) carries `TextureIndex`, `CacheId`, `TextureID`, `TextureAsset`. `Serial` (`:77`) is the field Halcyon and the viewer both use as the COF version. Nothing here needs to change for SSB; the compositor writes into the same slots the viewer would have. - -## R6. Building blocks already in the tree, and one that is missing - -| Need | Present? | Where | -|---|---|---| -| J2K decode | yes | `CoreJ2K.Skia` (9 projects); `GetTextureHandler.cs:303` | -| J2K encode | **yes** | `Source/OpenSim.Framework/SkiaImageUtils.cs:27-52` `TryEncodeToJ2KLossless(SKBitmap)` (CoreJ2K encoder; lossless preset, a lossy preset is a one-line variant) | -| Raster ops (resize, blend, tint, masks) | yes | SkiaSharp 4.151.1 (11 projects) | -| Wearable asset parser (`LLWearable` text: params + textures) | yes | `UtopiaSkye.OpenMetaverse` 1.1.6 (`Directory.Build.props:13-17`); the DLL exports `AssetWearable` and `VisualParams` | -| Visual-param / alpha-mask definitions (`avatar_lad.xml`) and the TGA mask layers | yes, shipped | `/d/legiongrid/regionserver/openmetaverse_data/` (`avatar_lad.xml`, `head_alpha.tga`, …), loaded by libomv's `VisualParams` from `Settings.RESOURCE_DIR` | -| **The compositor itself** | **no** | `UtopiaSkye.OpenMetaverse` does not export `Imaging.Baker`/`BakeLayer` (checked against the DLL; the fork dropped System.Drawing-era imaging, see `Docs/OPENMETAVERSE_SYSTEM_DRAWING_SPIKE.md`). The reference implementation exists in libomv (`/d/libomv-src/OpenMetaverse/Imaging/BakeLayer.cs`, 672 lines, BSD-3) and in LibreMetaverse 3.1.4 (`LibreMetaverse.Imaging.Baker`) | -| Inventory/COF read from the sim | yes | `AvatarFactoryModule.cs:839-1116` via `IInventoryService` | -| Bake asset serving to viewers | partly | `GetTextureHandler.cs:142-163` serves any `AssetType.Texture` from the asset service; the SL bake URL shape (R4) is a different route that does not exist | -| Login-response field | no | `Source/OpenSim.Services.LLLoginService/LLLoginResponse.cs:486-497` has no `agent_appearance_service` | -| AIS v3 | **no** | `Source/OpenSim.Services.AISv3` contains only a `WeatherForecast` scaffold | - -## R7. Halcyon lineage: persistent grid-side bake cache, no compositor - -Halcyon is client-bake with a grid-wide cache; it never composes either. - -- **Upload persists.** `OpenSim/Region/CoreModules/Capabilities/AssetCapsModule.cs:206-209` registers `UploadBakedTexture`; `:387-420` stores the upload as a `Local = true` asset with `Temporary` deliberately *not* set ("Persist baked textures as we will use them in the baked texture cache", `:412-416`). -- **Cache is keyed by the viewer's cache id and lives in the user database.** `ScenePresence.SetAppearance` (`OpenSim/Region/Framework/Scenes/ScenePresence.cs:3546-3600`) builds `cacheId → textureId` from the `WearableData` blocks (`:3552-3563`, note the V1/V2 index conversion), sets `Serial` from the COF version (`:3566`) and hands both to `IAvatarFactory.UpdateDatabase` (`OpenSim/Region/Framework/Interfaces/IAvatarFactory.cs`, two methods). `AvatarFactoryModule.UpdateDatabase` (`OpenSim/Region/CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs:656-743`) coalesces updates for 3 s, refuses appearances with a zeroed required wearable, then calls `AvatarService.UpdateUserAppearance` and `SetCachedBakedTextures` (`:737-738`). The user server exposes `get_cached_baked_textures` / `set_cached_baked_textures` over XML-RPC (`OpenSim/Grid/UserServer.Modules/UserServerAvatarAppearanceModule.cs:76-77`, `:267-310`) backed by a `cachedbakedtextures (cache, texture)` table (`OpenSim/Data/MySQL/MySQLUserData.cs:1565-1610`). -- **Cache hits answer `AgentCachedTexture` from the grid**, not the region: `AvatarFactoryModule.cs:387-410` (`_cacheBakedTexturesEnabled`, `:298`, `:369-370`), zero-filling indexes the grid does not know so the viewer rebakes only those. -- **COF building for V1 viewers.** `BuildCOF` (`:58-170`) and `AvatarIsWearing` (`:535-650`) synthesise Current Outfit links for viewers that cannot manage their own COF — solved a 2011 problem, irrelevant to modern viewers. -- **No compositor.** A grep for `Oven`, `BakeLayer`, `Composite`, `ManagedImage` outside `ThirdParty/` hits only unrelated files; the only baker in the tree is libomv's client-side `ThirdParty/libopenmetaverse/OpenMetaverse/Imaging/BakeLayer.cs` and a desktop `Programs/Baker` tool. - -**Assessment.** Halcyon's design is a better *client-bake* experience than stock OpenSim's XBakes (cross-region and cross-login cache hits, so Firestorm users rebake less; bakes survive restarts because they are stored as non-temporary assets), but it does nothing for a viewer that will not bake. Porting it into Tranquillity would mean: a Robust `cachedbakedtextures` service (XBakes already stores by agent, not by cache id), a persistent-asset flag on `UploadBakedTexture`, and answering `AgentCachedTexture` from the grid. All of that is orthogonal to SSB and helps only Firestorm-legacy users. It does not move either gate in the brief. - -## R8. Stock-OpenSim lineage: no SSB either; XBakes is a cache of viewer bakes - -`grep UpdateAvatarAppearance` over stock 0.9.3 returns nothing; there is no appearance service and no compositor upstream. What upstream *did* build is exactly what Tranquillity carries: `UploadBakedTexture` (temporary/local storage), `XBakes` (Robust file store keyed by agent, `XBakes.cs:56-107`, region client `XBakesModule.cs`), `ValidateBakedTextureCache`/`RequestRebake`, bit 63 and `BakesOnMeshEnabled` for eleven-slot BOM, and the hard-coded `RegionProtocols` value with the comment that bit 0 "signals server side texture baking" (`LLClientView.cs:993`). Upstream's intent, as far as the tree shows it, is to *support viewers that bake* and to *refuse to advertise* SSB. "Porting or completing stock SSB" therefore has no source to port; it means writing the appearance service from the viewer contract (R4). - -## R9. The web viewer's gateway library already speaks the SL SSB client contract - -LibreMetaverse 3.1.4 (the `web-viewer` gateway's dependency) exports `AppearanceManager.UpdateAvatarAppearanceAsync(CancellationToken, int cofVersion)`, `AssetManager.RequestServerBakedImageAsync(UUID avatarId, UUID textureId, string bakeName, …)`, `NetworkManager.AgentAppearanceServiceURL` (populated from the login reply) and `RegionProtocols.AgentAppearanceService` (bit 0), plus `ImageType.ServerBaked`. It also still ships the client-side `LibreMetaverse.Imaging.Baker` (512×512 / 128×128 eyes) with the `content/linden/character` resources. - -Consequence: if Tranquillity implements the *SL* contract (cap + login field + bake URL route + `AppearanceVersion = 1`), the gateway needs no protocol work to dress avatars — it reads the bit, asks for a bake for the agent it is logged in as, receives `AvatarAppearance` for everyone, and fetches bakes by the same URL a viewer uses. A *non-SL* design (bakes only reachable through `GetTexture`, no appearance-service URL) would still work for the gateway if bakes are stored as ordinary texture assets, but would not work for the LL viewer at all (R3/R4). This is the strongest argument for keeping the wire contract SL-exact. - -## R10. Who bakes NPCs today - -`Source/OpenSim.Region.OptionalModules/World/NPC/NPCModule.cs:135` still carries upstream's "We can't just use IAvatarFactoryModule.SetAppearance() yet". NPCs are created from a stored `AvatarAppearance` whose TextureEntry points at baked textures that *some viewer* uploaded earlier; with `PersistBakedTextures = false` those assets are temporary, so NPC bodies survive only as long as the region's asset cache does. SSB with persistent bakes fixes NPC appearance as a side effect (a server bake is reproducible from wearables at any time). Relevant to the bot/NPC track (`feature/bot-npc-framework`). - ---- - -## A. The architectural fork - -### A.1 Option (a) — stock-OpenSim-style SSB - -There is nothing to port (R8); "stock-style" means: a region-side compositor + an `UpdateAvatarAppearance` cap + storing bakes through the existing asset service, with `XBakes` optionally kept as the persistent store. Work items: compositor (§B), the cap module, the bake-URL route, the login field, `AppearanceData` in `SendAppearance`, per-region protocol bit. Fidelity is whatever the compositor achieves. Serves the LL viewer and the gateway equally because it is the SL contract. - -### A.2 Option (b) — Halcyon/InWorldz-lineage server-side appearance - -Also has nothing that bakes (R7). Its transferable ideas are storage-side: persist bakes as real assets, key a grid cache by cache id, coalesce appearance saves, answer `AgentCachedTexture` from the grid. Porting means new Robust surface and database tables that serve *client-baking* viewers only. It does not open either gate and would still need everything in (a) to do so. - -### A.3 Where each is better or worse - -| Criterion | (a) SL-contract SSB on Tranquillity | (b) Halcyon-style cache | -|---|---|---| -| LL viewer shows a body | yes, once the compositor works | no | -| Web viewer shows a body | yes, no gateway protocol work (R9) | no (still needs a bake to exist) | -| Firestorm legacy users | unchanged until the region advertises bit 0; then Firestorm switches to SSB like LL (`llagent.cpp:6369-6382`) | fewer rebakes, bakes persist | -| Fidelity | bounded by the compositor (§B) — the viewer's own bake is the reference and will look slightly different | pixel-identical (it is the viewer's bake) | -| Effort | compositor + cap + route + login field + storage + rollout gating | grid service + table + cap change + cache answers | -| Risk | rollout (§D); compositor correctness | low; none of the risks in the brief | -| Persistence | bakes are reproducible; can be re-baked on demand | bakes persist but cannot be regenerated | - -### A.4 Recommendation: **(a), as an SL-contract appearance service inside the region, with (b)'s persistence rule adopted as storage policy.** - -Reasoning, tied to the brief's three criteria: - -- **Fidelity.** Only a compositor puts a body on a viewer that will not bake; (b) has none, so fidelity for the two gated viewers is zero under (b). Under (a) the achievable fidelity is "close to a viewer bake" (§B.4 lists the known gaps), and the gaps are in the compositor, which can be iterated without touching the protocol. -- **Effort.** (b) is cheaper but buys nothing against either gate; (a)'s protocol surface is small and fully specified by the viewer source (R4, seven concrete points). The compositor is the only sizeable unknown, and a BSD-3 reference implementation exists to port (R6). -- **Serving both viewers.** The SL contract is the *only* one both consumers implement today: the LL viewer (R4) and LibreMetaverse (R9). Any Tranquillity-specific variant would need custom code in the gateway and would never work for the LL viewer. - -From (b) keep one rule: **bakes are stored as persistent, regenerable texture assets** (not `Temporary`), so `GetTexture`, `XBakes`, NPCs and the gateway all see them, and a restart does not cloud everyone. - -**What would change this recommendation.** (1) If the compositor's fidelity proves unacceptable on real content (mesh bodies with BOM rely on the skin/tattoo/alpha bakes being right) *and* no better compositor can be sourced, the fallback is to keep the region legacy (bit 0 clear, `AppearanceVersion = 0`) and accept that the LL viewer stays unsupported — the web viewer could then bake in its own gateway with LibreMetaverse's `Baker` instead. (2) If a maintained third-party SSB service surfaces that Tranquillity could proxy the cap to (`caps.RegisterHandler("UpdateAvatarAppearance", url)` is the existing pattern for remote caps, `UploadBakedTextureModule.cs:108-109`), the in-region compositor becomes optional. Neither exists on this machine or in these trees today. - ---- - -## B. The compositor - -### B.1 Inputs - -Per avatar, from the sim's own data: the COF (links → wearable items → assets, exactly what `SetAppearanceAssets` walks, `AvatarFactoryModule.cs:839-1066`), each wearable's `LLWearable` asset (parameters + per-slot texture ids; parsed by `AssetWearable` from the linked libomv), the visual-param definitions and alpha-mask TGAs from `openmetaverse_data/avatar_lad.xml`, and the wearable textures via the asset service (J2K decode through CoreJ2K). - -### B.2 Layer model (what the reference does, `libomv-src/OpenMetaverse/Imaging/BakeLayer.cs`) - -Per bake type: canvas 512×512 (128×128 eyes) initialised to the base colour (`:118-125`); skin/body-paint and tattoo layers pulled out for special ordering on the head bake (`:130-150`, `:183-187`); built-in base layers `head_color.tga` / `upperbody_color.tga` / `lowerbody_color.tga`, head alpha and skin-grain multiply (`:153-167`); then each clothing texture in slot order, resized to the bake (nearest-neighbour, `:192-197`, with a `FIXME` to tile instead), tinted with the wearable colour (`ApplyTint`, `:222`, `:579`), masked by the wearable's alpha params (`VisualAlphaParam`, multiply vs non-multiply blends, `:239-270`), drawn with source alpha only for skirt/hair layer 0 (`:292-293`); finally the hair layer of the head bake multiplied by `head_hair.tga` (`:203-210`). `AppearanceManager.DecodeWearableParams` (`AppearanceManager.cs:1376-1478`) is the piece that turns a wearable's parameters into `AlphaMasks` and colour info — it must come across with the baker. - -### B.3 Library choice - -SkiaSharp is sufficient for every operation the bake needs — `SKCanvas.DrawBitmap` with `SKBlendMode.SrcOver`/`Multiply`/`DstIn` for layers, masks and skin grain, `SKColorFilter.CreateBlendMode` for tint, `SKBitmap.Resize` (bilinear or better, an improvement on the reference's nearest-neighbour) — and it is already loaded in the region process. The recommended shape is: **port `BakeLayer.cs` + `ManagedImage.cs` + `TGALoader.cs` (BSD-3, attribution header as done for PrimMesher in the web viewer) as the algorithm, replacing `ManagedImage` per-pixel loops with `SKBitmap` operations where they are the same operation**, decode/encode through `CoreJ2K` (`SkiaImageUtils.TryEncodeToJ2KLossless` exists; add a lossy preset at quality comparable to viewer uploads). Do *not* take a dependency on the LibreMetaverse NuGet inside the region: it would load a second copy of every type in the `LibreMetaverse` namespace next to `UtopiaSkye.OpenMetaverse` and the two `avatar_lad.xml` loaders would fight over `openmetaverse_data`. - -### B.4 Known fidelity gaps to plan for (the reference is a bot baker, not the viewer's `LLTexLayer`) - -1. **Resolution.** Reference bakes at 512 (viewer default for "medium/high" is also 512; 1024 is a viewer option). Start at 512. -2. **Resize.** Nearest-neighbour; use bilinear. The `FIXME: tile` case (texture smaller than the bake) is real for old content. -3. **Morph-driven masks.** The viewer evaluates alpha masks against the avatar's shape parameters; the reference applies `VisualAlphaParam` weights from the *wearable's* parameters only. Expect slight seam/length differences on gloves, sleeves, skirt length. -4. **Eleven-slot bakes.** The reference `BakeType` covers the six classic bakes; left-arm/left-leg/aux1–3 (BOM universal wearables) need adding — the layer rules for those are in the viewer's `avatar_lad.xml` (`bake` attributes) and are the same mechanism. -5. **Bakes-on-Mesh.** BOM does not change compositing — a BOM mesh body samples the *same* baked textures at the slots bit 63 already advertises. What BOM does change is *visibility of errors*: a mesh body shows the whole skin/tattoo/alpha bake, so gaps 1–3 are more visible than on the system body. The alpha-wearable layers (`LowerAlpha`…`HairAlpha`, skipped as colour layers at `:177-181` but applied as masks) matter most here. -6. **Materials/PBR.** Out of scope; bakes are diffuse only, as in SL. - -### B.5 Storage and serving - -- Bake output: J2K, stored through the asset service as `AssetType.Texture`, **not temporary**, creator = the avatar, name `Baked `, with a deterministic *cache key* recorded per avatar: `(avatarId, slot) → (textureId, cofVersion, inputHash)` where `inputHash` covers the wearable asset ids, colours and parameters that feed that slot. Re-baking a slot whose `inputHash` is unchanged is a no-op (the equivalent of the viewer's `AgentCachedTexture`). -- Serving: (1) the SL route `GET texture///` — a region-hosted HTTP handler that resolves `` through the asset service and answers `image/x-j2c` (Range requests welcome; the viewer's `FTT_SERVER_BAKE` uses the same fetcher as `GetTexture`); (2) `GetTexture` continues to serve the same asset id, which is what LibreMetaverse's normal texture pipeline and any Firestorm user in legacy mode will hit. `agent_appearance_service` should point at a grid-level URL that reverse-proxies to "the region the avatar is in", or, simpler for a single-host grid like this one, at a Robust handler that serves from the asset service directly — the asset is the same either way. Decision D-2. -- Old bakes: keep the last N per avatar/slot (viewers cache by texture id; a changed bake must have a new id) and let a sweeper delete assets that are no longer referenced by any `(avatarId, slot)` record. - -### B.6 Where it runs - -An `ISharedRegionModule` (`AppearanceBakeModule`) owning: the cap handler, a per-avatar bake queue (one bake job at a time per avatar, latest `cof_version` wins), the compositor, the cache-key table (SQLite/MySQL through the existing data layer; a region-local table is enough for milestone 1), and the bake URL route. A bake of six 512² slots from already-cached textures is tens of milliseconds of raster work plus J2K encode; the dominant cost is fetching wearable textures on first use. Concurrency limit per region (say 2 bakes in flight) protects the sim thread. - ---- - -## C. Wire changes, in one list - -| Area | Change | Site | -|---|---|---| -| Handshake | `RegionProtocols` bit 0 from a per-region flag, not a constant | `LLClientView.cs:995` | -| `AvatarAppearance` | emit `AppearanceData { AppearanceVersion, CofVersion }` (1 block): version 1 + COF version for server-baked avatars, version 0 otherwise | `LLClientView.cs:4532-4533`, callers `ScenePresence.cs:4338-4347` | -| Cap | `UpdateAvatarAppearance` (POST LLSD `cof_version` → `{ success, cof_version }` or `{ success:false, error, expected }`) | new module, pattern `UploadBakedTextureModule.cs:97-112` | -| Login | `agent_appearance_service` in the login response | `LLLoginResponse.cs:486-497` (+ `[LoginService]` config) | -| Bake route | `texture///` | new handler (region and/or Robust) | -| Visual params | param 11000 ("appearance version") set to 1 in the broadcast params for server-baked avatars (the viewer cross-checks it, `llvoavatar.cpp:10851-10863`) | `AvatarAppearance.SetVisualParams` consumers | -| Storage | bake assets persistent; cache-key table | new | -| Legacy path | `AgentSetAppearance` / `UploadBakedTexture` keep working (Firestorm legacy, bots); when bit 0 is set for a region, an incoming `AgentSetAppearance` from a viewer is accepted but the server bake wins for the broadcast | `AvatarFactoryModule.cs:168-215` | - ---- - -## D. Rollout hazards and the safe rollout - -**The hazard, precisely.** The moment a region's handshake carries bit 0, every viewer connecting to it (LL *and* Firestorm — `llagent.cpp:6369-6382` moves a legacy avatar to server bakes on entering a CBV>0 region) stops baking locally and asks the cap. If the cap or compositor fails, *every* avatar in that region is a cloud, including Firestorm users who were fine. There is no viewer-side fallback: `checkForUnsupportedServerBakeAppearance` only fires the other way (server-baked avatar entering a legacy region, `llvoavatarself.cpp:3636-3657`). - -**Gating design.** - -1. **Three switches, all default off.** `[Appearance] ServerSideBaking = false` (grid default in `OpenSimDefaults.ini`); per-region override in the region's own ini (`ServerSideBaking = true` under `[Appearance]` scoped by region name, using the existing `Region_` override convention); and a **runtime console command** `appearance ssb on|off` that flips the advertised bit for *new* handshakes without a restart (existing sessions keep their mode until they re-enter). The compositor module always loads; only the *advertisement* is gated. -2. **Bit 0 is advertised only when the module reports ready:** compositor self-test passed at startup (bake the default avatar from the library wearables and decode the result), the bake route answers a probe, and the login field is configured. If any check fails the region logs why and stays legacy even with the flag on. -3. **Dual-mode regions are legitimate.** With bit 0 clear the region still *accepts* `UpdateAvatarAppearance` requests (a viewer would not send them, but LibreMetaverse can be told to), which is how the web viewer and NPC re-baking can be exercised on a legacy region before any human-facing region flips. This is the key to testing on the live grid without touching Firestorm users. -4. **Fallback for a failed bake.** The cap answers `{ success:false, error:"…" }`; the viewer logs and retries with back-off (R4). The sim keeps broadcasting the *previous* good bake for that avatar (never the default cloud TE) and re-queues. If no bake has ever succeeded for an avatar, the broadcast falls back to the wearables' own textures where a slot has one (skin, eyes) — imperfect, visibly better than a cloud. -5. **Per-region enable order on the live grid:** (a) a new, empty test region (`SSB-Test`) with the flag on; verify with the web viewer, then an LL viewer, then Firestorm; (b) Elm or Transylvania off-peak with the console switch, watched, revert with the same command; (c) Ebony last; (d) grid default only after every region has run it. -6. **What to watch.** Per region: bake queue depth, bake failures per avatar, bake latency p95, bake-route 404s (a 404 means an `AvatarAppearance` referenced an id the store lost), and `AgentSetAppearance` arrivals on an SSB region (a viewer that has not switched). - -**How to test without breaking the live grid.** Milestone 1 (§F) runs entirely on a legacy-advertised region: the gateway asks the cap directly, the resulting `AvatarAppearance` (version 1) is broadcast — *and here is the one live-grid caveat*: Firestorm users in the same region would receive that version-1 appearance for the test avatar and fetch its bakes from the appearance service, which must therefore already be reachable. Do milestone 1 on the `SSB-Test` region only, or during a window with no other users on the region. - ---- - -## E. Dependencies and ordering - -- **Track-L order (login benefits → AgentProfile → AIS → SSB), as given in the brief.** No Track-L document was found in the trees on this machine (`Docs/feature/sl-parity-audit/` is not present in `/d/tranquillity-develop`, `/d/tranquillity-hypergrid` or the other checkouts; the closest house documents are `Docs/feature/trusted-hypergrid/` in `tranquillity-hypergrid` and `Docs/voice/`). The order is taken as accepted and recorded as ledger A-1. -- **AIS v3 is not a hard dependency.** The sim reads the COF and wearables through `IInventoryService` today (R2 step 4) and can read the COF folder's version the same way; AIS v3 is the *viewer's* HTTP inventory path. Without AIS the viewer still maintains its COF through legacy UDP inventory ops and the `FetchInventory2`/`FetchInventoryDescendents2` caps (both registered, R1), and the COF folder version still increments on the inventory service. What AIS buys SSB is *tighter agreement on `cof_version`* (the viewer comments in `llvoavatar.cpp:10936-10937` say the canonical COF version is "maintained by the AIS code"); without it expect more `expected`-mismatch retries after fast outfit changes. SSB can proceed independently; the retry loop is the viewer's own mitigation. -- **Login benefits / AgentProfile** have no code coupling to SSB other than the login response being touched for `agent_appearance_service` (one field; coordinate the edit). -- **Not built yet and needed:** the compositor (R6), the cap, the bake route, the login field, per-region flag, `AppearanceData` emission, bake persistence policy, a cache-key store. **Present and reusable:** J2K encode/decode, SkiaSharp, wearable parser, `avatar_lad.xml` + masks on disk, `XBakes` (optional persistent store), `GetTexture`, inventory reads. -- **Bots/NPC track** benefits (R10) and should consume the same bake service rather than its own path. - ---- - -## F. Build-plan skeleton (sizing only; not a BP) - -| # | Milestone | Content | Done when | -|---|---|---|---| -| 0 | Hygiene (independent, ship first) | emit `AppearanceData { 0, cof }` explicitly; per-region `RegionProtocols` from config (still 0); `agent_appearance_service` field plumbing behind a flag | LL viewer no longer logs "AgentAppearanceServiceURL not set" for legacy avatars; nothing else changes for Firestorm | -| 1 | **First demoable: one avatar dressed in both viewers** | port compositor (six classic bakes, 512²) into `AppearanceBakeModule`; `UpdateAvatarAppearance` cap; bakes stored persistent; bake route on the region; version-1 broadcast for baked avatars; console switch; `SSB-Test` region flagged | Truly logs in on `SSB-Test` with a system-body outfit: a stock LL viewer shows her dressed (no cloud, no orange), and the web viewer's gateway (LibreMetaverse) fetches the same six bakes and renders them on its avatar. Evidence: cap request/response log, six bake assets, bake-route 200s from both clients, viewer log free of bake fetch failures | -| 2 | Fidelity + BOM slots | eleven-slot bakes (left arm/leg, aux1–3), bilinear resize, tiling, alpha-wearable correctness on a BOM mesh body; side-by-side comparison against Firestorm's own bake of the same outfit | a BOM mesh body looks the same to within an agreed visual tolerance in LL and Firestorm-legacy | -| 3 | Robustness | failed-bake fallback, previous-good retention, `expected` COF handling, queue limits, sweeper, metrics, `appearance ssb` console surface | soak on `SSB-Test` with outfit churn; no cloud regressions | -| 4 | Persistence + grid | Robust-side bake route (or reverse proxy), `XBakes`/asset-service policy, NPC re-bake on rez, HG considerations (foreign avatars carry their own bakes; foreign regions may be legacy) | an avatar TPs between an SSB and a legacy region and back without clouding in either | -| 5 | Rollout | per-region enable per §D.5; Firestorm users observed; grid default flip | all regions SSB; `AgentSetAppearance` arrivals ≈ 0 | - -Dependencies inside the skeleton: 0 → 1 → 2/3 (parallel) → 4 → 5. Milestone 1 is the one to size first; its unknown is the compositor port, everything else is protocol plumbing with the viewer source as the spec. - ---- - -## G. Risk register - -| ID | Risk | Likelihood | Impact | Mitigation | -|---|---|---|---|---| -| RK-1 | Advertising bit 0 before the compositor is reliable clouds every avatar in the region, Firestorm included | high if unmanaged | high | §D gating: default off, readiness checks, console switch, test region first | -| RK-2 | Compositor fidelity below user expectation on BOM mesh bodies | medium | medium | milestone 2 comparison suite; keep per-region legacy as the fallback | -| RK-3 | `cof_version` drift without AIS causes retry storms after rapid outfit changes | medium | low–medium | honour `expected`; coalesce bake jobs per avatar; measure on `SSB-Test` | -| RK-4 | Bake assets bloat the asset store (a new id per rebake) | medium | medium | cache-key no-op rebakes; retention of last N; sweeper | -| RK-5 | Two libomv copies in one process (if LibreMetaverse were referenced for its Baker) | certain if done | medium | port the BSD code instead (§B.3) | -| RK-6 | Bake route reachability for foreign (HG) visitors and for the web gateway's host | medium | medium | grid-level `agent_appearance_service`; same asset served by `GetTexture` | -| RK-7 | Wearable assets missing/undecodable for old outfits | medium | low | per-slot fallback to base layers; log and continue | -| RK-8 | Login-response edit collides with the login-benefits work in Track-L | low | low | one field, coordinate | diff --git a/Docs/feature/ssb-appearance/S0a-VERIFICATION.md b/Docs/feature/ssb-appearance/S0a-VERIFICATION.md deleted file mode 100644 index c8a6042fc73..00000000000 --- a/Docs/feature/ssb-appearance/S0a-VERIFICATION.md +++ /dev/null @@ -1,76 +0,0 @@ -# S0a — Verification pass - -Tree: `D:\tranq-ssb`, branch `feature/ssb-appearance`, HEAD `0f441a03df` (on deployed `db7c746248`). -Upstream comparison target: `upstream/develop` = `ee71b6951b`; merge-base with HEAD = `93765a999e`. -Method: rg/grep to locate, then the cited site was opened and read. Line numbers are at HEAD. - -Path correction versus the session brief: the UDP client stack lives at -`Source/OpenSim.Region.ClientStack.LindenUDP/` (no dot between `Linden` and `UDP`). -All LLClientView citations below use that path. - -## Summary table - -| Item | Verdict | file:line | Reasoning | -|---|---|---|---| -| V1 | **Yes, still count 0**; hover written as 1 block `(0, 0, hover)` | `Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs:4499` (method), `:4531-4532` (`// no AppearanceData` / `data[pos++] = 0;`), `:4533-4537` (AppearanceHover) | `SendAppearance` hand-packs the packet; the AppearanceData block count byte is a literal 0, and AppearanceHover is a literal count 1 followed by `Utils.FloatToBytesSafepos(0/0/hover)`. Line moved from 4521 to 4531 since 645b0f3. | -| V2 | **`1UL << 63` only**, bit 0 clear | `LLClientView.cs:995` inside `SendRegionHandshake()` (`:886`) | Exact expression: `zc.AddUInt64(1UL << 63);` preceded by the comments `// bit 0 signals server side texture baking` / `// bit 63 signals more than 6 baked textures support"`. No other write to RegionProtocols in the file. | -| V3 | **No hits** | — | `grep -rn 'UpdateAvatarAppearance\|agent_appearance_service'` over `Source/` and `Addons/` (`*.cs`, `*.ini*`, `*.csproj`, `*.xml`, excluding `bin/`, `obj/`, `AISv3`) returns nothing. Neither the cap nor the login-response key exists anywhere in the tree. | -| V4 | **Yes, both sides present (stock XBakes)**; stores a per-agent XML blob of `WearableCacheItem[]` including the full baked `AssetBase` | Region: `Source/OpenSim.Region.CoreModules/Avatar/BakedTextures/XBakesModule.cs:44` (`INonSharedRegionModule, IBakedTextureModule`), `:57` (config section `[XBakes]`, key `URL`), `:184-231` (serialises `{AssetBase}…`, indices >26 as ``), `:238-241` (POST `bakes/`). Robust: `Source/OpenSim.Server.Handlers/BakedTextures/XBakes.cs:37` (`ServiceBase, IBakedTextureService`), `:56` (`BaseDirectory`), `:82-90` (`Store` writes the raw POST body to `/aa/bb/cc/`), `:66-73` (`Get` reads it back). Connector: `Source/OpenSim.Server.Handlers/BakedTextures/XBakesHandler.cs:38` (`XBakesConnector`), `:62` (`/bakes` handler). Interface: `Source/OpenSim.Services.Interfaces/IBakedTextureService.cs`. Config: `Source/OpenSim.Server.GridServer/AppData/Robust.ini.example:130` (ServiceList entry, commented out by default), `:662-665` (`[BakedTextureService]` section). | Region module is a thin REST client; the Robust service is an opaque file store keyed by agent id. It stores the viewer-uploaded bakes (the whole `AssetBase`, base64 in XML), not wearables or params. It is a cache of client-side bakes, not a bake producer, so it does not conflict with ADR-004's asset-service persistence; it is a separate, optional, off-by-default path. | -| V5 | **Wipe pattern present — NOT fixed.** Starts from an empty wearable array and fills only what the viewer sent. **Hard gate open.** | `Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs:1205` — `AvatarAppearance avatAppearance = new AvatarAppearance(sp.Appearance, false);` → `Source/OpenSim.Framework/AvatarAppearance.cs:187` (`copyWearables=false`) → `:192` ctor → `:213-225` (`if (copyWearables …) {copy} else ClearWearables();`) → `:274-276` (`m_wearables = new AvatarWearable[LEGACY_VERSION_MAX_WEARABLES]`, all empty). Then `AvatarFactoryModule.cs:1207-1219` adds only `e.NowWearing`, `:1221` `GetAssetsFrom(sp.Appearance)` resolves asset ids only for items present, `:1228` `sp.Appearance.Wearables = avatAppearance.Wearables;` replaces the whole array, `:1231` `QueueAppearanceSave`. | Any `AgentIsNowWearing` that lists fewer than all worn types clears the unlisted slots and persists the result. This is stock OpenSim behaviour, and the Legion fix has not been ported to Tranquillity. Must be fixed before any server-initiated bake reads stored wearables. | -| V6 | **Yes on all three paths.** Service layer never increments; the data layer does, on every item store, delete and move and on folder store/move. Link create, item delete, item move and cap folder create all bump the parent folder's `version`. | Service: `Source/OpenSim.Services.InventoryService/XInventoryService.cs` has **no** increment site — only `:183` (`version = 1` on system-folder creation) and `:410-445` (`UpdateFolder` monotonic guard that refuses a lower version on typed folders). Data layer (MySQL; PGSQL and SQLite mirror it): `Source/OpenSim.Data.MySQL/MySQLXInventoryData.cs:239-244` (`Store(XInventoryItem)` → `IncrementFolderVersion(item.parentFolderID)`), `:148-162` (`Delete(string[],string[])` → increment each distinct parent), `:133-137` (`Delete(string,string)` delegates to the generic `:341-344`, which virtual-dispatches back to `:148`), `:167-188` (`MoveItem` → old and new parent), `:283-288` (`Store(XInventoryFolder)` → parent), `:258-278` (`MoveFolder` → both parents), `:303-316` (`update inventoryfolders set version=version+1`). PGSQL: `Source/OpenSim.Data.PGSQL/PGSQLXInventoryData.cs:158-159,232,276-277,287`. SQLite: `Source/OpenSim.Data.SQLite/SQLiteXInventoryData.cs:143,177,200-201,258,282-283`. | See traces below. | -| V7 | **CoreJ2K.Skia 2.3.3.91** (plain NuGet package, `nuget.org`), API `J2kImage.ToBytes(SKBitmap, J2KEncoderConfiguration)` / `SKBitmap.EncodeToJ2K(cfg)`. No OpenJPEG/CSJ2K code in the tree. | Encode sites: `Source/OpenSim.Framework/SkiaImageUtils.cs:27` (config `new J2KEncoderConfiguration().WithLossless().WithFileFormat(true)`), `:52` (`workingImage.EncodeToJ2K(encoderConfiguration)`); `Source/OpenSim.Region.CoreModules/World/LegacyMap/MapImageModule.cs:166` (`J2kImage.ToBytes(skBitmap, encoderConfig)`); `Source/OpenSim.Region.CoreModules/Scripting/VectorRender/VectorRenderModule.cs:410`. Decode: `Source/OpenSim.Region.CoreModules/Agent/TextureSender/J2KDecoderModule.cs:178`, `Source/OpenSim.Capabilities.Handlers/GetTexture/GetTextureRobustHandler.cs:324`. Package refs (no CPM, per-csproj): `Source/OpenSim.Framework/OpenSim.Framework.csproj:20`, `OpenSim.Capabilities.Handlers.csproj:27`, `OpenSim.Region.CoreModules.csproj:42`, `OpenSim.Region.Framework.csproj:31`, `OpenSim.Region.PhysicsModules.Meshing.csproj:11`, `OpenSim.Region.PhysicsModules.ubODEMeshing.csproj:24`, `OpenSim.Services.Connectors.csproj:28`, `OpenSim.Services.MapImageService.csproj:24`, `Warp3D.csproj:12`, all `Version="2.3.3.91"`. The only "OpenJPEG" string is a comment at `GetTextureRobustHandler.cs:329`. NuGet cache holds `corej2k.skia/2.3.3.91` and `2.1.2.38`. | A new library under `Source/` can `PackageReference` CoreJ2K.Skia directly with no region dependency; the plumbing in `OpenSim.Framework/SkiaImageUtils.cs` is a convenience, not a requirement. Note from V10: upstream #201 moved map tiles from fixed 256×256 J2K tiles to single-tile (`WithTiles(t => t.SetSize(width, height))`) because multi-tile output rendered blank in viewers; bake output should use the same single-tile setting. | -| V8 | **SkiaSharp 4.151.1**, per-csproj `PackageReference` (no `Directory.Packages.props`, so no central package management). Native assets only in the two host projects. | `Source/OpenSim.Framework/OpenSim.Framework.csproj:19`, `OpenSim.Capabilities.Handlers.csproj:26`, `OpenSim.Region.CoreModules.csproj:43`, `OpenSim.Region.Framework.csproj:30`, `OpenSim.Region.PhysicsModules.Meshing.csproj:12`, `OpenSim.Region.PhysicsModules.ubODEMeshing.csproj:25`, `OpenSim.Services.Connectors.csproj:27`, `OpenSim.Services.MapImageService.csproj:23`, `Warp3D.csproj:10`, `OpenSim.Server.RegionServer.csproj:86-88` and `OpenSim.Server.GridServer.csproj:62-64` (the latter two also `SkiaSharp.NativeAssets.Win32` / `.Linux` 4.151.1). NuGet cache also has 3.119.x and 4.150.1 from earlier builds. | Version is uniform at 4.151.1 across the tree; the library pins the same. | -| V9 | Pattern: `[ServiceList]` ini entry → `GridServiceConnectorLoader` → `ServerUtils.LoadPlugin(dll:Class, {config, server, configName})` → connector ctor loads the service via `LocalServiceModule`, builds `ServiceAuth`, calls `server.AddSimpleStreamHandler(new XHandler(...))`; the handler subclasses `SimpleStreamHandler("/path", auth)` and overrides `ProcessRequest`. | Loader: `Source/OpenSim.Server.GridServer/GridServiceConnectorLoader.cs:47-61` (reads `[Startup] ServiceConnectors` plus every key in `[ServiceList]`), `:64-90` (parses `[configName@]port/Dll.dll:Class`), `:110-118` (`ServerUtils.LoadPlugin(conn, new object[]{config, server, configName})`, falls back to `{config, server}`). LoadPlugin: `Source/OpenSim.Server.Base/ServerUtils.cs:186,234`. Base class: `Source/OpenSim.Server.Handlers/Base/ServerConnector.cs:38` (`ServiceConnector : IServiceConnector`). Worked example: `Source/OpenSim.Server.Handlers/BakedTextures/XBakesHandler.cs:38-63` (connector), `:66-77` (`BakesServerHandler : SimpleStreamHandler`, base `("/bakes", auth)`, `ProcessRequest(IOSHttpRequest, IOSHttpResponse)`), `:97-124` (GET/POST, `SplitParams(httpRequest.UriPath)`). Ini: `Source/OpenSim.Server.GridServer/AppData/Robust.ini.example:130` (`BakedTextureService = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:XBakesConnector"`) and `:662-665`. | S4 copies `XBakesConnector` + `BakesServerHandler` verbatim in shape: one connector class per service, one `SimpleStreamHandler` per URL prefix, `ServiceAuth.Create(config, sectionName)` for auth, and a `[ServiceList]` line plus a `[SectionName]` block with `LocalServiceModule`. | -| V10 | See "Upstream delta" below. libopenmetaverse: HEAD `1.1.6-ga897ffefd7`, upstream `1.1.7`. | `Directory.Build.props:13-17` (HEAD) vs `upstream/develop:Directory.Build.props:13-17`. | Only 7 of the ~25 files/dirs in scope differ; none of the appearance, inventory-version or AvatarFactory files differ at all. | - -## V6 — path traces - -**UDP `LinkInventoryItem` (link create):** -`LLClientView.cs:8473` (dispatch table) → `:10294` `HandleLinkInventoryItem` → `:10297` `OnLinkInventoryItem` → -`Source/OpenSim.Region.Framework/Scenes/Scene.cs:3460` (`client.OnLinkInventoryItem += HandleLinkInventoryItem`) → -`Source/OpenSim.Region.Framework/Scenes/Scene.Inventory.cs:1224` `HandleLinkInventoryItem` → `:1257-1262` `CreateNewInventoryItem(...)` → `:1195` `AddInventoryItem(item)` → `:123-125` `InventoryService.AddItem(item)` → -`XInventoryService.cs:507-512` `AddItem` → `m_Database.StoreItem(...)` → -`MySQLXInventoryData.cs:239-244` `Store(XInventoryItem)` → `IncrementFolderVersion(item.parentFolderID)`. -**Parent folder version is bumped on link create.** - -**UDP `RemoveInventoryItem` (item delete, including link delete):** -`LLClientView.cs:8481` → `:10412` `HandleRemoveInventoryItem` → `:10423` `OnRemoveInventoryItem` → -`Scene.cs:3471` → `Scene.Inventory.cs:1275` `RemoveInventoryItem` → `:1282` `InventoryService.DeleteItems(...)` → -`XInventoryService.cs:579-607` `DeleteItems` (both the `m_AllowDelete=false` link-only branch `:589-595` and the plain branch `:604` call `m_Database.DeleteItems`) → -`MySQLXInventoryData.cs:133` `Delete(string,string)` → `base.Delete` = `MySQLGenericTableHandler.cs:341-344` → virtual `Delete(string[],string[])` = `MySQLXInventoryData.cs:148-162` → `IncrementFolderVersion` for every distinct parent of the deleted rows. -**Parent folder version is bumped on item delete.** - -**UDP `MoveInventoryItem`:** -`LLClientView.cs:8480` → `:10390` → `:10409` `OnMoveInventoryItem` → `Scene.cs:3470` → `Scene.Inventory.cs:1119` `MoveInventoryItem` → `:1125` `InventoryService.MoveItems` → `XInventoryService.cs:567-573` → `m_Database.MoveItem` → `MySQLXInventoryData.cs:167-188` → `IncrementFolderVersion(oldParent)` and `(newParent)`. -**Both old and new parent versions are bumped on move.** - -**`CreateInventoryCategory` cap:** -`Source/OpenSim.Region.ClientStack.LindenCaps/BunchOfCaps/BunchOfCaps.cs:264-265` (registration) → `:1138` handler → `:1200` `new InventoryFolderBase(folderID, folderName, m_AgentID, (short)folderType, parentID, 1)` → `:1201` `m_Scene.InventoryService.AddFolder(folder)` → `XInventoryService.cs:369-407` `AddFolder` → `m_Database.StoreFolder` → `MySQLXInventoryData.cs:283-288` `Store(XInventoryFolder)` → `IncrementFolderVersion(folder.parentFolderID)`. -**Parent folder version is bumped on folder create; the new folder itself starts at version 1.** - -Caveat for ADR-006: the bump is a DB-side `version+1` with no read-back, and `XInventoryService.UpdateFolder` (`:423-426`) refuses a lower-than-stored version on typed folders (which includes COF, `FolderType.CurrentOutfit`), so a stale cached `InventoryFolderBase` written back by a region will not regress the counter. The sim must read `Version` fresh via `GetFolder`, not from a cached object. - -## V10 — Upstream delta (`HEAD..upstream/develop`, 22 commits upstream, 144 commits HEAD-only) - -`git diff --stat HEAD..upstream/develop --` over: `LLClientView.cs`, `Avatar/AvatarFactory/`, `OpenSim.Services.InventoryService/`, `OpenSim.Framework/SkiaImageUtils.cs`, `OpenSim.Framework/AvatarAppearance.cs`, `Scenes/Scene.Inventory.cs`, `OpenSim.Data.MySQL/MySQLXInventoryData.cs`, `World/LegacyMap/MapImageModule.cs`, `Capabilities.Handlers/GetTexture/`, `Directory.Build.props`, and the eleven csprojs cited in V7/V8. - -| File | What changed | -|---|---| -| `Directory.Build.props` | libopenmetaverse packages `1.1.6-ga897ffefd7` → `1.1.7` (upstream `58424056dd`). No SkiaSharp/CoreJ2K entries here on either side. | -| `Source/OpenSim.Region.ClientStack.LindenUDP/LLClientView.cs` | +15 lines: new overload `SendAlertMessage(string message, string info, byte[] extra)` at upstream `:3149`. **No change** to `SendAppearance`, `SendRegionHandshake`, or any appearance/cached-texture handler. | -| `Source/OpenSim.Capabilities.Handlers/GetTexture/GetTextureHandler.cs` | J2K area: decode changed from `j2k.As()` (+ `SKBitmap.FromImage`) to `j2k.As()`; comment: "CoreJ2K.Skia only registers an image creator for SKBitmap, not SKImage." Relevant to the library's decode path. | -| `Source/OpenSim.Capabilities.Handlers/GetTexture/GetTextureRobustHandler.cs` | Same `As` fix, then `SKImage.FromBitmap` for re-encode. | -| `Source/OpenSim.Region.CoreModules/World/LegacyMap/MapImageModule.cs` | J2K area (upstream `ae089ab7a7`/`301fc1421d`, #201): encoder config becomes `BuildEncoderConfig(w, h)` with `WithTiles(t => t.SetSize(w, h))` (single tile) instead of fixed 256×256 tiles; map tile sized to the region; terrain renderer selection simplified. Diverges both ways: HEAD carries `db7c746248` (legacy terrain restore) that upstream lacks. | -| `Source/OpenSim.Server.GridServer/OpenSim.Server.GridServer.csproj` | Removes `OpenSim.Services.MembershipService` ProjectReference. SkiaSharp lines unchanged. | -| `Source/OpenSim.Server.RegionServer/OpenSim.Server.RegionServer.csproj` | Adds Addons ProjectReferences (Gloebit, Groups, OfflineIM, Mutelist, Search) for publish; removes MembershipService and LegionJolt references. SkiaSharp lines unchanged. | -| `Source/Warp3D/Warp3D.csproj` | Adds `PackageReference OpenMetaverse.Rendering.Meshmerizer 1.0.6` (the non-UtopiaSkye package) alongside the global UtopiaSkye one — likely an upstream slip, not J2K/Skia related. | -| `Avatar/AvatarFactory/**`, `OpenSim.Services.InventoryService/**`, `AvatarAppearance.cs`, `Scene.Inventory.cs`, `MySQLXInventoryData.cs`, `SkiaImageUtils.cs`, `OpenSim.Framework.csproj`, `OpenSim.Region.CoreModules.csproj`, `OpenSim.Region.Framework.csproj`, `OpenSim.Capabilities.Handlers.csproj`, `OpenSim.Services.Connectors.csproj`, `OpenSim.Services.MapImageService.csproj`, `PhysicsModules.Meshing.csproj`, `PhysicsModules.ubODEMeshing.csproj` | **Identical** on both sides. | - -"#198" is not among the 22 upstream commits in this range, so no file in scope is logging-only from it. The only logging commit upstream (`ee71b6951b`) touches none of the files in scope. - -## Notes carried forward for S0b+ - -- `Directory.Build.props:3-5` sets `net10.0`, `ImplicitUsings=enable`, `Nullable=disable` tree-wide and injects all five `UtopiaSkye.OpenMetaverse*` packages into every project (`:13-17`). A new library must override `Nullable` locally and `Remove` the four non-Types packages if it wants only `OpenMetaverse.UUID`. `OpenMetaverse.UUID` is provided by `UtopiaSkye.OpenMetaverse.Types` (`lib/net10.0/OpenMetaverse.Types.dll`). -- Test framework in the tree is mixed: 19 test csprojs use NUnit, 7 use xunit (`xunit 2.9.3`, `xunit.runner.visualstudio 3.1.5`, `Microsoft.NET.Test.Sdk 18.8.1`, e.g. `Tests/OpenSim.Framework.PluginMigration.Tests/*.csproj`). The newer projects are xunit; the brief asks for xunit. -- Solution: `Tranquillity.sln` (94 projects, Tests in solution folder `{7F5EC7C6-FD99-4084-8530-5D3F28D6D7C0}`). -- `F:\viewer-develop` is not a git checkout (no `.git`); `indra/newview/VIEWER_VERSION.txt` reads `26.1.1`, and `avatar_lad.xml` is 354,436 bytes, `wearable_definition_version="22"`, 11 `` elements. The addendum §3 names the viewer commit as `62033f2`; that could not be confirmed from the directory. diff --git a/Docs/feature/ssb-appearance/S1b-FIDELITY.md b/Docs/feature/ssb-appearance/S1b-FIDELITY.md deleted file mode 100644 index 59137ece063..00000000000 --- a/Docs/feature/ssb-appearance/S1b-FIDELITY.md +++ /dev/null @@ -1,215 +0,0 @@ -# S1b — The fidelity surface, measured against a second outfit - -**Date:** 2026-09-03 **Branch:** `feature/ssb-appearance` **Closes/updates:** Ledger Q-11 -**Sets:** `Source/OpenSimNGC.Appearance.Baking.Tests/Golden/truly-stock`, `.../aleric-max` - -S1 shipped on one outfit. Ledger Q-11 recorded that this proved nothing about the layers that outfit never -touched. This session added a second reference set — Aleric Fenwood, Ebony, captured 2026-09-03 — and ran the -whole layer surface against it. - -**Result in one line: one real defect, in the upper channel's morph mask, and it is a parity gap the stock -outfit could not have shown.** No layer returned UNSUPPORTED on either outfit. - ---- - -## 1. What Aleric actually wears - -From `aleric-max/fixtures/avatar.json` (the live `Avatars` rows), 10 worn slots: - -| Type | Wearable | Index | Asset id | Parsed name | -|---|---|---|---|---| -| 0 | Shape | 0 | `8dca9b1a-9aeb-6580-c846-126d1cb5a280` | New Shape (82 params) | -| 1 | Skin | 0 | `045d0bb6-e055-6990-99e0-814936d03f9e` | Adam Skin (shaved) — 26 params, 3 textures | -| 2 | Hair | 0 | `e38631ab-a09a-3837-7965-59a485683402` | Professional Male 1-Hair base | -| 3 | Eyes | 0 | `cc5860c4-5651-0528-83ad-9dd95b25e9ee` | New Eyes | -| 4 | **Shirt** | 0 | `00000000-0000-0000-0000-000000000000` | **null asset id — slot worn but empty** | -| 5 | Pants | 0 | `5cc5ebc5-095e-328f-0435-13d2e0860531` | Professional Male 1-Pants | -| 6 | Shoes | 0 | `196eab6e-eae7-ca77-a752-0e97a2fcb7f3` | shoe base | -| 7 | **Socks** | 0 | `28cffb2a-557f-0cd8-5dee-089a915728ff` | mens black socks | -| 8 | **Jacket** | 0 | `ba106f90-f560-0ac3-868b-37088090b3a8` | Black Suede Blazer with blue shirt | -| 14 | **Tattoo** | 0 | `40294285-0ac8-3053-a459-acc66cc2101c` | Professional Male 1-Hair base | - -253 visual-param bytes. 11 distinct textures referenced and fetched. - -The Shirt row is worth its own line: the slot is **worn but carries the null asset id**. The orchestrator skips -it (`BakeOrchestrator.Resolve`, `assetId.IsZero()`), the harness skips it, and the compositor therefore reports -`upper_clothes skipped: no Shirt worn`. Section 4 argues this row may be the whole finding. - -## 2. Q-11 coverage: what this outfit reached and what it did not - -| Q-11 surface | Covered? | Evidence | -|---|---|---| -| **Tattoo** | **YES** | `head_tattoo drawn: Tattoo 40294285: HeadTattoo 512x512+alpha -> 1024`. `upper_tattoo` / `lower_tattoo` skipped: this tattoo wearable carries only a head texture. | -| **Socks** | **YES** | `lower_socks drawn: Socks 28cffb2a: LowerSocks 32x32 -> 1024; masks [Socks Length bump#1050=0.35+]` | -| **Jacket** | **YES** | `upper_jacket drawn` (5 param masks + texture alpha) and `lower_jacket drawn` (3) | -| Multi-wearables per slot | no | Every worn slot is index 0; no slot holds two wearables. The `LLTexLayerTemplate` multi-instance path (S0e) is still exercised only by the unit tests. | -| Universal | no | 6 layers report `no Universal worn` (`head_/upper_/lower_/eyes_/hair_universal_tattoo`, `*_tattoo` on eyes and hair). | -| Alpha | no | 5 layers report `no Alpha worn` (`head/upper/lower/eyes/hair alpha`). The `IMG_INVISIBLE` whole-region path is untouched. | -| Gloves | no | `upper_gloves skipped: no Gloves worn` | -| Skirt | no | No Skirt worn; the Skirt channel is not produced at all. | - -**Three of the eight covered; five remain untested against live content.** The five BoM aux channels -(`leftarm`, `leftleg`, `aux1`, `aux2`, `aux3`) are also unexercised — they need a Universal wearable, which is -the same gap. - -Q-11 therefore **narrows but does not close.** What it did buy: the first evidence that the compositor's -behaviour on a non-stock outfit is sound in RGB and alpha on every channel, and the discovery in §4. - -## 3. The layer surface, all 11 channels - -93 layers are declared across the 11 layer sets in `avatar_lad.xml`. 22 of them are `render_pass="bump"` -layers, which **the LL compositor never renders into a bake** (`lltexlayer.cpp:395`, S0d): they are listed -below as *not rendered by design*, not as gaps. - -| Channel | Layers | Produced for Aleric? | Drawn | Skipped | Bump (by design) | UNSUPPORTED | -|---|---|---|---|---|---|---| -| head | 25 | yes | 5 | 15 | 5 | **0** | -| upper_body | 23 | yes | 4 | 11 | 8 | **0** | -| lower_body | 24 | yes | 7 | 8 | 9 | **0** | -| eyes | 4 | yes | 2 | 2 | 0 | **0** | -| hair | 4 | yes | 2 | 2 | 0 | **0** | -| skirt | 3 | **no — no Skirt worn** | — | — | — | untested | -| leftarm | 2 | **no — no Universal worn** | — | — | — | untested | -| leftleg | 2 | **no — no Universal worn** | — | — | — | untested | -| aux1 | 2 | **no — no Universal worn** | — | — | — | untested | -| aux2 | 2 | **no — no Universal worn** | — | — | — | untested | -| aux3 | 2 | **no — no Universal worn** | — | — | — | untested | - -`unsupportedLayers=[]` on every produced channel, for both outfits. The compositor did not meet a mask file it -lacks, a resource it lacks, or a `local_texture` it does not know. - -### Why each layer was skipped (the five produced channels) - -Every skip falls into one of five reasons, none of which is a gap in the compositor: - -| Reason | Count | Example | -|---|---|---| -| `no worn` | 13 | `upper_gloves skipped: no Gloves worn` | -| `every mask parameter skipped` | 12 | `freckles skipped: every mask parameter skipped [Freckles#165=skip]` | -| `colour alpha 0` | 7 | `shadow skipped: colour alpha 0 (0.00,0.00,0.00,0.00)` | -| `no texture on the wearable` | 2 | `upper_tattoo skipped: Tattoo 40294285: no UpperTattoo texture on the wearable` | -| `no Shirt worn` (null-asset slot) | 3 | `upper_clothes skipped: no Shirt worn` — see §4 | - -## 4. THE FINDING — upper channel morph mask, `upper_clothes` - -**Channel:** `upper` (upper_body). **Layer:** `upper_clothes`, in its morph-mask role. -**Numbers, at all three bake sizes** (so this is structural, not a resampling artefact): - -| Size | mean abs dM | pixels dM > 8 | threshold | -|---|---|---|---| -| 512 | **82.25** | **33.27%** | 4.0 / 5% | -| 1024 | **82.24** | **33.21%** | 4.0 / 5% | -| 2048 | **82.25** | **33.22%** | 4.0 / 5% | - -The reference upper morph mask is a real mask — 31.4% of pixels near 0, 67.0% near 255. Ours is **uniform 255**: -we produce no upper morph mask at all. The compositor says why: - -``` -upper_clothes morph: no Shirt worn: mask left at 255 -``` - -**The machinery is not at fault.** On the same avatar, the lower channel's morph mask reproduces the reference -*exactly* — identical histograms, 29.3% near 0 and 69.2% near 255 in both, mean abs difference 0.47: - -``` -lower_pants morph: Pants 5cc5ebc5: morph mask *= layer mask (mean 178.3) -``` - -So the S0d/S0e gather is right where a declared morph-mask layer is worn. What is wrong is that in this outfit -the upper morph mask should not have been empty. `avatar_lad.xml`'s `` block names exactly one -upper_body layer, `upper_clothes` (for `Displace_Loose_Upperbody` and `Shirtsleeve_flair`), and that layer's -`local_texture` is the **Shirt** slot. Aleric wears no shirt asset — yet the LL viewer still produced a mask. - -**Two candidate causes; the evidence does not yet separate them.** - -1. *The gather should include `upper_jacket`.* The jacket is the only upper clothing worn, and it carries five - param masks (`jacket Sleeve Length#1020`, `jacket Collar Front#1022`, `Collar Back#1024`, - `bottom length upper#620`, `open upper#622`) that would produce a mask of roughly the observed shape. - *Against it:* if the gather simply took every alpha-param layer, the lower channel would pick up - `lower_socks` (`Socks Length bump#1050=0.35`) and `lower_shoes` (`Shoe Height#1052=0.10`) too, and our - pants-only result would then differ from the reference. It does not — it matches exactly. So the viewer's - gather is not "every layer", and any fix must explain that asymmetry. -2. *The null-asset Shirt row should still render `upper_clothes`.* Aleric's Shirt slot **is** worn, with the - null asset id. If the viewer treats that as a present-but-blank shirt wearable, it would run the - `upper_clothes` layer with the shirt's own param masks and produce a mask, while we skip the slot entirely. - This explains the asymmetry with the lower channel for free. - -Ruled out by measurement: the mask is **not** the jacket's texture alpha (that texture is 99.3% opaque, and -mean |refM − jacketAlpha| = 80.71), and **not** the bake's own alpha channel (mean |refM − refA| = 81.26). - -**Impact.** The 5th component drives the viewer's *clothing morphs* — how the mesh body is displaced to sit -under loose clothing. A missing upper morph mask means an avatar wearing a jacket and no shirt would not get -the loose-upper-body displacement it gets on SL. It does not affect the visible texture: RGB and alpha for the -same channel are well inside threshold (mean abs RGB 0.78, 0.22% of pixels over 8, at 1024). - -**Discriminating experiment** (not run here — it needs a code change and viewer-source access, neither in this -session's scope): re-bake Aleric's upper with (a) `upper_jacket` added to the gather and (b) a synthetic blank -Shirt wearable present, and see which reproduces the reference's 31.4%/67.0% histogram. - -## 5. Everything else passes, on both outfits - -At each set's manifest size, with S1's thresholds (mean abs RGB ≤ 4.0, pixels over 8 ≤ 5%, mean abs alpha ≤ 2.0, -mean abs morph ≤ 4.0): - -| Set | Channel | meanAbsRGB | meanAbsA | pctRGB>8 | meanAbsM | verdict | -|---|---|---|---|---|---|---| -| truly-stock @512 | head | 1.37 | 0.64 | 1.35% | 0.00 | pass | -| | upper | 1.41 | 1.00 | 0.64% | 1.00 | pass | -| | lower | 1.36 | 1.00 | 0.63% | 1.00 | pass | -| | eyes | 0.36 | 1.00 | 0.00% | 1.00 | pass | -| | hair | 0.00 | 0.00 | 0.00% | 1.00 | pass (RGB skipped: reference alpha all zero) | -| aleric-max @1024 | head | 0.80 | 0.84 | 0.00% | 0.00 | pass | -| | upper | 0.78 | 1.00 | 0.22% | **82.24** | **FAIL on morph mask** (§4) | -| | lower | 0.89 | 1.00 | 0.04% | 0.47 | pass | -| | eyes | 0.45 | 1.00 | 0.48% | 1.00 | pass | -| | hair | 0.33 | 0.53 | 0.00% | 1.00 | pass | - -The richer outfit is, if anything, **closer** in RGB than the stock one (0.78–0.89 against 1.36–1.41). Nothing -about jacket, socks or tattoo compositing degrades the visible bake. - -## 6. Bake size (S1b Part 2) — evidence for ADR-008 - -References as captured: head, upper, lower and hair are **2048×2048**; **eyes is 512×512** on both avatars. - -Fidelity against bake size (mean abs RGB; both images resampled to the compared size): - -| Set | Channel | 512 | 1024 | 2048 | -|---|---|---|---|---| -| truly-stock | head | 1.37 | 0.96 | 0.97 | -| | upper | 1.41 | 1.25 | 1.26 | -| | lower | 1.36 | 1.11 | 1.19 | -| | eyes | 0.36 | 0.32 | 0.33 | -| aleric-max | head | 1.21 | 0.80 | 0.82 | -| | upper | 1.07 | 0.78 | 0.84 | -| | lower | **2.29** | 0.89 | 0.94 | -| | eyes | 0.42 | 0.45 | 0.46 | -| | hair | 0.47 | 0.33 | 0.21 | - -Encoded bytes, all five channels summed: - -| Set | 512 | 1024 | 2048 | -|---|---|---|---| -| truly-stock | 213,431 | 310,333 | 574,645 | -| aleric-max | 227,143 | 347,324 | 634,332 | - -**Reading: 1024 is the knee.** Every channel improves from 512 to 1024 — and the richer outfit improves -dramatically (aleric lower 2.29 → 0.89, the single worst number in the whole matrix, and it is at 512). Going -1024 → 2048 buys nothing: five of the nine channel rows get *worse*, none improves materially, and the cost -rises ~1.8×. 512 costs ~0.69× of 1024, which is not much of a saving for a visible loss on a busy outfit. - -This is evidence, not a decision: **ADR-008 is not changed here.** But note the inconsistency it should -settle — ADR-008 and Ledger D-7 record the default as **512**, while S1 shipped -`[Appearance] BakeSize = 1024` in `OpenSimDefaults.ini` and the live sim is running 1024. The measurements -support 1024; the ADR should be updated to match reality rather than the ini changed to match the ADR. - -## 7. Harness changes - -`Golden/` now holds one subdirectory per reference set, each with its own committed `manifest.json` and -gitignored `fixtures/`. `fetch-fixtures.sh ` populates one set, reading the avatar name from that set's -manifest, with the S0b fallbacks unchanged (Robust first, then the region's Flotsam cache; the source is -reported per UUID). `GoldenTests` is a `[Theory]` over the sets present, plus `bake_size_sweep`, which reports -§6's numbers and asserts only that every channel encodes at the size asked for. - -For `aleric-max`: all 9 non-null wearables and all 11 textures came from **Robust**; all 5 reference bakes came -from the **region cache** (bakes are temporary assets — Robust 404s them). The fetch script now skips a worn -slot carrying the null asset id, as the orchestrator does, and says so in its table. diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AISv3Module.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AISv3Module.cs index 65292cc2e58..bb27ea07ee6 100644 --- a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AISv3Module.cs +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AISv3Module.cs @@ -37,8 +37,8 @@ public class AISv3Module : ISharedRegionModule /// <capurl>/category/{id}/children, <capurl>/orphans. The listener keeps exact and /// variable-path handlers in different dictionaries and only the latter is matched by prefix /// (BaseHttpServer.TryGetSimpleStreamHandler, AddSimpleStreamHandler), so registering the - /// default way makes every AIS request 404 before the handler is entered. That is the A6 live failure; - /// see Docs/feature/ais-v3/A6-LIVE-FAILURE.md. + /// default way makes every AIS request 404 before the handler is entered, while the seed capabilities + /// response still looks correct — which is exactly how this was first found, on a live region. /// public const bool VarPath = true; diff --git a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisInventory.cs b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisInventory.cs index 651b1424bac..5661f0f6477 100644 --- a/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisInventory.cs +++ b/Source/OpenSim.Region.ClientStack.LindenCaps/AIS/AisInventory.cs @@ -124,9 +124,9 @@ public static InventoryFolderBase GetCurrentOutfit(IAisInventoryBackend backend, /// The service's own resolution is a coin flip: XInventoryService.GetSystemFolderForType returns /// folders[0] from a query with no ORDER BY and no LIMIT /// (MySQLGenericTableHandler.Get passes an empty options), and nothing in the schema forbids - /// duplicates — inventoryfolders has no unique key on (agentID, type). On Legion Grid seven - /// accounts carry two type-46 folders each, and picking the wrong one silently writes an outfit change into a - /// folder no viewer reads. That is the A7 live failure; see Docs/feature/ais-v3/A7-DUPLICATE-COF.md. + /// duplicates — inventoryfolders has no unique key on (agentID, type). Where an agent does + /// carry two folders of a type, picking the wrong one silently writes an outfit change into a folder no + /// viewer reads, which is how this was found live. /// /// The rule is **highest Version, lowest id on a tie**. A folder's version is incremented on /// every child add or remove and never decreases, so the folder the viewer has been writing to is the folder @@ -165,10 +165,10 @@ public static InventoryFolderBase GetSystemFolder(IAisInventoryBackend backend, // outfit change that quietly does not stick. m_log.LogWarning( "[AIS]: agent {Agent} has {Count} folders of type {Type} ({Candidates}); using {Chosen} version {Version}. " - + "Duplicate system folders DIRECTLY UNDER THE ROOT are a data fault, not an AIS one - see " - + "Docs/feature/ais-v3/A7-DUPLICATE-COF.md. A second folder of this type inside My Suitcase is " - + "EXPECTED (HGSuitcaseInventoryService.CreateSystemFolders builds a full set there) and is not a " - + "fault; AIS-COF-1 found that every \"duplicate\" Current Outfit on this grid was the suitcase one", + + "Duplicate system folders DIRECTLY UNDER THE ROOT are a data fault, not an AIS one. A second " + + "folder of this type inside My Suitcase is EXPECTED (HGSuitcaseInventoryService.CreateSystemFolders " + + "builds a full set there) and is not a fault: on the grid where this was first investigated every " + + "apparent duplicate Current Outfit turned out to be the suitcase one", agentId, candidates.Count, type, string.Join(", ", candidates.Select(f => $"{f.ID} v{f.Version}")), chosen.ID, chosen.Version); diff --git a/Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs b/Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs index 93918faf7a1..ea672d3f167 100755 --- a/Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs +++ b/Source/OpenSim.Region.CoreModules/Avatar/AvatarFactory/AvatarFactoryModule.cs @@ -106,7 +106,7 @@ public void RemoveRegion(Scene scene) /// **silently**: the change stayed in memory and died with the presence. A detach followed by a logout inside /// the save window therefore left the stored appearance still wearing the garment, and the viewer put the item /// back on the next login. Wear is affected identically. This predates AIS and hits the legacy path just as - /// hard; the evidence is in Docs/feature/ais-v3/A12-ATTACHMENT-RECONCILIATION.md of the AIS branch. + /// hard: it is a property of the deferred save, not of whichever protocol requested the change. /// /// Why OnRemovePresence is the right hook. Scene.RemoveClient raises it at /// Scene.cs:3866, while the presence is still in the scene graph: it is not removed until diff --git a/Source/OpenSim.Services.InventoryService/XInventoryService.cs b/Source/OpenSim.Services.InventoryService/XInventoryService.cs index 0e93dba1508..ca1704bca24 100644 --- a/Source/OpenSim.Services.InventoryService/XInventoryService.cs +++ b/Source/OpenSim.Services.InventoryService/XInventoryService.cs @@ -210,9 +210,8 @@ private bool CreateUserInventoryLocked(UUID principalID) /// trips old, and this method is entered concurrently for the same principal: Direct Delivery calls /// CreateUserInventory on every delivery, and a region can call it at any time through /// XInventoryInConnector. Two overlapping calls that both read "missing" both create, and there is no - /// unique key on (agentID, type) to catch the loser — which is how seven Legion Grid accounts came to - /// hold two Current Outfit folders each, one of them never written to (A7, - /// Docs/feature/ais-v3/A7-DUPLICATE-COF.md). + /// unique key on (agentID, type) to catch the loser, so the losing folder simply exists from then on, + /// never written to. /// /// Re-reading immediately before the insert narrows that window from the whole method to a single /// query. It does not close it. Nothing here can: only a unique constraint on (agentID, type) @@ -260,8 +259,9 @@ private XInventoryFolder EnsureSystemFolder(UUID principalID, UUID rootID, XInve /// keeps only type >= 0. /// /// - /// A warning here is a data fault and wants the dedupe in Docs/feature/ais-v3/A7-DUPLICATE-COF.md. It is - /// not self-healing: nothing in this class removes a folder. + /// A warning here is a data fault and wants an operator to merge the folders by hand, moving the + /// contents of the unused one into the one the viewer writes to. It is not self-healing: nothing in this + /// class removes a folder. /// private void WarnOnDuplicateSystemFolders(UUID principalID, XInventoryFolder[] sysFolders) { @@ -285,9 +285,9 @@ private void WarnOnDuplicateSystemFolders(UUID principalID, XInventoryFolder[] s m_log.LogWarning( "[XINVENTORY]: agent {Principal} has {Count} folders of type {Type} directly under the inventory " - + "root ({Folders}); exactly one is expected. This is a data fault, not a fault of this login - see " - + "Docs/feature/ais-v3/A7-DUPLICATE-COF.md for the dedupe. Folders of the same type inside My " - + "Suitcase are expected and are not counted here.", + + "root ({Folders}); exactly one is expected. This is a data fault, not a fault of this login, and " + + "wants the folders merged by hand. Folders of the same type inside My Suitcase are expected and " + + "are not counted here.", principalID, kv.Value.Count, (FolderType)kv.Key, string.Join(", ", kv.Value.ConvertAll(f => $"{f.folderID} v{f.version}"))); } diff --git a/Source/OpenSimNGC.Appearance.Baking/Docs/MORPH-MASK-PASS.md b/Source/OpenSimNGC.Appearance.Baking/Docs/MORPH-MASK-PASS.md index 7f222b589e6..1da9b155684 100644 --- a/Source/OpenSimNGC.Appearance.Baking/Docs/MORPH-MASK-PASS.md +++ b/Source/OpenSimNGC.Appearance.Baking/Docs/MORPH-MASK-PASS.md @@ -5,7 +5,7 @@ **morph mask** from `LLTexLayerSet::gatherMorphMaskAlpha` (`lltexlayer.cpp:460-472`), and that is what this library produces as `CompositeResult.MorphMask` and encodes as component 4. -Authority (Ledger P-1): the LL viewer source, read read-only at `F:\viewer-develop` (viewer 26.1.1 per +Authority: the Linden Lab viewer source, read read-only (viewer 26.1.1 per `indra/newview/VIEWER_VERSION.txt`): `indra/llappearance/lltexlayer.cpp`, `lltexlayer.h`, `lltexlayerparams.cpp`, `indra/newview/llviewertexlayer.cpp`, and `indra/newview/character/avatar_lad.xml`. Line numbers below are from those files at that version. This document is the spec `TexLayerCompositor` diff --git a/Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md b/Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md index 76d5e7783db..94615b19d2a 100644 --- a/Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md +++ b/Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md @@ -14,8 +14,8 @@ code). The file is embedded unmodified as a data resource; it is not compiled or linked into executable code. The full LGPL 2.1 text is in the viewer's `LICENSE` file and at . -**Copied from:** a local checkout of the viewer source tree at `F:\viewer-develop` -on 2026-09-03. That directory is not a git repository (no `.git`), so the exact +**Copied from:** a local checkout of the Linden Lab viewer source tree, taken +on 2026-09-03. That checkout is not a git repository (no `.git`), so the exact upstream commit could not be read from it. Identifying data that could be read: | Field | Value | @@ -28,7 +28,7 @@ upstream commit could not be read from it. Identifying data that could be read: The SSB design documents (`Docs/feature/ssb-appearance/RECON-ssb-appearance-addendum.md` §3) name the viewer commit used for the wire contract as `62033f2`; that identity could -not be confirmed against `F:\viewer-develop` and is recorded here as a claim, not a fact. +not be confirmed against that checkout and is recorded here as a claim, not a fact. **Modifications:** none. Byte-for-byte copy. @@ -42,7 +42,7 @@ shaped by one), so they ship with the library alongside `avatar_lad.xml` (ADR-00 **Licence:** the same as `avatar_lad.xml` above: GNU LGPL 2.1 with the Linden Lab viewer linking exception. Embedded unmodified as data resources; not compiled or linked. -**Source:** the Linden Lab viewer source tree at `F:\viewer-develop` (viewer 26.1.1 per +**Source:** the Linden Lab viewer source tree (viewer 26.1.1 per `indra/newview/VIEWER_VERSION.txt`), directory `indra/newview/character/`. The files were first taken (S0b, 2026-09-03) from the copy redistributed inside the `LibreMetaverse` 3.1.4 NuGet package; in S0d every one of the 56 was re-verified byte for byte (SHA-256 below) against the viewer tree and found identical, so the viewer diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCapRegistrationTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCapRegistrationTests.cs index e8e0229a981..1f8aba7fa0e 100644 --- a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCapRegistrationTests.cs +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisCapRegistrationTests.cs @@ -13,10 +13,9 @@ namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; /// -/// The wiring, not the handler. These are the tests that would have caught the A6 live failure: AIS advertised its -/// caps correctly and then every request 404'd inside the HTTP server, because a capability whose URLs carry -/// sub-paths must be registered as a **variable-path** handler and AIS was not -/// (Docs/feature/ais-v3/A6-LIVE-FAILURE.md). +/// The wiring, not the handler. These are the tests that would have caught the first live failure: AIS advertised +/// its caps correctly and then every request 404'd inside the HTTP server, because a capability whose URLs carry +/// sub-paths must be registered as a **variable-path** handler and AIS was not. /// /// The existing 114 tests all passed while this was broken: they call /// AisHandler.Handle(request, response) directly, so they exercise routing, envelopes and every operation, diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisDuplicateSystemFolderTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisDuplicateSystemFolderTests.cs index 55127e4713b..6d538d253a3 100644 --- a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisDuplicateSystemFolderTests.cs +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisDuplicateSystemFolderTests.cs @@ -8,9 +8,9 @@ namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; /// -/// The A7 live failure: an agent with two Current Outfit folders, where the inventory service resolves -/// "current" to the wrong one and a take-off is written into a folder no viewer reads -/// (Docs/feature/ais-v3/A7-DUPLICATE-COF.md). +/// The live failure this resolution rule exists for: an agent with two Current Outfit folders, where the +/// inventory service resolves "current" to the wrong one and a take-off is written into a folder no +/// viewer reads. /// /// The existing suite could not catch this because every fixture gives its fake agent exactly **one** folder /// per type. `FakeAisBackend.GetFolderForType` then answers correctly no matter what rule it uses, so the tests diff --git a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisResponseLoggingTests.cs b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisResponseLoggingTests.cs index 569c109b142..3c32beaba09 100644 --- a/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisResponseLoggingTests.cs +++ b/Tests/OpenSim.Region.ClientStack.LindenCaps.AIS.Tests/AIS/AisResponseLoggingTests.cs @@ -11,10 +11,10 @@ namespace OpenSim.Region.ClientStack.LindenCaps.AIS.Tests; /// -/// A11: every mutation must say, in the log, what it answered — the status and the delta keys with their -/// contents. A10 could not separate "the viewer rejected our delta" from "our delta was fine" because nothing -/// recorded what was sent back; both hypotheses died on the same missing evidence -/// (Docs/feature/ais-v3/A10-STEP10-REDIAGNOSIS.md). +/// Every mutation must say, in the log, what it answered — the status and the delta keys with their contents. +/// A live diagnosis once could not separate "the viewer rejected our delta" from "our delta was fine", because +/// nothing recorded what had been sent back; both hypotheses died on the same missing evidence. This is that +/// evidence, made permanent. /// [TestFixture] public class AisResponseLoggingTests