2342 results

global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Twitch Poop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "twitchpoop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "garry" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "garry.twitchpoop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "6/6/2026 7:39:31 PM" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "25" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.128.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.128.0")]
using System.Text.Json;
using SboxMcp.Server;
using Xunit;

namespace SboxMcp.Tests;

public class ProtocolTests
{
	[Fact]
	public void Parse_request_with_id_and_params()
	{
		var req = JsonRpcRequest.Parse( """{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"x"}}""" );

		Assert.False( req.IsNotification );
		Assert.Equal( 7, req.Id.Value.GetInt32() );
		Assert.Equal( "tools/call", req.Method );
		Assert.Equal( "x", req.Params.Value.GetProperty( "name" ).GetString() );
	}

	[Fact]
	public void Parse_notification_has_no_id()
	{
		var req = JsonRpcRequest.Parse( """{"jsonrpc":"2.0","method":"notifications/initialized"}""" );

		Assert.True( req.IsNotification );
		Assert.Equal( "notifications/initialized", req.Method );
	}

	[Fact]
	public void Parse_invalid_json_throws()
	{
		Assert.Throws<JsonRpcParseException>( () => JsonRpcRequest.Parse( "{nope" ) );
	}

	[Fact]
	public void Parse_missing_method_throws()
	{
		Assert.Throws<JsonRpcParseException>( () => JsonRpcRequest.Parse( """{"jsonrpc":"2.0","id":1}""" ) );
	}

	[Fact]
	public void Writer_result_emits_envelope()
	{
		var id = JsonDocument.Parse( "3" ).RootElement;
		var json = JsonRpcWriter.Result( id, new { protocolVersion = "2025-06-18" } );
		var doc = JsonDocument.Parse( json ).RootElement;

		Assert.Equal( "2.0", doc.GetProperty( "jsonrpc" ).GetString() );
		Assert.Equal( 3, doc.GetProperty( "id" ).GetInt32() );
		Assert.Equal( "2025-06-18", doc.GetProperty( "result" ).GetProperty( "protocolVersion" ).GetString() );
	}

	[Fact]
	public void Writer_error_emits_code_and_message()
	{
		var json = JsonRpcWriter.Error( null, JsonRpcError.MethodNotFound, "no such method" );
		var doc = JsonDocument.Parse( json ).RootElement;

		Assert.Equal( JsonValueKind.Null, JsonKind( doc, "id" ) );
		Assert.Equal( -32601, doc.GetProperty( "error" ).GetProperty( "code" ).GetInt32() );
		Assert.Equal( "no such method", doc.GetProperty( "error" ).GetProperty( "message" ).GetString() );
	}

	[Fact]
	public void Records_serialize_camel_case()
	{
		var schema = JsonDocument.Parse( """{"type":"object"}""" ).RootElement;
		var json = JsonRpcWriter.Result( null,
			McpResults.ToolsList( new[] { new McpToolDescriptor( "a_tool", "does things", schema ) } ) );
		var doc = JsonDocument.Parse( json ).RootElement;
		var tool = doc.GetProperty( "result" ).GetProperty( "tools" )[0];

		Assert.Equal( "a_tool", tool.GetProperty( "name" ).GetString() );
		Assert.Equal( "does things", tool.GetProperty( "description" ).GetString() );
		Assert.Equal( "object", tool.GetProperty( "inputSchema" ).GetProperty( "type" ).GetString() );
	}

	[Fact]
	public void Version_negotiation()
	{
		// only 2025-06-18 is supported (older revisions require JSON-RPC batching)
		Assert.Equal( "2025-06-18", McpVersion.Negotiate( "2025-06-18" ) );
		Assert.Equal( "2025-06-18", McpVersion.Negotiate( "2025-03-26" ) );
		Assert.Equal( "2025-06-18", McpVersion.Negotiate( null ) );
	}

	[Fact]
	public void Null_id_is_rejected()
	{
		Assert.Throws<JsonRpcParseException>( () =>
			JsonRpcRequest.Parse( """{"jsonrpc":"2.0","id":null,"method":"ping"}""" ) );
	}

	[Fact]
	public void Text_content_shape()
	{
		var json = JsonRpcWriter.Result( null, McpResults.TextContent( "hello", isError: true ) );
		var result = JsonDocument.Parse( json ).RootElement.GetProperty( "result" );

		Assert.Equal( "text", result.GetProperty( "content" )[0].GetProperty( "type" ).GetString() );
		Assert.Equal( "hello", result.GetProperty( "content" )[0].GetProperty( "text" ).GetString() );
		Assert.True( result.GetProperty( "isError" ).GetBoolean() );
	}

	[Fact]
	public void Image_content_shape()
	{
		var json = JsonRpcWriter.Result( null, McpResults.ImageContent( "QUJD", "a screenshot" ) );
		var content = JsonDocument.Parse( json ).RootElement.GetProperty( "result" ).GetProperty( "content" );

		Assert.Equal( "image", content[0].GetProperty( "type" ).GetString() );
		Assert.Equal( "QUJD", content[0].GetProperty( "data" ).GetString() );
		Assert.Equal( "image/png", content[0].GetProperty( "mimeType" ).GetString() );
		Assert.Equal( "a screenshot", content[1].GetProperty( "text" ).GetString() );
	}

	static JsonValueKind JsonKind( JsonElement el, string prop ) =>
		el.TryGetProperty( prop, out var v ) ? v.ValueKind : JsonValueKind.Undefined;
}
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Maths;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Cleanup;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>Tunables for the grounded-foot stance recalibration pass.</summary>
public sealed class FootGroundAlignOptions
{
    /// <summary>
    /// Dead zone (degrees): measured stance offsets at or below this are genuine planted
    /// articulation (heel-roll bias, natural lean — measured 2–4° on well-rested rigs and
    /// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.
    /// Only offsets beyond it are clearly rest-pose artifacts (measured 12–25° on the
    /// repro rig) and get recalibrated.
    /// </summary>
    public float MinCorrectionDeg { get; set; } = 8f;

    /// <summary>
    /// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE
    /// for the offset measurement. Plants beyond this are not standing on the sole (crawls,
    /// kneels, prone contact — measured 60–90° there) and are excluded; genuine rest-pose
    /// stance artifacts measure well below it (largest seen: 27°).
    /// </summary>
    public float MaxStanceDeviationDeg { get; set; } = 35f;
}

/// <summary>Per-foot results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignFootReport
{
    /// <summary>Plants that contributed to the stance measurement.</summary>
    public int StancePlants { get; set; }

    /// <summary>Plants excluded as non-stance (mean sole deviation beyond
    /// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/>).</summary>
    public int SkippedPlants { get; set; }

    /// <summary>Measured planted sole offset from the ground plane, degrees (0 when no
    /// stance plants exist).</summary>
    public float MeasuredOffsetDeg { get; set; }

    /// <summary>Foot correction applied to every frame, degrees (0 = inside the dead zone,
    /// nothing changed).</summary>
    public float AppliedFootDeg { get; set; }

    /// <summary>Toe correction applied to every frame, degrees.</summary>
    public float AppliedToeDeg { get; set; }
}

/// <summary>Results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignReport
{
    /// <summary>Left-foot results.</summary>
    public required FootGroundAlignFootReport Left { get; init; }

    /// <summary>Right-foot results.</summary>
    public required FootGroundAlignFootReport Right { get; init; }
}

/// <summary>
/// Grounded-foot stance recalibration: measures how far the foot's SOLE sits from the ground
/// plane while planted, and — when that offset is clearly a rest-pose artifact — rotates it
/// out with one constant per foot, applied to every frame of the clip.
/// </summary>
/// <remarks>
/// <para><b>Why a cleanup pass.</b> The solver transfers feet as rest-relative deltas
/// (<see cref="Solve.RoleTransferMode.CharacterDeltaFromRest"/>), so the target keeps its own
/// ankle anatomy — correct whenever the source's rest pose is a flat-footed stance (the delta
/// is then "deviation from standing"). Some rigs ship a NON-stance rest (measured: an
/// Auto-Rig-Pro export whose rest foot sits 12–25° from its planted stance), and that constant
/// offset rides into every frame of the replay — planted feet hover toe-down/heel-up. What a
/// stance actually looks like is animation evidence (planted phases), which a per-frame
/// solver cannot see, so the recalibration lives here.</para>
/// <para><b>Measurement.</b> Per foot: over every planted frame, the sole normal = rest up
/// carried by the foot's world delta from the target bind rest (whose feet stand on the
/// ground by construction); plants whose own mean normal sits beyond
/// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/> are excluded (crawl/kneel/prone
/// contact is not a stance). The pooled mean normal's deviation from up is the stance
/// offset.</para>
/// <para><b>Correction.</b> Offsets inside <see cref="FootGroundAlignOptions.MinCorrectionDeg"/>
/// are genuine articulation — nothing is changed (well-rested rigs and same-rig round trips
/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the
/// pooled normal back to up (pitch+roll only — yaw/toe-out is pose and follows the source)
/// premultiplies the foot's world rotation on EVERY frame: a rest artifact is constant, so
/// the fix is too — within-plant heel-roll, swing styling and frame-to-frame continuity are
/// preserved exactly, and no blending is needed. The toe then receives its own residual
/// constant measured on top of the corrected foot (it neither double-rotates with the foot
/// fix nor inherits the source toe's own rest artifact). Corrections rotate bones about
/// their own joints: ankle positions are untouched, so the pass composes freely with the
/// <see cref="FootPlant"/> position pinning (which preserves foot world rotations).</para>
/// <para><b>Plant intervals come from the caller</b> (the pipeline detects them on the
/// SOURCE clip via <see cref="FootPlant.DetectPlantIntervals"/> — ground truth, immune to
/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned
/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass
/// only when the source's normalized rest is implausible as a flat stance (toe at/above
/// ankle level or asymmetric feet — see <c>Retargeter.GroundAlignFeet</c>); on plausible
/// stance rests the solver's rest-relative transfer is already faithful and planted-sole
/// deviations are genuine articulation (boxing stances, heel rolls) that must not be
/// flattened.</para>
/// </remarks>
public static class FootGroundAlign
{
    /// <summary>Measures planted stance offsets and recalibrates feet whose offset is a
    /// rest-pose artifact; returns what was measured and done.</summary>
    /// <param name="frames">Per-frame local transforms (skeleton bone order); modified in place.</param>
    /// <param name="skeleton">Bone hierarchy the frames are expressed against; its bind rest
    /// is the flat-stance reference.</param>
    /// <param name="left">Left leg chain bone indices.</param>
    /// <param name="right">Right leg chain bone indices.</param>
    /// <param name="up">World up direction of the clip's space.</param>
    /// <param name="leftPlants">Left-foot plant intervals (frame indices into
    /// <paramref name="frames"/>; out-of-range parts are clamped/ignored).</param>
    /// <param name="rightPlants">Right-foot plant intervals.</param>
    /// <param name="options">Tunables; defaults used when null.</param>
    public static FootGroundAlignReport Apply(
        List<XForm[]> frames,
        SkeletonModel skeleton,
        FootChain left,
        FootChain right,
        Vector3 up,
        IReadOnlyList<FrameRange> leftPlants,
        IReadOnlyList<FrameRange> rightPlants,
        FootGroundAlignOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(frames);
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(left);
        ArgumentNullException.ThrowIfNull(right);
        ArgumentNullException.ThrowIfNull(leftPlants);
        ArgumentNullException.ThrowIfNull(rightPlants);

        options ??= new FootGroundAlignOptions();
        var report = new FootGroundAlignReport
        {
            Left = new FootGroundAlignFootReport(),
            Right = new FootGroundAlignFootReport(),
        };
        if (frames.Count == 0 || up.LengthSquared() < 1e-12f)
            return report;
        up = Vector3.Normalize(up);

        RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);
        RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);
        return report;
    }

    private static void RecalibrateFoot(
        List<XForm[]> frames, SkeletonModel skeleton, FootChain chain, Vector3 up,
        IReadOnlyList<FrameRange> plants, FootGroundAlignOptions options,
        FootGroundAlignFootReport report)
    {
        int n = frames.Count;
        var foot = chain.Ankle;
        var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);
        var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);

        // ---- measurement: pooled planted sole normal over the stance plants ----
        var pooled = Vector3.Zero;
        foreach (var plant in plants)
        {
            int start = Math.Max(plant.Start, 0);
            int end = Math.Min(plant.End, n - 1);
            if (start > end)
                continue;

            var plantSum = Vector3.Zero;
            for (int f = start; f <= end; f++)
            {
                var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;
                plantSum += Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));
            }
            if (plantSum.LengthSquared() < 1e-8f
                || Vector3.Dot(Vector3.Normalize(plantSum), up) < maxStanceCos)
            {
                report.SkippedPlants++; // not standing on the sole — crawl/kneel/toe contact
                continue;
            }
            report.StancePlants++;
            pooled += plantSum; // frame-count-weighted: longer stances dominate
        }
        if (pooled.LengthSquared() < 1e-8f)
            return;
        pooled = Vector3.Normalize(pooled);

        var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);
        report.MeasuredOffsetDeg = offsetDeg;
        if (offsetDeg <= options.MinCorrectionDeg)
            return; // genuine planted articulation — leave the transfer byte-faithful

        // ---- correction: one constant per foot, every frame ----
        var footFix = MathQ.FromTo(pooled, up);
        report.AppliedFootDeg = offsetDeg;

        // Toe residual measured on top of the corrected foot, same dead zone.
        var toeFix = Quaternion.Identity;
        if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
        {
            var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);
            var toePooled = Vector3.Zero;
            foreach (var plant in plants)
            {
                int start = Math.Max(plant.Start, 0);
                int end = Math.Min(plant.End, n - 1);
                for (int f = start; f <= end && f >= 0; f++)
                {
                    var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;
                    toePooled += Vector3.Transform(
                        up, MathQ.Normalize(footFix * toeRot * restToeRotInv));
                }
            }
            if (toePooled.LengthSquared() > 1e-8f)
            {
                toePooled = Vector3.Normalize(toePooled);
                var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);
                if (toeDeg > options.MinCorrectionDeg && Vector3.Dot(toePooled, up) >= maxStanceCos)
                {
                    toeFix = MathQ.FromTo(toePooled, up);
                    report.AppliedToeDeg = toeDeg;
                }
            }
        }

        for (int f = 0; f < n; f++)
            CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);
    }

    /// <summary>Premultiplies the foot's world rotation by the constant fix (the joint
    /// position is untouched — the rotation pivots the foot about its own head), then gives
    /// the toe its own residual on top of the corrected foot.</summary>
    private static void CorrectFrame(
        XForm[] locals, SkeletonModel skeleton, FootChain chain,
        Quaternion footFix, Quaternion toeFix)
    {
        var foot = chain.Ankle;
        var parent = skeleton[foot].ParentIndex;
        var parentRot = parent < 0
            ? Quaternion.Identity
            : FkUtil.BoneWorld(locals, skeleton, parent).Rot;

        var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);
        var newFootWorld = MathQ.Normalize(footFix * footWorld);
        locals[foot] = new XForm(
            locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));

        if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
        {
            // Desired toe world = toeFix ∘ footFix ∘ original world; re-derive its local
            // against the corrected foot so it does not double-rotate with the foot fix.
            var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);
            var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);
            locals[toe] = new XForm(
                locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));
        }
    }
}
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Solve;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Dl;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>The z-normalization statistics shipped with the SAME checkpoint
/// (<c>ms_dict</c>): per-feature mean/std applied to every input except contact.</summary>
public sealed class SameStats
{
    internal float[] LoM, LoS, GoM, GoS, QM, QS, PM, PS, RM, RS, PvM, PvS, QvM, QvS, PprevM, PprevS;

    /// <summary>Reads the 16 <c>ms.*</c> arrays from a parsed weight blob.</summary>
    public SameStats(SameWeights weights)
    {
        ArgumentNullException.ThrowIfNull(weights);
        LoM = weights.Stat("lo_m"); LoS = weights.Stat("lo_s");
        GoM = weights.Stat("go_m"); GoS = weights.Stat("go_s");
        QM = weights.Stat("q_m"); QS = weights.Stat("q_s");
        PM = weights.Stat("p_m"); PS = weights.Stat("p_s");
        RM = weights.Stat("r_m"); RS = weights.Stat("r_s");
        PvM = weights.Stat("pv_m"); PvS = weights.Stat("pv_s");
        QvM = weights.Stat("qv_m"); QvS = weights.Stat("qv_s");
        PprevM = weights.Stat("pprev_m"); PprevS = weights.Stat("pprev_s");
    }
}

/// <summary>A batched per-frame source graph ready for <see cref="SameModel.Encode"/>.</summary>
public sealed class SameSourceGraph
{
    /// <summary>Normalized node features, flat [FrameCount·JointCount × 32].</summary>
    public required float[] X { get; init; }

    /// <summary>Edge sources (bidirectional + self-loops, all frames).</summary>
    public required int[] EdgeSrc { get; init; }

    /// <summary>Edge destinations.</summary>
    public required int[] EdgeDst { get; init; }

    /// <summary>Frame id per node.</summary>
    public required int[] Batch { get; init; }

    /// <summary>Number of feature frames (matches the clip's frame count in production
    /// mode; native frames − 2 in golden-parity mode).</summary>
    public required int FrameCount { get; init; }

    /// <summary>Graph joints per frame (hips subtree + end joints).</summary>
    public required int JointCount { get; init; }

    /// <summary>Graph node names within one frame (bone names; synthesized leaf tips get
    /// a <c>_end</c> suffix). For diagnostics and parity tests.</summary>
    public required string[] JointNames { get; init; }
}

/// <summary>
/// Source-side feature pipeline of the SAME port (FEASIBILITY.md "C# port work list"
/// steps 1–5): skeleton normalization, cm/Y-up/+Z-facing alignment, per-frame
/// q/p/r/pv/qv/pprev/c features in the root-facing frame, z-normalization, and the
/// bidirectional+self-loop edge list.
/// </summary>
/// <remarks>
/// <para><b>Skeleton normalization without an intermediate skeleton.</b> SAME's
/// <c>motion_normalize</c> rebuilds the rig with identity rest-local rotations and
/// re-expresses every frame against it. Algebraically the normalized motion's world
/// rotations are exactly the world-space deltas from the T-pose,
/// <c>Ĝ(j,t) = G(j,t) · G_tpose(j)⁻¹</c>, its local rotations are
/// <c>Ĝ(parent)⁻¹ · Ĝ(j)</c>, and its world positions equal the original world positions
/// — so this port computes the features directly from FK world transforms, no rebuilt
/// skeleton needed (verified against the Python pipeline by the golden-vector tests).</para>
/// <para><b>T-pose reference.</b> SAME consumes the source clip's first frame as the
/// reference; production keeps that convention but emits one feature frame per clip frame
/// (the sequence is computed over [f0, f0…fN−1] with f0 doubling as the reference — see
/// <see cref="TposeReference"/> for why the rest-pose alternative measurably loses).
/// Golden-parity mode replicates Python's frame accounting exactly (frame 0 = reference,
/// frame 1 dropped).</para>
/// <para><b>Alignment.</b> Features assume cm (guaranteed by the importers), Y-up and
/// rest facing +Z with +X to the character's left. The source is rotated by a world
/// alignment derived from the rig's rest geometry (<see cref="CharacterFrame"/> via the
/// mapping when computable, else the file's axis metadata), snapped to the nearest whole
/// axis permutation (an exact-axis rig must map to the identity — the rest-geometry tilt
/// of a few degrees otherwise leaks into every feature), and shifted so the lowest joint
/// over the clip sits on the ground plane.</para>
/// <para><b>Graph.</b> Nodes are the hips subtree (hips = mapped Hips role, else the
/// shallowest branch bone) in skeleton order — hips is always node 0, which is where the
/// root feature row lives — plus one synthesized end joint per childless leaf (BVH End
/// Sites already import as <c>_end</c> bones and are used as-is; FBX leaves get a
/// half-length continuation of their parent segment).</para>
/// </remarks>
public static class SameFeatures
{
    /// <summary>How the T-pose reference (skeleton normalization + lo/go features) is chosen.</summary>
    public enum TposeReference
    {
        /// <summary>The clip's own first frame — SAME's native convention and the
        /// production default. Empirically the pretrained checkpoint tracks arms FAR
        /// better against the clip's first frame than against a synthesized true T-pose,
        /// even though its training references are T-poses (measured on the fixture clip:
        /// mean role cosine vs the geometric solver 0.94 first-frame vs 0.57 rest-pose,
        /// hands flipping negative — reproduced identically in the Python reference
        /// pipeline, so it is a property of the checkpoint, not of this port).</summary>
        FirstFrame,

        /// <summary>Synthesize the reference from the skeleton's rest pose (the
        /// FEASIBILITY suggestion; kept for experiments — see above for why it lost).</summary>
        RestPose,
    }

    /// <summary>Options for <see cref="BuildSourceGraph"/>; defaults are production mode.</summary>
    public sealed class SourceOptions
    {
        /// <summary>T-pose reference choice (see <see cref="TposeReference"/>).</summary>
        public TposeReference Reference { get; init; } = TposeReference.FirstFrame;

        /// <summary>SAME's native frame accounting: the first frame is consumed as the
        /// reference and the next dropped for its undefined velocity, so the output has
        /// two frames fewer than the clip. Golden-parity tests only — production emits
        /// one feature frame per clip frame (the first frame doubles as the reference
        /// and gets zero velocity).</summary>
        public bool NativeFrameDrop { get; init; }

        /// <summary>Apply the rest-geometry world alignment (Y-up, +Z facing). Disabled
        /// only by golden-parity tests (Python applies none).</summary>
        public bool Align { get; init; } = true;

        /// <summary>Ground both the T-pose reference and the animation: the T-pose is
        /// shifted so its lowest joint sits at height 0 (a BVH rest pose has its root at
        /// the origin and would otherwise put the hips on the floor), and the animation is
        /// shifted by its own lowest joint height over the clip (no-op for the usual
        /// authored-ground-at-0 data). Disabled only by golden-parity tests (the Python
        /// reference consumes data as authored).</summary>
        public bool GroundShift { get; init; } = true;
    }

    private const float ContactHeightCm = 5f;
    private const float ContactSpeedMps = 0.4f;
    private const float VelocityFps = 30f;

    /// <summary>
    /// Builds the batched source graph for one clip: graph selection, alignment, per-frame
    /// features, normalization, edges.
    /// </summary>
    /// <param name="scene">Imported source (cm, native axes).</param>
    /// <param name="clipIndex">Clip to encode.</param>
    /// <param name="map">Source mapping; used only for hips identification and the
    /// rest-geometry alignment (the model itself is skeleton-agnostic). May be sparse —
    /// heuristics cover missing roles.</param>
    /// <param name="stats">Normalization statistics.</param>
    /// <param name="options">Null = production mode.</param>
    public static SameSourceGraph BuildSourceGraph(
        SourceScene scene, int clipIndex, MappingResult? map, SameStats stats, SourceOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(scene);
        ArgumentNullException.ThrowIfNull(stats);
        options ??= new SourceOptions();
        if (clipIndex < 0 || clipIndex >= scene.Clips.Count)
            throw new ArgumentOutOfRangeException(nameof(clipIndex));
        var clip = scene.Clips[clipIndex];
        if (clip.FrameCount < 1)
            throw new ArgumentException("Clip has no frames.", nameof(clipIndex));
        if (options.NativeFrameDrop && clip.FrameCount < 3)
            throw new ArgumentException("Native frame accounting needs at least 3 frames.", nameof(options));

        var skeleton = scene.Skeleton;
        var hips = FindHips(skeleton, map);
        var nodes = GraphNodes.Build(skeleton, hips);

        var align = options.Align ? ComputeAlignment(skeleton, map, scene) : Quaternion.Identity;

        // T-pose reference world transforms (aligned), grounded on its own lowest joint
        // (a BVH rest pose has the root at the origin — ungrounded, its hips would sit on
        // the floor and every height-bearing feature would be wrong).
        var tposeLocals = options.Reference == TposeReference.RestPose
            ? Pose.Rest(skeleton).Locals
            : clip.Frames[0];
        var tposeWorld = AlignedWorld(skeleton, tposeLocals, align, nodes);
        if (options.GroundShift)
            ShiftToGround(tposeWorld.Pos);

        // The pose sequence the features run over; features are emitted for seq[1..].
        var seq = new List<XForm[]>();
        if (options.NativeFrameDrop)
        {
            for (var f = 1; f < clip.FrameCount; f++)
                seq.Add(clip.Frames[f]);
        }
        else
        {
            seq.Add(clip.Frames[0]); // duplicated: gives the real first frame zero velocity
            for (var f = 0; f < clip.FrameCount; f++)
                seq.Add(clip.Frames[f]);
        }

        var frames = seq.Count - 1;
        var j = nodes.Count;

        // Pass 0: aligned world transforms; ground the whole clip on its lowest joint.
        var worlds = new AlignedFrame[seq.Count];
        for (var t = 0; t < seq.Count; t++)
            worlds[t] = AlignedWorld(skeleton, seq[t], align, nodes);
        if (options.GroundShift)
        {
            var ground = float.PositiveInfinity;
            foreach (var world in worlds)
            {
                foreach (var p in world.Pos)
                    ground = MathF.Min(ground, p.Y);
            }
            if (float.IsFinite(ground) && ground != 0f)
            {
                foreach (var world in worlds)
                {
                    for (var i = 0; i < j; i++)
                        world.Pos[i].Y -= ground;
                }
            }
        }

        // Pass 1: normalized-skeleton local rotations + facing per frame.
        var localRots = new Quaternion[seq.Count][]; // facing-adjusted at the root row
        var facing = new (float Yaw, Vector3 Pos)[seq.Count];
        for (var t = 0; t < seq.Count; t++)
        {
            var world = worlds[t];

            // Normalized-skeleton world rotations: world delta from the T-pose.
            var normWorld = new Quaternion[j];
            for (var i = 0; i < j; i++)
                normWorld[i] = MathQ.Normalize(world.Rot[i] * Quaternion.Conjugate(tposeWorld.Rot[i]));

            // Root facing: yaw (about +Y) of the normalized root rotation, at the root's
            // ground-plane position.
            var yaw = YawAngle(normWorld[0]);
            facing[t] = (yaw, new Vector3(world.Pos[0].X, 0f, world.Pos[0].Z));

            // Normalized-skeleton local rotations; root premultiplied by the inverse facing.
            var locals = new Quaternion[j];
            locals[0] = MathQ.Normalize(Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw) * normWorld[0]);
            for (var i = 1; i < j; i++)
            {
                locals[i] = MathQ.Normalize(
                    Quaternion.Conjugate(normWorld[nodes.Parent[i]]) * normWorld[i]);
            }
            localRots[t] = locals;
        }

        // Pass 2: feature rows.
        var x = new float[frames * j * SameModel.InputDim];
        for (var t = 1; t < seq.Count; t++)
        {
            var f = t - 1;
            var (yaw, fpos) = facing[t];
            var invFacing = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw);
            var (yawPrev, fposPrev) = facing[t - 1];
            var invFacingPrev = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yawPrev);

            // r: facing delta (dθ, dx, dz) + absolute root height.
            var dTheta = WrapPi(yaw - yawPrev);
            var dPlanar = Vector3.Transform(fpos - fposPrev, invFacingPrev);
            var rootHeight = worlds[t].Pos[0].Y;

            for (var i = 0; i < j; i++)
            {
                var row = (f * j + i) * SameModel.InputDim;
                var col = 0;

                // ---- skel: lo, go (tiled per frame) -------------------------------------
                Vector3 lo, go;
                if (i == 0)
                {
                    lo = new Vector3(0f, tposeWorld.Pos[0].Y, 0f);
                    go = lo;
                }
                else
                {
                    lo = tposeWorld.Pos[i] - tposeWorld.Pos[nodes.Parent[i]];
                    go = tposeWorld.Pos[i] - new Vector3(tposeWorld.Pos[0].X, 0f, tposeWorld.Pos[0].Z);
                }
                WriteNorm3(x, row, ref col, lo, stats.LoM, stats.LoS);
                WriteNorm3(x, row, ref col, go, stats.GoM, stats.GoS);

                // ---- q ------------------------------------------------------------------
                WriteNorm6(x, row, ref col, SixD(localRots[t][i]), stats.QM, stats.QS);

                // ---- p (facing-frame-relative global position) --------------------------
                var p = Vector3.Transform(worlds[t].Pos[i] - fpos, invFacing);
                WriteNorm3(x, row, ref col, p, stats.PM, stats.PS);

                // ---- r (root row only; other rows are the mean → zeros after norm) ------
                if (i == 0)
                {
                    x[row + col++] = (dTheta - stats.RM[0]) / stats.RS[0];
                    x[row + col++] = (dPlanar.X - stats.RM[1]) / stats.RS[1];
                    x[row + col++] = (dPlanar.Z - stats.RM[2]) / stats.RS[2];
                    x[row + col++] = (rootHeight - stats.RM[3]) / stats.RS[3];
                }
                else
                {
                    col += 4; // already zero
                }

                // ---- pv (facing-frame velocity, ×30 fps) ---------------------------------
                var pv = Vector3.Transform(worlds[t].Pos[i] - worlds[t - 1].Pos[i], invFacing) * VelocityFps;
                WriteNorm3(x, row, ref col, pv, stats.PvM, stats.PvS);

                // ---- qv (local rotation delta) -------------------------------------------
                var qv = MathQ.Normalize(Quaternion.Conjugate(localRots[t - 1][i]) * localRots[t][i]);
                WriteNorm6(x, row, ref col, SixD(qv), stats.QvM, stats.QvS);

                // ---- pprev (previous position in the CURRENT facing frame) ---------------
                var pprev = Vector3.Transform(worlds[t - 1].Pos[i] - fpos, invFacing);
                WriteNorm3(x, row, ref col, pprev, stats.PprevM, stats.PprevS);

                // ---- c (ground contact; not normalized) -----------------------------------
                var speedMps = (worlds[t].Pos[i] - worlds[t - 1].Pos[i]).Length() * VelocityFps / 100f;
                x[row + col] = worlds[t].Pos[i].Y < ContactHeightCm && speedMps < ContactSpeedMps ? 1f : 0f;
            }
        }

        var (edgeSrc, edgeDst) = BuildEdges(nodes.Parent, frames);
        var batch = new int[frames * j];
        for (var f = 0; f < frames; f++)
        {
            for (var i = 0; i < j; i++)
                batch[f * j + i] = f;
        }

        AssertFinite(x, "SAME source features");
        return new SameSourceGraph
        {
            X = x,
            EdgeSrc = edgeSrc,
            EdgeDst = edgeDst,
            Batch = batch,
            FrameCount = frames,
            JointCount = j,
            JointNames = nodes.Names,
        };
    }

    // ================================================================ graph topology

    /// <summary>The per-frame graph node set: hips-subtree bones in skeleton order
    /// (hips first) plus synthesized end joints for childless leaves.</summary>
    internal sealed class GraphNodes
    {
        /// <summary>Skeleton bone index per node; -1 for synthesized end joints.</summary>
        public required int[] Bone { get; init; }

        /// <summary>Graph-parent node index; -1 for the root (node 0).</summary>
        public required int[] Parent { get; init; }

        /// <summary>For synthesized end joints: the rest-local offset from the leaf bone
        /// (zero vector for real bones).</summary>
        public required Vector3[] EndOffset { get; init; }

        public required string[] Names { get; init; }

        public int Count => Bone.Length;

        public static GraphNodes Build(SkeletonModel skeleton, int hips)
        {
            // Hips subtree, skeleton order (parents precede children, hips first).
            var inSubtree = new bool[skeleton.Count];
            inSubtree[hips] = true;
            var bones = new List<int> { hips };
            for (var i = hips + 1; i < skeleton.Count; i++)
            {
                var parent = skeleton[i].ParentIndex;
                if (parent >= 0 && inSubtree[parent])
                {
                    inSubtree[i] = true;
                    bones.Add(i);
                }
            }

            var nodeOfBone = new Dictionary<int, int>(bones.Count);
            for (var n = 0; n < bones.Count; n++)
                nodeOfBone[bones[n]] = n;

            var hasChild = new bool[skeleton.Count];
            foreach (var b in bones)
            {
                var parent = skeleton[b].ParentIndex;
                if (parent >= 0 && inSubtree[parent])
                    hasChild[parent] = true;
            }

            var bone = new List<int>(bones);
            var parentNode = new List<int>(bones.Count);
            var endOffset = new List<Vector3>(bones.Count);
            var names = new List<string>(bones.Count);
            foreach (var b in bones)
            {
                var p = skeleton[b].ParentIndex;
                parentNode.Add(b == hips ? -1 : nodeOfBone[p]);
                endOffset.Add(Vector3.Zero);
                names.Add(skeleton[b].Name);
            }

            // Synthesized end joints: leaves with no children anywhere in the skeleton.
            // BVH End Sites already import as real `_end`/`_End` bones and ARE the end
            // joints — no tip on a tip. The tip continues the parent→leaf segment at half
            // length — a neutral stand-in for the unknown bone tail (FBX carries none).
            foreach (var b in bones)
            {
                if (hasChild[b]
                    || skeleton[b].Name.EndsWith("_end", StringComparison.OrdinalIgnoreCase))
                    continue;
                var p = skeleton[b].ParentIndex;
                var segment = p >= 0
                    ? skeleton.RestWorld[b].Pos - skeleton.RestWorld[p].Pos
                    : Vector3.Zero;
                var tip = segment.Length() > 1e-4f ? segment * 0.5f : new Vector3(0f, 2f, 0f);
                // Express in the leaf's rest-local frame (applied via the leaf's world rot).
                var local = Vector3.Transform(tip, Quaternion.Conjugate(skeleton.RestWorld[b].Rot));
                bone.Add(-1);
                parentNode.Add(nodeOfBone[b]);
                endOffset.Add(local);
                names.Add(skeleton[b].Name + "_end");
            }

            return new GraphNodes
            {
                Bone = bone.ToArray(),
                Parent = parentNode.ToArray(),
                EndOffset = endOffset.ToArray(),
                Names = names.ToArray(),
            };
        }
    }

    /// <summary>Aligned world transforms of the graph nodes for one pose.</summary>
    internal readonly struct AlignedFrame
    {
        public required Vector3[] Pos { get; init; }
        public required Quaternion[] Rot { get; init; }
    }

    private static AlignedFrame AlignedWorld(
        SkeletonModel skeleton, XForm[] locals, Quaternion align, GraphNodes nodes)
    {
        var world = new Pose(locals).ToWorld(skeleton);
        var pos = new Vector3[nodes.Count];
        var rot = new Quaternion[nodes.Count];
        for (var n = 0; n < nodes.Count; n++)
        {
            XForm w;
            if (nodes.Bone[n] >= 0)
            {
                w = world[nodes.Bone[n]];
            }
            else
            {
                // Synthesized end joint: rides its leaf bone (identity local rotation).
                var leaf = world[nodes.Bone[nodes.Parent[n]]];
                w = new XForm(leaf.TransformPoint(nodes.EndOffset[n]), leaf.Rot);
            }
            pos[n] = Vector3.Transform(w.Pos, align);
            rot[n] = MathQ.Normalize(align * w.Rot);
        }
        return new AlignedFrame { Pos = pos, Rot = rot };
    }

    /// <summary>Bidirectional parent↔child pairs plus one self-loop per node, replicated
    /// per frame with node indices offset.</summary>
    internal static (int[] Src, int[] Dst) BuildEdges(int[] parent, int frames)
    {
        var j = parent.Length;
        var nonRoot = 0;
        for (var i = 0; i < j; i++)
        {
            if (parent[i] >= 0)
                nonRoot++;
        }
        var perFrame = nonRoot * 2 + j;
        var src = new int[perFrame * frames];
        var dst = new int[perFrame * frames];
        var e = 0;
        for (var f = 0; f < frames; f++)
        {
            var offset = f * j;
            for (var i = 0; i < j; i++)
            {
                if (parent[i] < 0)
                    continue;
                src[e] = offset + parent[i];
                dst[e] = offset + i;
                e++;
                src[e] = offset + i;
                dst[e] = offset + parent[i];
                e++;
            }
            for (var i = 0; i < j; i++)
            {
                src[e] = offset + i;
                dst[e] = offset + i;
                e++;
            }
        }
        return (src, dst);
    }

    // ================================================================ alignment + hips

    /// <summary>Mapped Hips role when available, else the shallowest bone with two or more
    /// children (the hips of any humanoid: the legs/spine branch point).</summary>
    internal static int FindHips(SkeletonModel skeleton, MappingResult? map)
    {
        if (map is not null && map.RoleToBone.TryGetValue(BoneRole.Hips, out var mapped)
            && mapped >= 0 && mapped < skeleton.Count)
            return mapped;

        var childCount = new int[skeleton.Count];
        for (var i = 0; i < skeleton.Count; i++)
        {
            if (skeleton[i].ParentIndex >= 0)
                childCount[skeleton[i].ParentIndex]++;
        }

        var best = -1;
        var bestDepth = int.MaxValue;
        for (var i = 0; i < skeleton.Count; i++)
        {
            if (childCount[i] < 2)
                continue;
            var depth = 0;
            for (var a = skeleton[i].ParentIndex; a >= 0; a = skeleton[a].ParentIndex)
                depth++;
            if (depth < bestDepth)
            {
                best = i;
                bestDepth = depth;
            }
        }
        return best >= 0 ? best : 0;
    }

    /// <summary>
    /// World rotation taking the rig into the canonical SAME frame (X = character left,
    /// Y = up, Z = facing): rest-geometry character frame when computable from the mapping,
    /// else the file's recorded axis conventions.
    /// </summary>
    internal static Quaternion ComputeAlignment(SkeletonModel skeleton, MappingResult? map, SourceScene? scene)
    {
        if (map is not null)
        {
            try
            {
                var frame = CharacterFrame.Compute(skeleton, map, skeleton.RestWorld);
                return AlignFromBasis(frame.Lateral, frame.Up, frame.Forward);
            }
            catch (ArgumentException)
            {
                // fall through to axis metadata
            }
        }

        if (scene is not null)
        {
            var up = AxisVector(scene.UpAxis, scene.UpAxisSign);
            var forward = AxisVector(scene.FrontAxis, scene.FrontAxisSign);
            if (MathF.Abs(Vector3.Dot(up, forward)) < 0.5f)
                return AlignFromBasis(Vector3.Cross(up, forward), up, forward);
        }

        return Quaternion.Identity;
    }

    /// <summary>
    /// Rotation mapping the given (left, up, forward) world directions onto (+X, +Y, +Z),
    /// snapped to the nearest whole axis permutation when one is unambiguous: rigs authored
    /// on exact axes (BVH Y-up/+Z, the s&amp;box rig, Z-up FBX) must map by an exact
    /// quarter-turn — the few degrees of rest-geometry tilt (shoulders not exactly above
    /// hips) otherwise leak into every feature and measurably cost accuracy.
    /// </summary>
    internal static Quaternion AlignFromBasis(Vector3 left, Vector3 up, Vector3 forward)
    {
        var l = SnapAxis(left);
        var u = SnapAxis(up);
        var f = SnapAxis(forward);
        if (MathF.Abs(Vector3.Dot(l, u)) > 0.5f || MathF.Abs(Vector3.Dot(l, f)) > 0.5f
            || MathF.Abs(Vector3.Dot(u, f)) > 0.5f)
        {
            // Genuinely oblique rig: keep the exact (orthonormalized) directions.
            l = Vector3.Normalize(left);
            u = Vector3.Normalize(up - l * Vector3.Dot(up, l));
            f = Vector3.Cross(l, u);
        }

        // Row-major with rows = basis images maps +X→left, +Y→up, +Z→forward
        // (System.Numerics row-vector convention); the alignment is its inverse.
        var m = new Matrix4x4(
            l.X, l.Y, l.Z, 0f,
            u.X, u.Y, u.Z, 0f,
            f.X, f.Y, f.Z, 0f,
            0f, 0f, 0f, 1f);
        return Quaternion.Conjugate(MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m)));
    }

    private static Vector3 SnapAxis(Vector3 v)
    {
        var ax = MathF.Abs(v.X);
        var ay = MathF.Abs(v.Y);
        var az = MathF.Abs(v.Z);
        if (ax >= ay && ax >= az)
            return new Vector3(MathF.Sign(v.X), 0f, 0f);
        if (ay >= az)
            return new Vector3(0f, MathF.Sign(v.Y), 0f);
        return new Vector3(0f, 0f, MathF.Sign(v.Z));
    }

    private static Vector3 AxisVector(int axis, int sign) => axis switch
    {
        0 => new Vector3(sign, 0f, 0f),
        2 => new Vector3(0f, 0f, sign),
        _ => new Vector3(0f, sign, 0f),
    };

    // ================================================================ small math

    /// <summary>The yaw (rotation about +Y) closest to <paramref name="q"/> — fairmotion's
    /// <c>Q_closest(q, identity, +Y)</c>, reproduced exactly for parity.</summary>
    internal static float YawAngle(Quaternion q)
    {
        var alpha = Math.Atan2(q.W, q.Y);
        var theta1 = -2.0 * alpha + Math.PI;
        var theta2 = -2.0 * alpha - Math.PI;
        var d1 = q.Y * Math.Sin(theta1 * 0.5) + q.W * Math.Cos(theta1 * 0.5);
        var d2 = q.Y * Math.Sin(theta2 * 0.5) + q.W * Math.Cos(theta2 * 0.5);
        return (float)(d1 > d2 ? theta1 : theta2);
    }

    private static void ShiftToGround(Vector3[] positions)
    {
        var ground = float.PositiveInfinity;
        foreach (var p in positions)
            ground = MathF.Min(ground, p.Y);
        if (!float.IsFinite(ground) || ground == 0f)
            return;
        for (var i = 0; i < positions.Length; i++)
            positions[i].Y -= ground;
    }

    internal static float WrapPi(float angle)
    {
        while (angle > MathF.PI)
            angle -= 2f * MathF.PI;
        while (angle < -MathF.PI)
            angle += 2f * MathF.PI;
        return angle;
    }

    /// <summary>6D rotation representation: the first two columns of the rotation matrix
    /// (<c>R·e_x</c> then <c>R·e_y</c>).</summary>
    internal static (Vector3 C0, Vector3 C1) SixD(Quaternion q)
        => (Vector3.Transform(Vector3.UnitX, q), Vector3.Transform(Vector3.UnitY, q));

    private static void WriteNorm3(float[] x, int row, ref int col, Vector3 v, float[] m, float[] s)
    {
        x[row + col++] = (v.X - m[0]) / s[0];
        x[row + col++] = (v.Y - m[1]) / s[1];
        x[row + col++] = (v.Z - m[2]) / s[2];
    }

    private static void WriteNorm6(float[] x, int row, ref int col, (Vector3 C0, Vector3 C1) sixD, float[] m, float[] s)
    {
        x[row + col++] = (sixD.C0.X - m[0]) / s[0];
        x[row + col++] = (sixD.C0.Y - m[1]) / s[1];
        x[row + col++] = (sixD.C0.Z - m[2]) / s[2];
        x[row + col++] = (sixD.C1.X - m[3]) / s[3];
        x[row + col++] = (sixD.C1.Y - m[4]) / s[4];
        x[row + col++] = (sixD.C1.Z - m[5]) / s[5];
    }

    internal static void AssertFinite(float[] values, string what)
    {
        foreach (var v in values)
        {
            if (!float.IsFinite(v))
                throw new InvalidOperationException($"{what} contain non-finite values.");
        }
    }
}
using System.Collections.Generic;

namespace HumanoidRetargeter.Mapping;

/// <summary>
/// Built-in preset profiles, embedded as C# data (the same data is written to
/// <c>Assets/humanoid_retargeter/profiles/*.json</c> by a regenerate-and-diff test so the
/// shipped JSON can never drift from the code).
/// </summary>
public static class ProfileLibrary
{
    /// <summary>Mixamo / Adobe rigs: <c>mixamorig[N]:</c> namespace, <c>LeftArm</c> /
    /// <c>LeftForeArm</c> / <c>LeftHandIndex1..3</c> style names.</summary>
    public static Profile Mixamo { get; } = BuildMixamo();

    /// <summary>
    /// Reallusion ActorCore / AccuRig / Character Creator rigs (<c>CC_Base_*</c>).
    /// Empirical notes from <c>research/rig_actorcore.json</c>:
    /// <list type="bullet">
    /// <item><c>CC_Base_Hip</c> is the parent of BOTH <c>CC_Base_Pelvis</c> (leg branch) and
    /// <c>CC_Base_Waist</c> (spine branch), i.e. the LCA of legs+spine and the true animated
    /// hips root → it carries <see cref="BoneRole.Hips"/>; <c>CC_Base_Pelvis</c> is a
    /// leg-branch intermediate and stays unmapped.</item>
    /// <item>The neck chain is <c>CC_Base_NeckTwist01 → CC_Base_NeckTwist02 → CC_Base_Head</c>;
    /// despite the name, <c>NeckTwist01</c> IS the neck bone (there is no plain
    /// <c>CC_Base_Neck</c>), so it is the <see cref="BoneRole.Neck"/> alias. NeckTwist02 is
    /// left unmapped. All other Twist/ShareBone helpers are excluded (no aliases).</item>
    /// <item><c>CC_Base_L_ToeBase</c> is the toe role; the co-located
    /// <c>CC_Base_L_ToeBaseShareBone</c> is a helper and must never be mapped.</item>
    /// </list>
    /// </summary>
    public static Profile ActorCoreCc { get; } = BuildActorCoreCc();

    /// <summary>Unreal Engine mannequin (UE4/UE5): <c>pelvis</c>, <c>spine_01..05</c>,
    /// <c>clavicle_l</c>, <c>thumb_01_l</c>, UE5 <c>*_metacarpal_*</c>; <c>*_twist_*</c>
    /// bones have no aliases and are never mapped.</summary>
    public static Profile UeMannequin { get; } = BuildUeMannequin();

    /// <summary>Rokoko / Xsens style BVH rigs: plain <c>Hips</c>/<c>Spine..Spine4</c>/<c>
    /// LeftArm|LeftUpperArm</c> name variants, usually no fingers.</summary>
    public static Profile RokokoBvh { get; } = BuildRokokoBvh();

    /// <summary>
    /// SMPL body model family (AMASS exports, Meshcapade FBX rigs). Joint names per the
    /// published model (vchoutas/smplx <c>joint_names.py</c>, Meshcapade wiki):
    /// <c>pelvis</c>, sided <c>hip→knee→ankle→foot</c> legs (the "hip" joint IS the thigh;
    /// "ankle" is the foot, "foot" is the toe region) and <c>collar→shoulder→elbow→wrist</c>
    /// arms ("shoulder" is the upper arm, "wrist" is the hand; the <c>hand</c> joint is a
    /// finger stub and stays unmapped). Both spellings occur in the wild: <c>left_hip</c>
    /// (model joints) and <c>L_Hip</c> with gendered FBX prefixes <c>m_avg_</c>/<c>f_avg_</c>
    /// (SMPL Unity/FBX rigs). No fingers — that is SMPL-X (<see cref="SmplX"/>), kept as a
    /// separate preset so a finger-less SMPL rig still reaches full optional coverage.
    /// </summary>
    public static Profile Smpl { get; } = BuildSmpl(withFingers: false);

    /// <summary>
    /// SMPL-X: the SMPL body joints (<see cref="Smpl"/>) plus articulated hands —
    /// <c>left_thumb1..3</c>/<c>left_index1..3</c>-style finger joints per
    /// vchoutas/smplx <c>joint_names.py</c> (jaw/eye joints carry no humanoid role).
    /// Evaluated before <see cref="Smpl"/> so it wins the tie on SMPL-X rigs (both score
    /// the body fully; only this one maps the fingers).
    /// </summary>
    public static Profile SmplX { get; } = BuildSmpl(withFingers: true);

    /// <summary>
    /// NVIDIA SOMA uniform-proportion skeleton (SOMA/SEED BVH exports, e.g.
    /// github.com/NVIDIA/soma-retargeter <c>assets/motions/bvh</c>). Mixamo-identical
    /// upper-body and finger names, but: spine is <c>Spine1→Spine2→Chest</c> (no plain
    /// "Spine"), neck is <c>Neck1→Neck2</c>, and the legs are <c>LeftLeg→LeftShin</c> —
    /// SOMA's <c>LeftLeg</c> is the THIGH (mixamo's is the calf), which is exactly why the
    /// mixamo preset must never claim these rigs.
    /// </summary>
    public static Profile SomaBvh { get; } = BuildSomaBvh();

    /// <summary>
    /// Classic BVH / Character-Studio-friendly naming (MotionBuilder "Export BVH to
    /// Character Studio" convention, ACCAD-style mocap BVHs): <c>Hips</c>,
    /// <c>Chest[2..4]</c> spine, arms <c>Collar→Shoulder→Elbow→Wrist</c> (the "Shoulder"
    /// is the upper arm) and legs <c>Hip→Knee→Ankle→Toe</c> (the sided "Hip" is the
    /// thigh). No fingers.
    /// </summary>
    public static Profile ClassicBvh { get; } = BuildClassicBvh();

    /// <summary>
    /// 3ds Max Character Studio Biped rigs: every bone is "&lt;BipedName&gt; &lt;Part&gt;"
    /// where the biped name defaults to <c>Bip01</c> (3ds Max ≤2009) / <c>Bip001</c>
    /// (2010+) per the Autodesk "Naming the Biped" documentation; some exporters mangle
    /// the spaces to underscores (<c>Bip01_L_Thigh</c>), hence the <c>^Bip\d+[ _]</c>
    /// namespace pattern (alias comparison is separator-insensitive, so "L UpperArm" and
    /// "L_UpperArm" normalize identically). Sided bones use a bare mid-name <c>L/R</c>:
    /// <c>L Clavicle→L UpperArm→L Forearm→L Hand</c> arms,
    /// <c>L Thigh→L Calf→L Foot→L Toe0</c> legs. Fingers are numbered chains
    /// <c>L Finger0..4</c> (0 = thumb) with phalanx segments <c>Finger01/Finger02</c>
    /// etc. (MotionBuilder's "3ds Max Biped Template" characterization maps exactly these
    /// names). The COM root <c>Bip01</c> itself, <c>Footsteps</c>, toe segments
    /// <c>Toe01/Toe02</c> and <c>HorseLink</c> carry no aliases and are never mapped.
    /// </summary>
    public static Profile Biped { get; } = BuildBiped();

    /// <summary>
    /// DAZ/Poser classic naming (Poser 4 era figures, DAZ Generation-4 V4/M4, Genesis 1/2,
    /// MakeHuman's "Poser/DAZ names" BVH export — verified against the local
    /// <c>dev/corpus/unknown_rigs/makehuman_cmu_03_03_dazNames.bvh</c>): camel-case bones
    /// with a lower-case <c>l</c>/<c>r</c> side prefix — <c>hip</c> (the translating
    /// root), <c>abdomen[→abdomen2]→chest</c> spine, <c>neck</c>, <c>head</c>,
    /// <c>lCollar→lShldr→lForeArm→lHand</c> arms, <c>lThigh→lShin→lFoot→lToe</c> legs and
    /// <c>lThumb1..3/lIndex1..3/lMid1..3/lRing1..3/lPinky1..3</c> fingers. The
    /// <c>l/rButtock</c> thigh helpers and eye bones carry no aliases and stay unmapped.
    /// DAZ Genesis 3/8/9 renamed the skeleton (<c>abdomenLower</c>, <c>lShldrBend</c>, …)
    /// and is NOT covered by this preset.
    /// </summary>
    public static Profile DazPoser { get; } = BuildDazPoser();

    /// <summary>
    /// Blender Rigify human rigs, per the metarig definition in the rigify add-on
    /// (<c>rigify/metarigs/human.py</c>) and the Blender manual's basic.human reference:
    /// the spine chain is <c>spine→spine.001..spine.006</c> where <c>spine</c> IS the
    /// pelvis/hips bone (it sits at the pelvis and parents the thighs), spine.001–003 are
    /// the torso, spine.004/005 the two neck bones (004 carries <see cref="BoneRole.Neck"/>,
    /// 005 stays unmapped — same policy as ActorCore's NeckTwist02) and spine.006 is the
    /// head. Limbs: <c>shoulder.L→upper_arm.L→forearm.L→hand.L</c>,
    /// <c>thigh.L→shin.L→foot.L→toe.L</c>; fingers <c>thumb.01.L..03.L</c> and
    /// <c>f_index/f_middle/f_ring/f_pinky.01.L..03.L</c>. The <c>^DEF-</c> namespace
    /// pattern also matches rigify's generated deform skeleton (<c>DEF-spine.001</c>,
    /// <c>DEF-upper_arm.L</c>, …); the segmented deform twins (<c>DEF-upper_arm.L.001</c>),
    /// <c>palm.*</c>, <c>pelvis.L/R</c>, <c>heel.02.L</c>, face bones and the generated
    /// ORG-/MCH-/control bones have no aliases and are never mapped.
    /// </summary>
    public static Profile Rigify { get; } = BuildRigify();

    /// <summary>
    /// VRoid Studio / VRM avatars (UniVRM exports): <c>J_Bip_&lt;side&gt;_&lt;Part&gt;</c>
    /// bones where side is <c>C</c> (center), <c>L</c> or <c>R</c> — the standard VRoid
    /// skeleton behind the VRM humanoid spec (vrm-c/vrm-specification, humanoid bone map):
    /// <c>J_Bip_C_Hips/Spine/Chest/UpperChest/Neck/Head</c>,
    /// <c>J_Bip_L_Shoulder→UpperArm→LowerArm→Hand</c>,
    /// <c>J_Bip_L_UpperLeg→LowerLeg→Foot→ToeBase</c>, fingers
    /// <c>J_Bip_L_Thumb1..3/Index1..3/Middle1..3/Ring1..3/Little1..3</c> ("Little" is the
    /// pinky, per the VRM littleProximal/Intermediate/Distal humanoid bones). Secondary
    /// physics/adjust bones (<c>J_Sec_*</c>, <c>J_Adj_*</c>) and the <c>Root</c> bone have
    /// no aliases and are never mapped.
    /// </summary>
    public static Profile Vrm { get; } = BuildVrm();

    /// <summary>
    /// Blender Auto-Rig Pro humanoid FBX exports — bone names verified empirically against
    /// the local user repro <c>dev/corpus/todo/Defenses.fbx</c> (the PunchPerfect family):
    /// <c>.x</c> suffix marks center bones, <c>.l/.r</c> the sides, and the exported limb
    /// deform bones carry the <c>_stretch</c> twin name — <c>root.x</c> is the hips
    /// (under a ground bone <c>root</c>), <c>spine_01.x→spine_02.x→spine_03.x</c>,
    /// <c>neck.x</c>, <c>head.x</c>, arms <c>shoulder.l→arm_stretch.l→forearm_stretch.l→
    /// hand.l</c> (plain "arm", NOT "upperarm"), legs <c>thigh_stretch.l→leg_stretch.l→
    /// foot.l→toes_01.l</c> ("leg" is the calf). Fingers keep Auto-Rig Pro's <c>c_</c>
    /// control prefix on the exported deform chain: <c>c_thumb1.l..3.l</c>,
    /// <c>c_index/c_middle/c_ring/c_pinky1.l..3.l</c>. Leftover finger-tip markers
    /// (<c>mixamorig:LeftHandIndex4</c> in the repro) and <c>root</c> have no aliases.
    /// </summary>
    public static Profile AutoRigPro { get; } = BuildAutoRigPro();

    /// <summary>All built-in presets, in detection order (first wins score ties — see
    /// <see cref="SmplX"/> vs <see cref="Smpl"/>).</summary>
    public static IReadOnlyList<Profile> All { get; } =
        new[]
        {
            Mixamo, ActorCoreCc, UeMannequin, RokokoBvh, SmplX, Smpl, SomaBvh, ClassicBvh,
            Biped, DazPoser, Rigify, Vrm, AutoRigPro,
        };

    // ---------------------------------------------------------------- mixamo

    private static Profile BuildMixamo()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hips" },
            [BoneRole.Spine0] = new[] { "Spine" },
            [BoneRole.Spine1] = new[] { "Spine1" },
            [BoneRole.Spine2] = new[] { "Spine2" },
            [BoneRole.Neck] = new[] { "Neck" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, nameSide) in Sides())
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}UpLeg" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Leg" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}ToeBase" };

            foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
            {
                aliases[Role($"{finger}Prox", roleSide)] = new[] { $"{nameSide}Hand{finger}1" };
                aliases[Role($"{finger}Mid", roleSide)] = new[] { $"{nameSide}Hand{finger}2" };
                aliases[Role($"{finger}Dist", roleSide)] = new[] { $"{nameSide}Hand{finger}3" };
            }
        }
        // Both ':' (FBX namespace) and '_' (namespace mangled by some exporters) forms occur
        // in the wild; some Mixamo downloads ship with no namespace at all, which still
        // matches because the aliases are the bare names.
        return new Profile("mixamo", new[] { "^mixamorig[0-9]*:", "^mixamorig[0-9]*_" }, aliases);
    }

    // ---------------------------------------------------------------- actorcore / cc

    private static Profile BuildActorCoreCc()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hip" },
            [BoneRole.Spine0] = new[] { "Waist" },
            [BoneRole.Spine1] = new[] { "Spine01" },
            [BoneRole.Spine2] = new[] { "Spine02" },
            [BoneRole.Neck] = new[] { "NeckTwist01" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var roleSide in new[] { "L", "R" })
        {
            var nameSide = roleSide; // CC bones use the bare side letter: CC_Base_L_Thigh.
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}_Clavicle" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}_Upperarm" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}_Forearm" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}_Hand" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}_Thigh" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}_Calf" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}_Foot" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}_ToeBase" };

            foreach (var (role, cc) in new[]
            {
                ("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Mid"), ("Ring", "Ring"), ("Pinky", "Pinky"),
            })
            {
                aliases[Role($"{role}Prox", roleSide)] = new[] { $"{nameSide}_{cc}1" };
                aliases[Role($"{role}Mid", roleSide)] = new[] { $"{nameSide}_{cc}2" };
                aliases[Role($"{role}Dist", roleSide)] = new[] { $"{nameSide}_{cc}3" };
            }
        }
        return new Profile("actorcore_cc", new[] { "^CC_Base_" }, aliases);
    }

    // ---------------------------------------------------------------- ue mannequin

    private static Profile BuildUeMannequin()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "pelvis" },
            [BoneRole.Spine0] = new[] { "spine_01" },
            [BoneRole.Spine1] = new[] { "spine_02" },
            [BoneRole.Spine2] = new[] { "spine_03" },
            [BoneRole.Spine3] = new[] { "spine_04" },
            [BoneRole.Spine4] = new[] { "spine_05" },
            [BoneRole.Neck] = new[] { "neck_01" },
            [BoneRole.Head] = new[] { "head" },
        };
        foreach (var (roleSide, s) in new[] { ("L", "l"), ("R", "r") })
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"clavicle_{s}" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"upperarm_{s}" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"lowerarm_{s}" };
            aliases[Role("Hand", roleSide)] = new[] { $"hand_{s}" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"thigh_{s}" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"calf_{s}" };
            aliases[Role("Foot", roleSide)] = new[] { $"foot_{s}" };
            aliases[Role("Toe", roleSide)] = new[] { $"ball_{s}" };

            foreach (var (role, ue) in new[]
            {
                ("Thumb", "thumb"), ("Index", "index"), ("Middle", "middle"), ("Ring", "ring"), ("Pinky", "pinky"),
            })
            {
                // UE5 mannequin adds metacarpals for the four fingers (not the thumb).
                if (role != "Thumb")
                    aliases[Role($"{role}Meta", roleSide)] = new[] { $"{ue}_metacarpal_{s}" };
                aliases[Role($"{role}Prox", roleSide)] = new[] { $"{ue}_01_{s}" };
                aliases[Role($"{role}Mid", roleSide)] = new[] { $"{ue}_02_{s}" };
                aliases[Role($"{role}Dist", roleSide)] = new[] { $"{ue}_03_{s}" };
            }
        }
        return new Profile("ue_mannequin", new string[0], aliases);
    }

    // ---------------------------------------------------------------- rokoko / xsens bvh

    private static Profile BuildRokokoBvh()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hips" },
            // Spine naming varies (Spine, Spine1..Spine4); ordered alias preference plus the
            // used-bone exclusion in the detector shifts the chain up when "Spine" is absent.
            [BoneRole.Spine0] = new[] { "Spine", "Spine1" },
            [BoneRole.Spine1] = new[] { "Spine1", "Spine2" },
            [BoneRole.Spine2] = new[] { "Spine2", "Spine3" },
            [BoneRole.Spine3] = new[] { "Spine3", "Spine4" },
            [BoneRole.Spine4] = new[] { "Spine4" },
            [BoneRole.Neck] = new[] { "Neck", "Neck1" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, nameSide) in Sides())
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder", $"{nameSide}Collar" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm", $"{nameSide}UpperArm" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm", $"{nameSide}LowerArm" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}UpLeg", $"{nameSide}Thigh", $"{nameSide}UpperLeg" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Leg", $"{nameSide}Shin", $"{nameSide}LowerLeg" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}Toe", $"{nameSide}ToeBase" };
        }
        return new Profile("rokoko_bvh", new string[0], aliases);
    }

    // ---------------------------------------------------------------- smpl / smpl-x

    private static Profile BuildSmpl(bool withFingers)
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Pelvis" },
            [BoneRole.Spine0] = new[] { "Spine1" },
            [BoneRole.Spine1] = new[] { "Spine2" },
            [BoneRole.Spine2] = new[] { "Spine3" },
            [BoneRole.Neck] = new[] { "Neck" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, abbr, word) in new[] { ("L", "L", "left"), ("R", "R", "right") })
        {
            // Both documented spellings per role: abbreviated FBX-rig names ("L_Hip") and
            // spelled model joint names ("left_hip"). Comparison is separator-insensitive.
            aliases[Role("Clavicle", roleSide)] = new[] { $"{abbr}_Collar", $"{word}_collar" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{abbr}_Shoulder", $"{word}_shoulder" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{abbr}_Elbow", $"{word}_elbow" };
            aliases[Role("Hand", roleSide)] = new[] { $"{abbr}_Wrist", $"{word}_wrist" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{abbr}_Hip", $"{word}_hip" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{abbr}_Knee", $"{word}_knee" };
            aliases[Role("Foot", roleSide)] = new[] { $"{abbr}_Ankle", $"{word}_ankle" };
            aliases[Role("Toe", roleSide)] = new[] { $"{abbr}_Foot", $"{word}_foot" };

            if (!withFingers)
                continue;

            // SMPL-X finger joints (left_index1..3 etc., per vchoutas/smplx joint_names.py).
            foreach (var finger in new[] { "thumb", "index", "middle", "ring", "pinky" })
            {
                var name = char.ToUpperInvariant(finger[0]) + finger[1..];
                aliases[Role($"{name}Prox", roleSide)] = new[] { $"{word}_{finger}1" };
                aliases[Role($"{name}Mid", roleSide)] = new[] { $"{word}_{finger}2" };
                aliases[Role($"{name}Dist", roleSide)] = new[] { $"{word}_{finger}3" };
            }
        }
        // Gendered SMPL FBX rigs prefix every bone (m_avg_L_Hip, f_avg_Pelvis).
        return new Profile(withFingers ? "smpl_x" : "smpl", new[] { "^m_avg_", "^f_avg_" }, aliases);
    }

    // ---------------------------------------------------------------- nvidia soma bvh

    private static Profile BuildSomaBvh()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hips" },
            [BoneRole.Spine0] = new[] { "Spine1" },
            [BoneRole.Spine1] = new[] { "Spine2" },
            [BoneRole.Spine2] = new[] { "Chest" },
            [BoneRole.Neck] = new[] { "Neck1" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, nameSide) in Sides())
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
            // SOMA's "Leg" is the thigh, "Shin" the calf — the decisive difference from
            // mixamo, where "Leg" is the calf under "UpLeg".
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}Leg" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Shin" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}ToeBase" };

            // Mixamo-style finger names; segment 4 ("LeftHandIndex4") and the *End markers
            // carry no role.
            foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
            {
                aliases[Role($"{finger}Prox", roleSide)] = new[] { $"{nameSide}Hand{finger}1" };
                aliases[Role($"{finger}Mid", roleSide)] = new[] { $"{nameSide}Hand{finger}2" };
                aliases[Role($"{finger}Dist", roleSide)] = new[] { $"{nameSide}Hand{finger}3" };
            }
        }
        return new Profile("soma_bvh", new string[0], aliases);
    }

    // ---------------------------------------------------------------- classic bvh

    private static Profile BuildClassicBvh()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hips" },
            [BoneRole.Spine0] = new[] { "Chest" },
            [BoneRole.Spine1] = new[] { "Chest2" },
            [BoneRole.Spine2] = new[] { "Chest3" },
            [BoneRole.Spine3] = new[] { "Chest4" },
            [BoneRole.Neck] = new[] { "Neck" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, nameSide) in Sides())
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Collar" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Shoulder" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}Elbow" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Wrist" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}Hip" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Knee" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Ankle" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}Toe" };
        }
        return new Profile("classic_bvh", new string[0], aliases);
    }

    // ---------------------------------------------------------------- 3ds max biped

    private static Profile BuildBiped()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Pelvis" },
            [BoneRole.Spine0] = new[] { "Spine" },
            [BoneRole.Spine1] = new[] { "Spine1" },
            [BoneRole.Spine2] = new[] { "Spine2" },
            [BoneRole.Spine3] = new[] { "Spine3" },
            [BoneRole.Neck] = new[] { "Neck" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            aliases[Role("Clavicle", s)] = new[] { $"{s} Clavicle" };
            aliases[Role("UpperArm", s)] = new[] { $"{s} UpperArm" };
            aliases[Role("LowerArm", s)] = new[] { $"{s} Forearm" };
            aliases[Role("Hand", s)] = new[] { $"{s} Hand" };
            aliases[Role("UpperLeg", s)] = new[] { $"{s} Thigh" };
            aliases[Role("LowerLeg", s)] = new[] { $"{s} Calf" };
            aliases[Role("Foot", s)] = new[] { $"{s} Foot" };
            aliases[Role("Toe", s)] = new[] { $"{s} Toe0" };

            // Numbered finger chains: Finger0 is the thumb; segment names append the
            // phalanx digit (Finger0 → Finger01 → Finger02, Finger1 → Finger11 → ...).
            foreach (var (finger, n) in new[]
            {
                ("Thumb", 0), ("Index", 1), ("Middle", 2), ("Ring", 3), ("Pinky", 4),
            })
            {
                aliases[Role($"{finger}Prox", s)] = new[] { $"{s} Finger{n}" };
                aliases[Role($"{finger}Mid", s)] = new[] { $"{s} Finger{n}1" };
                aliases[Role($"{finger}Dist", s)] = new[] { $"{s} Finger{n}2" };
            }
        }
        // "Bip01 "/"Bip001 " biped-name prefix; underscore form covers exporters that
        // mangle the spaces ("Bip01_L_Thigh"). The bare COM root "Bip01" is untouched by
        // the pattern (no trailing separator) and has no alias.
        return new Profile("biped", new[] { @"^Bip\d+[ _]" }, aliases);
    }

    // ---------------------------------------------------------------- daz / poser

    private static Profile BuildDazPoser()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "hip" },
            [BoneRole.Spine0] = new[] { "abdomen" },
            // Poser classic / DAZ Gen4 spine is abdomen→chest; DAZ Genesis 1/2 inserts
            // abdomen2. Ordered preference + used-bone exclusion handles both: without
            // abdomen2 the chest falls back to Spine1 and Spine2 stays unmapped.
            [BoneRole.Spine1] = new[] { "abdomen2", "chest" },
            [BoneRole.Spine2] = new[] { "chest" },
            [BoneRole.Neck] = new[] { "neck" },
            [BoneRole.Head] = new[] { "head" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            var p = s == "L" ? "l" : "r"; // lower-case side prefix: lShldr, rThigh
            aliases[Role("Clavicle", s)] = new[] { $"{p}Collar" };
            aliases[Role("UpperArm", s)] = new[] { $"{p}Shldr" };
            aliases[Role("LowerArm", s)] = new[] { $"{p}ForeArm" };
            aliases[Role("Hand", s)] = new[] { $"{p}Hand" };
            aliases[Role("UpperLeg", s)] = new[] { $"{p}Thigh" };
            aliases[Role("LowerLeg", s)] = new[] { $"{p}Shin" };
            aliases[Role("Foot", s)] = new[] { $"{p}Foot" };
            aliases[Role("Toe", s)] = new[] { $"{p}Toe" };

            foreach (var (role, daz) in new[]
            {
                ("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Mid"), ("Ring", "Ring"), ("Pinky", "Pinky"),
            })
            {
                aliases[Role($"{role}Prox", s)] = new[] { $"{p}{daz}1" };
                aliases[Role($"{role}Mid", s)] = new[] { $"{p}{daz}2" };
                aliases[Role($"{role}Dist", s)] = new[] { $"{p}{daz}3" };
            }
        }
        return new Profile("daz_poser", new string[0], aliases);
    }

    // ---------------------------------------------------------------- blender rigify

    private static Profile BuildRigify()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            // rigify's "spine" bone sits AT the pelvis and parents both thighs — it is
            // the hips, not a spine link (rigify/metarigs/human.py).
            [BoneRole.Hips] = new[] { "spine" },
            [BoneRole.Spine0] = new[] { "spine.001" },
            [BoneRole.Spine1] = new[] { "spine.002" },
            [BoneRole.Spine2] = new[] { "spine.003" },
            // spine.004 + spine.005 are the two neck bones, spine.006 the head;
            // spine.005 stays unmapped (same policy as ActorCore's NeckTwist02).
            [BoneRole.Neck] = new[] { "spine.004" },
            [BoneRole.Head] = new[] { "spine.006" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            aliases[Role("Clavicle", s)] = new[] { $"shoulder.{s}" };
            aliases[Role("UpperArm", s)] = new[] { $"upper_arm.{s}" };
            aliases[Role("LowerArm", s)] = new[] { $"forearm.{s}" };
            aliases[Role("Hand", s)] = new[] { $"hand.{s}" };
            aliases[Role("UpperLeg", s)] = new[] { $"thigh.{s}" };
            aliases[Role("LowerLeg", s)] = new[] { $"shin.{s}" };
            aliases[Role("Foot", s)] = new[] { $"foot.{s}" };
            aliases[Role("Toe", s)] = new[] { $"toe.{s}" };

            foreach (var (role, rigify) in new[]
            {
                ("Thumb", "thumb"), ("Index", "f_index"), ("Middle", "f_middle"),
                ("Ring", "f_ring"), ("Pinky", "f_pinky"),
            })
            {
                aliases[Role($"{role}Prox", s)] = new[] { $"{rigify}.01.{s}" };
                aliases[Role($"{role}Mid", s)] = new[] { $"{rigify}.02.{s}" };
                aliases[Role($"{role}Dist", s)] = new[] { $"{rigify}.03.{s}" };
            }
        }
        // The generated deform skeleton prefixes every deform bone with "DEF-"; its
        // segmented limb twins ("DEF-upper_arm.L.001") keep their numeric suffix after
        // stripping and therefore never collide with the whole-bone aliases.
        return new Profile("rigify", new[] { "^DEF-" }, aliases);
    }

    // ---------------------------------------------------------------- vroid / vrm

    private static Profile BuildVrm()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "J_Bip_C_Hips" },
            [BoneRole.Spine0] = new[] { "J_Bip_C_Spine" },
            [BoneRole.Spine1] = new[] { "J_Bip_C_Chest" },
            [BoneRole.Spine2] = new[] { "J_Bip_C_UpperChest" },
            [BoneRole.Neck] = new[] { "J_Bip_C_Neck" },
            [BoneRole.Head] = new[] { "J_Bip_C_Head" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            aliases[Role("Clavicle", s)] = new[] { $"J_Bip_{s}_Shoulder" };
            aliases[Role("UpperArm", s)] = new[] { $"J_Bip_{s}_UpperArm" };
            aliases[Role("LowerArm", s)] = new[] { $"J_Bip_{s}_LowerArm" };
            aliases[Role("Hand", s)] = new[] { $"J_Bip_{s}_Hand" };
            aliases[Role("UpperLeg", s)] = new[] { $"J_Bip_{s}_UpperLeg" };
            aliases[Role("LowerLeg", s)] = new[] { $"J_Bip_{s}_LowerLeg" };
            aliases[Role("Foot", s)] = new[] { $"J_Bip_{s}_Foot" };
            aliases[Role("Toe", s)] = new[] { $"J_Bip_{s}_ToeBase" };

            foreach (var (role, vrm) in new[]
            {
                ("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Middle"),
                ("Ring", "Ring"), ("Pinky", "Little"),
            })
            {
                aliases[Role($"{role}Prox", s)] = new[] { $"J_Bip_{s}_{vrm}1" };
                aliases[Role($"{role}Mid", s)] = new[] { $"J_Bip_{s}_{vrm}2" };
                aliases[Role($"{role}Dist", s)] = new[] { $"J_Bip_{s}_{vrm}3" };
            }
        }
        return new Profile("vrm", new string[0], aliases);
    }

    // ---------------------------------------------------------------- auto-rig pro

    private static Profile BuildAutoRigPro()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "root.x" },
            [BoneRole.Spine0] = new[] { "spine_01.x" },
            [BoneRole.Spine1] = new[] { "spine_02.x" },
            [BoneRole.Spine2] = new[] { "spine_03.x" },
            [BoneRole.Neck] = new[] { "neck.x" },
            [BoneRole.Head] = new[] { "head.x" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            var p = s == "L" ? "l" : "r";
            aliases[Role("Clavicle", s)] = new[] { $"shoulder.{p}" };
            aliases[Role("UpperArm", s)] = new[] { $"arm_stretch.{p}" };
            aliases[Role("LowerArm", s)] = new[] { $"forearm_stretch.{p}" };
            aliases[Role("Hand", s)] = new[] { $"hand.{p}" };
            aliases[Role("UpperLeg", s)] = new[] { $"thigh_stretch.{p}" };
            aliases[Role("LowerLeg", s)] = new[] { $"leg_stretch.{p}" };
            aliases[Role("Foot", s)] = new[] { $"foot.{p}" };
            aliases[Role("Toe", s)] = new[] { $"toes_01.{p}" };

            // Exported finger deform bones keep ARP's c_ control prefix (Defenses.fbx).
            foreach (var finger in new[] { "thumb", "index", "middle", "ring", "pinky" })
            {
                var role = char.ToUpperInvariant(finger[0]) + finger[1..];
                aliases[Role($"{role}Prox", s)] = new[] { $"c_{finger}1.{p}" };
                aliases[Role($"{role}Mid", s)] = new[] { $"c_{finger}2.{p}" };
                aliases[Role($"{role}Dist", s)] = new[] { $"c_{finger}3.{p}" };
            }
        }
        return new Profile("auto_rig_pro", new string[0], aliases);
    }

    // ---------------------------------------------------------------- helpers

    private static IEnumerable<(string RoleSide, string NameSide)> Sides()
    {
        yield return ("L", "Left");
        yield return ("R", "Right");
    }

    private static BoneRole Role(string baseName, string side)
        => System.Enum.Parse<BoneRole>(baseName + side);
}
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Target;

namespace HumanoidRetargeter.Solve;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>
/// Mirrors a solved TARGET-space clip across the target character's sagittal plane,
/// producing the left/right-swapped twin of an animation (e.g. a right-foot-lead walk from a
/// left-foot-lead one).
/// </summary>
/// <remarks>
/// <para><b>Mirror plane.</b> The plane through the rig-space origin spanned by the target
/// character's up and forward directions; its normal is the character's LATERAL axis,
/// computed from the target rig's rest geometry via <see cref="CharacterFrame"/> (never
/// hardcoded — an arbitrary target may be authored in any axis convention). When the
/// computed lateral lies on a coordinate axis up to float dirt (&lt; 1e-3 on the other two
/// components — true for every axis-aligned authored rig, including the s&amp;box citizen
/// rigs), it is snapped to that exact axis, which makes every reflection below an EXACT
/// sign-flip in IEEE arithmetic and therefore the whole mirror a bit-exact involution
/// (mirror ∘ mirror == identity, verified by test).</para>
/// <para><b>Math.</b> Let M = I − 2n̂n̂ᵀ be the reflection across the plane with unit normal
/// n̂. A world transform W = (R, t) maps to its mirror image by conjugation:
/// W′ = M̂ ∘ W ∘ M̂ (M̂ is its own inverse), giving rotation R′ = M·R·M and translation
/// t′ = M·t. For a quaternion q = (v, w), M·R·M is the rotation by the SAME angle about the
/// REFLECTED axis with REVERSED sense (a reflection flips orientation), i.e.
/// q′ = (2(n̂·v)n̂ − v, w); with n̂ = +X that is exactly q′ = (x, −y, −z, w), and positions
/// reflect as p′ = p − 2(n̂·p)n̂ = (−pₓ, p_y, p_z).</para>
/// <para><b>Locals, not worlds.</b> Because conjugation is a homomorphism
/// (M̂(AB)M̂ = (M̂AM̂)(M̂BM̂)) and world transforms are products of locals down the
/// hierarchy, mirroring every LOCAL transform and permuting bones by their L↔R partner is
/// exactly equivalent to mirroring the FK worlds — provided the partner permutation is
/// hierarchy-consistent (the partner's parent is the parent's partner), which is validated
/// and holds on structurally symmetric humanoid rigs. This avoids FK→inverse-FK float drift
/// entirely, which is what makes the double-mirror identity bit-exact.</para>
/// <para><b>Pairing.</b> Left/right bones are paired by the rig's canonical role annotations
/// first (UpperArmL ↔ UpperArmR, …); role-less bones (twist helpers, IK bones) fall back to
/// <c>_L</c>/<c>_R</c> name-token pairing (<c>arm_upper_L_twist0</c> ↔
/// <c>arm_upper_R_twist0</c>, <c>foot_L_IK_target</c> ↔ <c>foot_R_IK_target</c>); anything
/// unpaired (center bones: pelvis, spine, neck, head) mirrors in place, which reflects its
/// rotation across the sagittal plane and negates its lateral translation. IK-baked bones
/// should be re-baked from the mirrored body afterwards (<see cref="IkBoneBaker"/>) — the
/// pipeline does exactly that.</para>
/// </remarks>
public static class ClipMirror
{
    /// <summary>Maximum off-axis component magnitude below which the computed lateral axis is
    /// snapped to the exact coordinate axis (authored rigs are axis-aligned; the tiny rest
    /// asymmetries of a real mesh stay far below this).</summary>
    private const float AxisSnapTolerance = 1e-3f;

    /// <summary>
    /// Returns the mirrored copy of <paramref name="frames"/> (one new list, inputs
    /// untouched): per frame, bone i takes the conjugated local transform of its L↔R partner
    /// σ(i). See the class remarks for the math and pairing rules.
    /// </summary>
    /// <param name="frames">Solved per-frame local transforms (target skeleton bone order).</param>
    /// <param name="rig">The target rig (skeleton + roles) the frames belong to.</param>
    /// <exception cref="ArgumentException">Thrown when the rig maps a sided role without its
    /// counterpart, the pairing is not hierarchy-consistent, or the character frame is not
    /// computable — mirroring would silently produce garbage in those cases.</exception>
    public static List<XForm[]> Mirror(List<XForm[]> frames, TargetRig rig)
    {
        ArgumentNullException.ThrowIfNull(frames);
        ArgumentNullException.ThrowIfNull(rig);

        var skeleton = rig.Skeleton;
        var lateral = LateralAxis(rig);
        var pair = BuildPairing(rig);

        var result = new List<XForm[]>(frames.Count);
        foreach (var locals in frames)
        {
            if (locals.Length != skeleton.Count)
                throw new ArgumentException(
                    $"Frame has {locals.Length} bones but the target skeleton has {skeleton.Count}.",
                    nameof(frames));

            var mirrored = new XForm[locals.Length];
            for (var i = 0; i < locals.Length; i++)
            {
                var source = locals[pair[i]];
                mirrored[i] = new XForm(
                    ReflectPoint(source.Pos, lateral),
                    ReflectRotation(source.Rot, lateral));
            }
            result.Add(mirrored);
        }
        return result;
    }

    // ================================================================ mirror plane

    /// <summary>The unit mirror normal: the target character's lateral axis from rest
    /// geometry, snapped to an exact coordinate axis when within tolerance (bit-exact
    /// reflections, see class remarks).</summary>
    private static Vector3 LateralAxis(TargetRig rig)
    {
        Vector3 lateral;
        try
        {
            lateral = CharacterFrame.Compute(
                rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld).Lateral;
        }
        catch (ArgumentException e)
        {
            throw new ArgumentException(
                $"Cannot mirror: target character frame not computable ({e.Message}).", e);
        }

        var a = Vector3.Abs(lateral);
        if (a.Y <= AxisSnapTolerance && a.Z <= AxisSnapTolerance)
            return Vector3.UnitX;
        if (a.X <= AxisSnapTolerance && a.Z <= AxisSnapTolerance)
            return Vector3.UnitY;
        if (a.X <= AxisSnapTolerance && a.Y <= AxisSnapTolerance)
            return Vector3.UnitZ;
        return lateral; // general (non-axis-aligned) rig: exact involution is lost, math is not
    }

    /// <summary>p′ = p − 2(n̂·p)n̂. With a snapped axis this is an exact sign flip of one
    /// component (IEEE subtraction of representable values is exact).</summary>
    private static Vector3 ReflectPoint(Vector3 p, Vector3 n)
        => p - 2f * Vector3.Dot(p, n) * n;

    /// <summary>q′ = (2(n̂·v)n̂ − v, w): the conjugated rotation M·R·M — same angle, axis
    /// reflected, sense reversed. With n̂ = +X this is (x, −y, −z, w). Components are
    /// preserved exactly (no renormalization), keeping the double mirror bit-exact.</summary>
    private static Quaternion ReflectRotation(Quaternion q, Vector3 n)
    {
        var v = new Vector3(q.X, q.Y, q.Z);
        var reflected = 2f * Vector3.Dot(v, n) * n - v;
        return new Quaternion(reflected.X, reflected.Y, reflected.Z, q.W);
    }

    // ================================================================ L↔R pairing

    /// <summary>
    /// σ: bone → mirror partner (identity for center/unpaired bones). Roles pair first;
    /// role-less bones pair by <c>_L</c>/<c>_R</c> name tokens. Validated to be an involution
    /// consistent with the hierarchy (σ(parent(i)) == parent(σ(i))).
    /// </summary>
    private static int[] BuildPairing(TargetRig rig)
    {
        var skeleton = rig.Skeleton;
        var pair = new int[skeleton.Count];
        for (var i = 0; i < pair.Length; i++)
            pair[i] = i;

        for (var i = 0; i < skeleton.Count; i++)
        {
            if (rig.RoleOf(i) is { } role)
            {
                if (MirrorRole(role) is not { } mirroredRole)
                    continue; // center role: mirrors in place
                pair[i] = rig.BoneForRole(mirroredRole)
                    ?? throw new ArgumentException(
                        $"Cannot mirror: target rig maps role {role} ('{skeleton[i].Name}') "
                        + $"but not its counterpart {mirroredRole}.");
            }
            else
            {
                var partnerName = SwapSideTokens(skeleton[i].Name);
                if (partnerName is null)
                    continue; // no side token: center bone
                var partner = skeleton.IndexOf(partnerName);
                if (partner >= 0)
                    pair[i] = partner;
                // No partner bone: leave in place (e.g. an asymmetric prop bone) — its
                // rotation still mirrors across the sagittal plane.
            }
        }

        for (var i = 0; i < pair.Length; i++)
        {
            if (pair[pair[i]] != i)
                throw new ArgumentException(
                    $"Cannot mirror: bone pairing is not symmetric ('{skeleton[i].Name}' → "
                    + $"'{skeleton[pair[i]].Name}' → '{skeleton[pair[pair[i]]].Name}').");

            var parent = skeleton[i].ParentIndex;
            var partnerParent = skeleton[pair[i]].ParentIndex;
            var consistent = parent < 0
                ? partnerParent < 0
                : partnerParent == pair[parent];
            if (!consistent)
                throw new ArgumentException(
                    $"Cannot mirror: left/right pairing is not hierarchy-consistent — "
                    + $"'{skeleton[i].Name}' and partner '{skeleton[pair[i]].Name}' hang under "
                    + "non-mirrored parents.");
        }

        return pair;
    }

    /// <summary>UpperArmL → UpperArmR (and back); null for center roles. Every sided
    /// <see cref="BoneRole"/> ends in <c>L</c>/<c>R</c>; no center role does.</summary>
    private static BoneRole? MirrorRole(BoneRole role)
    {
        var name = role.ToString();
        var mirroredName = name[^1] switch
        {
            'L' => name[..^1] + "R",
            'R' => name[..^1] + "L",
            _ => null,
        };
        return mirroredName is not null && Enum.TryParse<BoneRole>(mirroredName, out var mirrored)
            ? mirrored
            : null;
    }

    /// <summary>Swaps <c>L</c>/<c>R</c> underscore-delimited name tokens
    /// (<c>foot_L_IK_target</c> → <c>foot_R_IK_target</c>); null when the name carries no
    /// side token.</summary>
    private static string? SwapSideTokens(string name)
    {
        var tokens = name.Split('_');
        for (var i = 0; i < tokens.Length; i++)
        {
            tokens[i] = tokens[i] switch
            {
                "L" => "R",
                "R" => "L",
                "l" => "r",
                "r" => "l",
                _ => tokens[i],
            };
        }
        var result = string.Join('_', tokens);
        return string.Equals(result, name, StringComparison.Ordinal) ? null : result;
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;

namespace HumanoidRetargeter.Solve;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>
/// Finger retargeting. Picks one of three strategies per finger chain:
/// <list type="number">
/// <item><b>1:1 absolute copy</b> (via the <c>transferOneToOne</c> callback into
/// <see cref="GeometricSolver"/>'s body path) when the source and target chains are
/// <i>geometrically identical</i> — same mapped role set, same canonical frames, same
/// normalized rest rotations. This is the same-rig round-trip case and is lossless (exact
/// identity, twist included).</item>
/// <item><b>Direction matching</b> when the phalanx counts match ordinally but the rigs
/// differ (the common cross-rig case, e.g. Mixamo Prox/Mid/Dist onto the s&amp;box finger
/// with its extra metacarpal — which keeps its rest local; a source metacarpal's rotation is
/// implicit in the proximal's absolute direction). Each target phalanx is swung — shortest
/// arc, rotation axis ⊥ the finger axis, hence <b>zero twist by construction</b> — so that its
/// segment direction matches the source phalanx's direction in character-frame coordinates
/// exactly. Curl and splay are both captured by the direction; the source's axial twist is
/// dropped (hinge-joint noise; copying it absolutely would read as roll through the
/// inter-phalanx canonical mismatch between rigs, measured up to ~12° on thumbs).</item>
/// <item><b>Proportional redistribution</b> when phalanx counts differ (e.g. a two-phalanx
/// source finger): per-phalanx local curls — swing-twist about the canonical hinge Y of
/// <c>λ_b = C_b⁻¹·(ΔR_prev⁻¹·ΔR_b)·C_b</c> — are summed over the source chain (metacarpal
/// included) and redistributed over the target phalanges proportional to rest segment
/// lengths; splay (metacarpal + proximal, swing-twist about canonical Z) goes 100% to the
/// target proximal; the X-twist residual is dropped.</item>
/// </list>
/// In every mode target world deltas rebuild hierarchically from the solved target hand:
/// <c>ΔR_i = ΔR_{i-1} · (C_i · λ_i · C_i⁻¹)</c>, then <c>W_i = ΔR_i · R_tgtNormRest,i</c>.
/// Instances are per-solve and not thread-safe.
/// </summary>
internal sealed class FingerSolver
{
    /// <summary>Two canonical frames / rest rotations within this angle count as identical
    /// (same-rig detection for the lossless 1:1 path); cross-rig differences are degrees.</summary>
    private const float SameRigToleranceRad = 1e-3f;

    private enum ChainMode
    {
        DirectionMatch,
        Proportional,
    }

    private readonly struct SourcePhalanx
    {
        public required int Slot { get; init; }
        public required Quaternion C { get; init; }
        public required Quaternion CInv { get; init; }
        public required bool TakesSplay { get; init; }
    }

    private readonly struct Recipient
    {
        public required int TgtBone { get; init; }
        public required Quaternion C { get; init; }
        public required Quaternion CInv { get; init; }
        public required Quaternion RestRot { get; init; }
        public required float Weight { get; init; }
        public required bool Splay { get; init; }
    }

    private sealed class Chain
    {
        public required ChainMode Mode { get; init; }
        public required int SrcHandSlot { get; init; }
        public required int TgtHandBone { get; init; }
        public required Quaternion TgtHandNormRestRotInv { get; init; }
        public required SourcePhalanx[] Sources { get; init; }
        public required Recipient[] Recipients { get; init; }
    }

    private readonly List<Chain> _chains;
    private readonly Quaternion _chrSrcInv;
    private readonly Quaternion _chrTgt;

    private FingerSolver(List<Chain> chains, Quaternion chrSrcInv, Quaternion chrTgt)
    {
        _chains = chains;
        _chrSrcInv = chrSrcInv;
        _chrTgt = chrTgt;
    }

    // ---------------------------------------------------------------- role tables

    private static readonly BoneRole[][] ChainRoles = BuildChainRoles();
    private static readonly HashSet<BoneRole> FingerRoleSet = ChainRoles.SelectMany(c => c.Skip(1)).ToHashSet();

    private static BoneRole[][] BuildChainRoles()
    {
        var chains = new List<BoneRole[]>();
        foreach (var side in new[] { "L", "R" })
        {
            foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
            {
                // Element 0 is the hand the chain hangs off; 1.. are Meta/Prox/Mid/Dist.
                chains.Add(new[]
                {
                    Enum.Parse<BoneRole>("Hand" + side),
                    Enum.Parse<BoneRole>(finger + "Meta" + side),
                    Enum.Parse<BoneRole>(finger + "Prox" + side),
                    Enum.Parse<BoneRole>(finger + "Mid" + side),
                    Enum.Parse<BoneRole>(finger + "Dist" + side),
                });
            }
        }
        return chains.ToArray();
    }

    /// <summary>True for the 40 per-finger segment roles (Meta/Prox/Mid/Dist × finger × side).</summary>
    public static bool IsFingerRole(BoneRole role) => FingerRoleSet.Contains(role);

    // ---------------------------------------------------------------- build

    /// <summary>
    /// Builds the per-chain plans. Geometrically identical chains are reported through
    /// <paramref name="transferOneToOne"/> instead of being planned here. Returns null when
    /// every mapped chain took that path (or none is mapped).
    /// </summary>
    public static FingerSolver? Build(
        MappingResult sourceMap,
        CanonicalFrames srcCanon,
        IReadOnlyList<XForm> srcNormRest,
        Func<BoneRole, int?> tgtBoneForRole,
        CanonicalFrames tgtCanon,
        IReadOnlyList<XForm> tgtNormRest,
        Quaternion chrSrcInv,
        Quaternion chrTgt,
        Func<int, int> registerSlot,
        Action<BoneRole> transferOneToOne)
    {
        var chains = new List<Chain>();
        foreach (var chainRoles in ChainRoles)
        {
            var handRole = chainRoles[0];
            var metaRole = chainRoles[1];
            var proxRole = chainRoles[2];
            var segments = chainRoles.Skip(1).ToArray();

            var srcRoles = segments
                .Where(r => sourceMap.RoleToBone.ContainsKey(r) && srcCanon.Has(r))
                .ToArray();
            var tgtRoles = segments
                .Where(r => tgtBoneForRole(r) is not null && tgtCanon.Has(r))
                .ToArray();
            if (srcRoles.Length == 0 || tgtRoles.Length == 0)
                continue;

            if (srcRoles.SequenceEqual(tgtRoles) && ChainsCoincide(
                srcRoles, sourceMap, srcCanon, srcNormRest, tgtBoneForRole, tgtCanon, tgtNormRest))
            {
                foreach (var role in srcRoles)
                    transferOneToOne(role);
                continue;
            }

            var srcPhalanges = srcRoles.Where(r => r != metaRole).ToArray();
            var tgtPhalanges = tgtRoles.Where(r => r != metaRole).ToArray();
            var recipientRoles = tgtPhalanges.Length > 0 ? tgtPhalanges : tgtRoles;
            var mode = srcPhalanges.Length == recipientRoles.Length && srcPhalanges.Length > 0
                ? ChainMode.DirectionMatch
                : ChainMode.Proportional;

            // Direction matching consumes only the non-meta phalanges (the metacarpal's
            // motion is implicit in the proximal's absolute direction); redistribution
            // decomposes every mapped source segment including the metacarpal.
            var sourceRolesUsed = mode == ChainMode.DirectionMatch ? srcPhalanges : srcRoles;
            var sources = sourceRolesUsed.Select(r =>
            {
                var c = srcCanon.WorldFrameOf(r);
                return new SourcePhalanx
                {
                    Slot = registerSlot(sourceMap.RoleToBone[r]),
                    C = c,
                    CInv = Quaternion.Conjugate(c),
                    TakesSplay = r == metaRole || r == proxRole,
                };
            }).ToArray();

            var weights = SegmentWeights(tgtRoles, recipientRoles, tgtBoneForRole, tgtNormRest);
            var recipients = recipientRoles.Select((r, i) =>
            {
                var bone = tgtBoneForRole(r)!.Value;
                var c = tgtCanon.WorldFrameOf(r);
                return new Recipient
                {
                    TgtBone = bone,
                    C = c,
                    CInv = Quaternion.Conjugate(c),
                    RestRot = tgtNormRest[bone].Rot,
                    Weight = weights[i],
                    Splay = i == 0,
                };
            }).ToArray();

            var tgtHand = tgtBoneForRole(handRole);
            chains.Add(new Chain
            {
                Mode = mode,
                SrcHandSlot = sourceMap.RoleToBone.TryGetValue(handRole, out var srcHand)
                    ? registerSlot(srcHand)
                    : -1,
                TgtHandBone = tgtHand ?? -1,
                TgtHandNormRestRotInv = tgtHand is int h
                    ? Quaternion.Conjugate(tgtNormRest[h].Rot)
                    : Quaternion.Identity,
                Sources = sources,
                Recipients = recipients,
            });
        }

        return chains.Count > 0 ? new FingerSolver(chains, chrSrcInv, chrTgt) : null;
    }

    /// <summary>Same-rig detection: every chain member's canonical frame and normalized rest
    /// rotation agree between source and target (within float noise). Only then is the 1:1
    /// absolute copy lossless.</summary>
    private static bool ChainsCoincide(
        BoneRole[] roles, MappingResult sourceMap, CanonicalFrames srcCanon,
        IReadOnlyList<XForm> srcNormRest, Func<BoneRole, int?> tgtBoneForRole,
        CanonicalFrames tgtCanon, IReadOnlyList<XForm> tgtNormRest)
    {
        foreach (var role in roles)
        {
            var srcBone = sourceMap.RoleToBone[role];
            var tgtBone = tgtBoneForRole(role)!.Value;
            if (MathQ.AngleBetween(srcCanon.WorldFrameOf(role), tgtCanon.WorldFrameOf(role)) > SameRigToleranceRad
                || MathQ.AngleBetween(srcNormRest[srcBone].Rot, tgtNormRest[tgtBone].Rot) > SameRigToleranceRad)
            {
                return false;
            }
        }
        return true;
    }

    /// <summary>Normalized rest segment lengths of the recipient phalanges (the proportional
    /// curl weights). The distal segment, having no chain child, is estimated as 0.8× its
    /// preceding segment.</summary>
    private static float[] SegmentWeights(
        BoneRole[] tgtRoles, BoneRole[] recipientRoles,
        Func<BoneRole, int?> tgtBoneForRole, IReadOnlyList<XForm> tgtNormRest)
    {
        var positions = tgtRoles.Select(r => tgtNormRest[tgtBoneForRole(r)!.Value].Pos).ToArray();
        var weights = new float[recipientRoles.Length];
        for (var i = 0; i < recipientRoles.Length; i++)
        {
            var j = Array.IndexOf(tgtRoles, recipientRoles[i]);
            weights[i] = j + 1 < positions.Length
                ? (positions[j + 1] - positions[j]).Length()
                : j > 0 ? 0.8f * (positions[j] - positions[j - 1]).Length() : 1f;
        }

        var sum = weights.Sum();
        if (sum <= 1e-6f)
            return Enumerable.Repeat(1f / weights.Length, weights.Length).ToArray();
        for (var i = 0; i < weights.Length; i++)
            weights[i] /= sum;
        return weights;
    }

    // ---------------------------------------------------------------- per frame

    /// <summary>
    /// Solves the planned chains for one frame. <paramref name="srcDeltas"/> holds the
    /// registered source world rotation deltas (from normalized rest); solved target world
    /// rotations are written into <paramref name="rot"/>/<paramref name="solved"/>. The target
    /// hands must already be solved (body pass runs first).
    /// </summary>
    public void Apply(Quaternion[] srcDeltas, bool[] solved, Quaternion[] rot)
    {
        foreach (var chain in _chains)
        {
            var acc = chain.TgtHandBone >= 0 && solved[chain.TgtHandBone]
                ? MathQ.Normalize(rot[chain.TgtHandBone] * chain.TgtHandNormRestRotInv)
                : Quaternion.Identity;

            if (chain.Mode == ChainMode.DirectionMatch)
                ApplyDirectionMatch(chain, srcDeltas, acc, solved, rot);
            else
                ApplyProportional(chain, srcDeltas, acc, solved, rot);
        }
    }

    private void ApplyDirectionMatch(
        Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)
    {
        for (var i = 0; i < chain.Recipients.Length; i++)
        {
            var sp = chain.Sources[i];
            var rc = chain.Recipients[i];

            // Source phalanx direction in character coords; re-expressed in the target world,
            // then relative to the already-reconstructed parent delta, then in the phalanx's
            // canonical frame — where the rest direction is unit X.
            var srcAbs = MathQ.Normalize(_chrSrcInv * srcDeltas[sp.Slot] * sp.C);
            var dirChr = Vector3.Transform(Vector3.UnitX, srcAbs);
            var dirTgtWorld = Vector3.Transform(dirChr, _chrTgt);
            var dirLocal = Vector3.Transform(dirTgtWorld, Quaternion.Conjugate(acc));
            var dirCanon = Vector3.Transform(dirLocal, rc.CInv);

            // Shortest-arc swing X -> dir: rotation axis ⊥ X, so it carries zero finger-axis
            // twist by construction.
            var swing = MathQ.FromTo(Vector3.UnitX, dirCanon);

            acc = MathQ.Normalize(acc * (rc.C * swing * rc.CInv));
            rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);
            solved[rc.TgtBone] = true;
        }
    }

    private static void ApplyProportional(
        Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)
    {
        // Decompose: total local curl over the chain, splay from metacarpal + proximal.
        var prev = chain.SrcHandSlot >= 0 ? srcDeltas[chain.SrcHandSlot] : Quaternion.Identity;
        float totalCurl = 0f, splay = 0f;
        foreach (var sp in chain.Sources)
        {
            var dr = srcDeltas[sp.Slot];
            var local = MathQ.Normalize(Quaternion.Conjugate(prev) * dr);
            var canon = MathQ.Normalize(sp.CInv * local * sp.C);

            MathQ.SwingTwist(canon, Vector3.UnitY, out var swing, out var curlQ);
            totalCurl += SignedAngle(curlQ, Vector3.UnitY);

            if (sp.TakesSplay)
            {
                MathQ.SwingTwist(swing, Vector3.UnitZ, out _, out var splayQ);
                splay += SignedAngle(splayQ, Vector3.UnitZ);
            }

            prev = dr;
        }

        foreach (var rc in chain.Recipients)
        {
            var mu = Quaternion.CreateFromAxisAngle(Vector3.UnitY, totalCurl * rc.Weight);
            if (rc.Splay)
                mu = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, splay) * mu;

            acc = MathQ.Normalize(acc * (rc.C * mu * rc.CInv));
            rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);
            solved[rc.TgtBone] = true;
        }
    }

    /// <summary>Signed rotation angle of an axis-aligned twist quaternion about
    /// <paramref name="axis"/>, wrapped to (−π, π].</summary>
    private static float SignedAngle(Quaternion twist, Vector3 axis)
    {
        var s = twist.X * axis.X + twist.Y * axis.Y + twist.Z * axis.Z;
        var angle = 2f * MathF.Atan2(s, twist.W);
        if (angle > MathF.PI)
            angle -= 2f * MathF.PI;
        else if (angle < -MathF.PI)
            angle += 2f * MathF.PI;
        return angle;
    }
}
using System.Collections.Generic;
using HumanoidRetargeter.Mapping;

namespace HumanoidRetargeter.Solve;

/// <summary>How a mapped role's rotation is transferred by the <see cref="GeometricSolver"/>.</summary>
public enum RoleTransferMode
{
    /// <summary>
    /// Absolute canonical-orientation matching: the target's animated chain direction is
    /// driven to <b>equal</b> the source's (in character-frame coordinates). Right for limbs
    /// and the spine — the pose IS the direction — but it also imposes the source rig's rest
    /// proportions/posture on roles whose rest directions legitimately differ between rigs.
    /// </summary>
    AbsoluteDirection,

    /// <summary>
    /// Rest-relative delta: the source's canonical-space rotation <i>delta from its own
    /// normalized rest</i> is replayed onto the <b>target's</b> normalized rest
    /// (<c>W_t(f) = C_t·ΔC(f)·C_t⁻¹·R_tgtNormRest</c> with
    /// <c>ΔC(f) = C_s⁻¹·ΔR(f)·C_s</c>). The target keeps its own rest carriage (shoulder
    /// line height, neck-base angle) and moves with the source. Identical to
    /// <see cref="AbsoluteDirection"/> when source and target rigs coincide.
    /// </summary>
    DeltaFromRest,

    /// <summary>
    /// Character-space delta: the source's world-rotation delta from its normalized rest is
    /// re-expressed in character coordinates and applied to the <b>target's</b> normalized
    /// rest (<c>W_t(f) = M·ΔR(f)·M⁻¹·R_tgtNormRest</c> with <c>M = Q_tgt·Q_src⁻¹</c>, the
    /// same character basis change <see cref="AbsoluteDirection"/> premultiplies). Like
    /// <see cref="DeltaFromRest"/> the target keeps its own rest carriage, but the delta
    /// keeps its <i>world</i> rotation axes instead of being remapped through the per-role
    /// canonical frames — the faithful replay when the rigs' rest chain directions diverge
    /// so far that canonical-axis remapping would tilt every rotation axis by that
    /// divergence (measured 23–44° on feet: CMU/ARP ankle anatomy vs the s&amp;box rig's
    /// steep ankle, where canonical remapping mis-pitched planted feet by up to 47°).
    /// Identical to the other modes when source and target rigs coincide.
    /// </summary>
    CharacterDeltaFromRest,
}

/// <summary>Options controlling a single retarget solve (one clip → one output clip).</summary>
public sealed class SolveOptions
{
    /// <summary>
    /// Default per-role transfer modes: shoulder girdle and neck carriage are
    /// <see cref="RoleTransferMode.DeltaFromRest"/> (each rig's clavicle line / neck-base
    /// direction is rig anatomy, not pose — absolute matching was measured to drag the
    /// s&amp;box shoulders 6–28° toward the source's flatter/lower clavicle line and is the
    /// "low shoulders, hunched neck" artifact), and feet are
    /// <see cref="RoleTransferMode.CharacterDeltaFromRest"/> (a rest foot→toe direction is
    /// ankle anatomy too — rigs diverge 11–44° from the s&amp;box rig's steep ankle, so
    /// absolute matching pitched planted feet up to 25° off flat, the "feet bent
    /// upward/inward" artifact; the character-space delta keeps the rotation's world axes,
    /// which canonical-frame remapping would tilt by that same divergence). The head is
    /// <see cref="RoleTransferMode.CharacterDeltaFromRest"/> for the same reason: the rest
    /// neck→head direction is head-joint-placement anatomy (measured 0–27° forward lean
    /// across neutral-rest rigs vs the s&amp;box rig's 25.5°), so the target keeps its own
    /// neutral skull attitude and replays the source's attitude <i>changes</i> — for the
    /// head this computes exactly what the previous virtual-frame absolute matching did.
    /// Two solver fallbacks adjust these defaults per rig pair: on a toe-less source the
    /// foot entries become <see cref="RoleTransferMode.DeltaFromRest"/> (virtual-foot
    /// fallback), and a source whose normalized rest head attitude is implausible as a
    /// neutral carriage (a posed bind — e.g. a chin-down/tilted fighting-stance rest,
    /// measured 40.7° forward / 16.9° lateral on such a rig where the delta replay read
    /// ~12° "looking up at an angle") switches the head to
    /// <see cref="RoleTransferMode.AbsoluteDirection"/> so the gaze follows the source
    /// absolutely instead of replaying deltas from a posed reference (see the
    /// <see cref="GeometricSolver"/> remarks for both). Everything else (limbs, spine,
    /// toes, fingers) stays absolute: there the worldspace direction IS the pose.
    /// </summary>
    public static IReadOnlyDictionary<BoneRole, RoleTransferMode> DefaultTransferModes { get; } =
        new Dictionary<BoneRole, RoleTransferMode>
        {
            [BoneRole.ClavicleL] = RoleTransferMode.DeltaFromRest,
            [BoneRole.ClavicleR] = RoleTransferMode.DeltaFromRest,
            [BoneRole.Neck] = RoleTransferMode.DeltaFromRest,
            [BoneRole.Head] = RoleTransferMode.CharacterDeltaFromRest,
            [BoneRole.FootL] = RoleTransferMode.CharacterDeltaFromRest,
            [BoneRole.FootR] = RoleTransferMode.CharacterDeltaFromRest,
        };

    /// <summary>
    /// Per-role transfer modes. Null (default) = <see cref="DefaultTransferModes"/> plus the
    /// solver's fallback heuristics (a toe-less source's virtual foot direction overrides
    /// the foot default to <see cref="RoleTransferMode.DeltaFromRest"/>, and a posed-rest
    /// source head overrides the head default to
    /// <see cref="RoleTransferMode.AbsoluteDirection"/> — see the
    /// <see cref="GeometricSolver"/> remarks). A non-null map REPLACES the defaults entirely
    /// and disables every fallback heuristic: each role uses exactly the mode in the map, and
    /// roles absent from it are <see cref="RoleTransferMode.AbsoluteDirection"/>. Pass an
    /// empty dictionary for fully absolute (legacy) behavior — API callers supplying a map
    /// opt out of all heuristics.
    /// </summary>
    public IReadOnlyDictionary<BoneRole, RoleTransferMode>? TransferModes { get; init; }

    /// <summary>
    /// Scale applied to the pelvis translation components perpendicular to the character up
    /// direction. Null (default) = automatic: target hip height / source hip height, both
    /// measured on the normalized rests.
    /// </summary>
    public float? HipScaleHorizontal { get; init; }

    /// <summary>
    /// Scale applied to the pelvis translation component along the character up direction.
    /// Null (default) = the same automatic hip-height ratio as <see cref="HipScaleHorizontal"/>.
    /// </summary>
    public float? HipScaleVertical { get; init; }

    /// <summary>Whether finger roles are transferred; when false, target finger bones keep
    /// their rest locals.</summary>
    public bool TransferFingers { get; init; } = true;

    /// <summary>Output clip name; null = the source clip's name.</summary>
    public string? ClipName { get; init; }

    /// <summary>Index of the source clip to retarget (<c>SourceScene.Clips</c>).</summary>
    public int ClipIndex { get; init; }
}
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Maths;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Cleanup;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>Tunables for the grounded-foot stance recalibration pass.</summary>
public sealed class FootGroundAlignOptions
{
    /// <summary>
    /// Dead zone (degrees): measured stance offsets at or below this are genuine planted
    /// articulation (heel-roll bias, natural lean — measured 2–4° on well-rested rigs and
    /// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.
    /// Only offsets beyond it are clearly rest-pose artifacts (measured 12–25° on the
    /// repro rig) and get recalibrated.
    /// </summary>
    public float MinCorrectionDeg { get; set; } = 8f;

    /// <summary>
    /// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE
    /// for the offset measurement. Plants beyond this are not standing on the sole (crawls,
    /// kneels, prone contact — measured 60–90° there) and are excluded; genuine rest-pose
    /// stance artifacts measure well below it (largest seen: 27°).
    /// </summary>
    public float MaxStanceDeviationDeg { get; set; } = 35f;
}

/// <summary>Per-foot results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignFootReport
{
    /// <summary>Plants that contributed to the stance measurement.</summary>
    public int StancePlants { get; set; }

    /// <summary>Plants excluded as non-stance (mean sole deviation beyond
    /// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/>).</summary>
    public int SkippedPlants { get; set; }

    /// <summary>Measured planted sole offset from the ground plane, degrees (0 when no
    /// stance plants exist).</summary>
    public float MeasuredOffsetDeg { get; set; }

    /// <summary>Foot correction applied to every frame, degrees (0 = inside the dead zone,
    /// nothing changed).</summary>
    public float AppliedFootDeg { get; set; }

    /// <summary>Toe correction applied to every frame, degrees.</summary>
    public float AppliedToeDeg { get; set; }
}

/// <summary>Results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignReport
{
    /// <summary>Left-foot results.</summary>
    public required FootGroundAlignFootReport Left { get; init; }

    /// <summary>Right-foot results.</summary>
    public required FootGroundAlignFootReport Right { get; init; }
}

/// <summary>
/// Grounded-foot stance recalibration: measures how far the foot's SOLE sits from the ground
/// plane while planted, and — when that offset is clearly a rest-pose artifact — rotates it
/// out with one constant per foot, applied to every frame of the clip.
/// </summary>
/// <remarks>
/// <para><b>Why a cleanup pass.</b> The solver transfers feet as rest-relative deltas
/// (<see cref="Solve.RoleTransferMode.CharacterDeltaFromRest"/>), so the target keeps its own
/// ankle anatomy — correct whenever the source's rest pose is a flat-footed stance (the delta
/// is then "deviation from standing"). Some rigs ship a NON-stance rest (measured: an
/// Auto-Rig-Pro export whose rest foot sits 12–25° from its planted stance), and that constant
/// offset rides into every frame of the replay — planted feet hover toe-down/heel-up. What a
/// stance actually looks like is animation evidence (planted phases), which a per-frame
/// solver cannot see, so the recalibration lives here.</para>
/// <para><b>Measurement.</b> Per foot: over every planted frame, the sole normal = rest up
/// carried by the foot's world delta from the target bind rest (whose feet stand on the
/// ground by construction); plants whose own mean normal sits beyond
/// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/> are excluded (crawl/kneel/prone
/// contact is not a stance). The pooled mean normal's deviation from up is the stance
/// offset.</para>
/// <para><b>Correction.</b> Offsets inside <see cref="FootGroundAlignOptions.MinCorrectionDeg"/>
/// are genuine articulation — nothing is changed (well-rested rigs and same-rig round trips
/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the
/// pooled normal back to up (pitch+roll only — yaw/toe-out is pose and follows the source)
/// premultiplies the foot's world rotation on EVERY frame: a rest artifact is constant, so
/// the fix is too — within-plant heel-roll, swing styling and frame-to-frame continuity are
/// preserved exactly, and no blending is needed. The toe then receives its own residual
/// constant measured on top of the corrected foot (it neither double-rotates with the foot
/// fix nor inherits the source toe's own rest artifact). Corrections rotate bones about
/// their own joints: ankle positions are untouched, so the pass composes freely with the
/// <see cref="FootPlant"/> position pinning (which preserves foot world rotations).</para>
/// <para><b>Plant intervals come from the caller</b> (the pipeline detects them on the
/// SOURCE clip via <see cref="FootPlant.DetectPlantIntervals"/> — ground truth, immune to
/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned
/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass
/// only when the source's normalized rest is implausible as a flat stance (toe at/above
/// ankle level or asymmetric feet — see <c>Retargeter.GroundAlignFeet</c>); on plausible
/// stance rests the solver's rest-relative transfer is already faithful and planted-sole
/// deviations are genuine articulation (boxing stances, heel rolls) that must not be
/// flattened.</para>
/// </remarks>
public static class FootGroundAlign
{
    /// <summary>Measures planted stance offsets and recalibrates feet whose offset is a
    /// rest-pose artifact; returns what was measured and done.</summary>
    /// <param name="frames">Per-frame local transforms (skeleton bone order); modified in place.</param>
    /// <param name="skeleton">Bone hierarchy the frames are expressed against; its bind rest
    /// is the flat-stance reference.</param>
    /// <param name="left">Left leg chain bone indices.</param>
    /// <param name="right">Right leg chain bone indices.</param>
    /// <param name="up">World up direction of the clip's space.</param>
    /// <param name="leftPlants">Left-foot plant intervals (frame indices into
    /// <paramref name="frames"/>; out-of-range parts are clamped/ignored).</param>
    /// <param name="rightPlants">Right-foot plant intervals.</param>
    /// <param name="options">Tunables; defaults used when null.</param>
    public static FootGroundAlignReport Apply(
        List<XForm[]> frames,
        SkeletonModel skeleton,
        FootChain left,
        FootChain right,
        Vector3 up,
        IReadOnlyList<FrameRange> leftPlants,
        IReadOnlyList<FrameRange> rightPlants,
        FootGroundAlignOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(frames);
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(left);
        ArgumentNullException.ThrowIfNull(right);
        ArgumentNullException.ThrowIfNull(leftPlants);
        ArgumentNullException.ThrowIfNull(rightPlants);

        options ??= new FootGroundAlignOptions();
        var report = new FootGroundAlignReport
        {
            Left = new FootGroundAlignFootReport(),
            Right = new FootGroundAlignFootReport(),
        };
        if (frames.Count == 0 || up.LengthSquared() < 1e-12f)
            return report;
        up = Vector3.Normalize(up);

        RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);
        RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);
        return report;
    }

    private static void RecalibrateFoot(
        List<XForm[]> frames, SkeletonModel skeleton, FootChain chain, Vector3 up,
        IReadOnlyList<FrameRange> plants, FootGroundAlignOptions options,
        FootGroundAlignFootReport report)
    {
        int n = frames.Count;
        var foot = chain.Ankle;
        var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);
        var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);

        // ---- measurement: pooled planted sole normal over the stance plants ----
        var pooled = Vector3.Zero;
        foreach (var plant in plants)
        {
            int start = Math.Max(plant.Start, 0);
            int end = Math.Min(plant.End, n - 1);
            if (start > end)
                continue;

            var plantSum = Vector3.Zero;
            for (int f = start; f <= end; f++)
            {
                var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;
                plantSum += Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));
            }
            if (plantSum.LengthSquared() < 1e-8f
                || Vector3.Dot(Vector3.Normalize(plantSum), up) < maxStanceCos)
            {
                report.SkippedPlants++; // not standing on the sole — crawl/kneel/toe contact
                continue;
            }
            report.StancePlants++;
            pooled += plantSum; // frame-count-weighted: longer stances dominate
        }
        if (pooled.LengthSquared() < 1e-8f)
            return;
        pooled = Vector3.Normalize(pooled);

        var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);
        report.MeasuredOffsetDeg = offsetDeg;
        if (offsetDeg <= options.MinCorrectionDeg)
            return; // genuine planted articulation — leave the transfer byte-faithful

        // ---- correction: one constant per foot, every frame ----
        var footFix = MathQ.FromTo(pooled, up);
        report.AppliedFootDeg = offsetDeg;

        // Toe residual measured on top of the corrected foot, same dead zone.
        var toeFix = Quaternion.Identity;
        if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
        {
            var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);
            var toePooled = Vector3.Zero;
            foreach (var plant in plants)
            {
                int start = Math.Max(plant.Start, 0);
                int end = Math.Min(plant.End, n - 1);
                for (int f = start; f <= end && f >= 0; f++)
                {
                    var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;
                    toePooled += Vector3.Transform(
                        up, MathQ.Normalize(footFix * toeRot * restToeRotInv));
                }
            }
            if (toePooled.LengthSquared() > 1e-8f)
            {
                toePooled = Vector3.Normalize(toePooled);
                var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);
                if (toeDeg > options.MinCorrectionDeg && Vector3.Dot(toePooled, up) >= maxStanceCos)
                {
                    toeFix = MathQ.FromTo(toePooled, up);
                    report.AppliedToeDeg = toeDeg;
                }
            }
        }

        for (int f = 0; f < n; f++)
            CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);
    }

    /// <summary>Premultiplies the foot's world rotation by the constant fix (the joint
    /// position is untouched — the rotation pivots the foot about its own head), then gives
    /// the toe its own residual on top of the corrected foot.</summary>
    private static void CorrectFrame(
        XForm[] locals, SkeletonModel skeleton, FootChain chain,
        Quaternion footFix, Quaternion toeFix)
    {
        var foot = chain.Ankle;
        var parent = skeleton[foot].ParentIndex;
        var parentRot = parent < 0
            ? Quaternion.Identity
            : FkUtil.BoneWorld(locals, skeleton, parent).Rot;

        var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);
        var newFootWorld = MathQ.Normalize(footFix * footWorld);
        locals[foot] = new XForm(
            locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));

        if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
        {
            // Desired toe world = toeFix ∘ footFix ∘ original world; re-derive its local
            // against the corrected foot so it does not double-rotate with the foot fix.
            var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);
            var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);
            locals[toe] = new XForm(
                locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));
        }
    }
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;

namespace HumanoidRetargeter.Formats.Bvh;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>Options for <see cref="BvhImporter.Import"/>.</summary>
public sealed class BvhImportOptions
{
    /// <summary>Fixed resampling rate for the motion data, frames per second.</summary>
    public float SampleFps { get; init; } = 30f;
}

/// <summary>
/// BVH (Biovision Hierarchy) → <see cref="SourceScene"/> importer.
/// </summary>
/// <remarks>
/// <para><b>Format conventions implemented</b> (verified against Blender's
/// <c>io_anim_bvh</c> importer, which is the project's ground-truth extractor):</para>
/// <list type="bullet">
/// <item><b>Rest pose:</b> each joint's rest local translation is its <c>OFFSET</c>; rest
/// rotation is identity (BVH stores no rest orientation).</item>
/// <item><b>Rotation channels:</b> the channel list order IS the rotation order. The listed
/// rotations apply left-to-right as intrinsic rotations, which in this library's
/// column-vector convention (<c>a * b</c> applies <c>b</c> first) is the product
/// <c>R = R_chan1 * R_chan2 * R_chan3</c> — e.g. <c>Zrotation Yrotation Xrotation</c> gives
/// <c>R = Rz * Ry * Rx</c>. This matches Blender, which builds
/// <c>Euler((x,y,z), reversed(channelOrder))</c> for the same matrix. Angles are degrees.</item>
/// <item><b>Position channels:</b> when a joint has any position channel, the channel values
/// REPLACE the joint's local translation (missing components are 0) — they are not added to
/// the <c>OFFSET</c>. This is Blender's behavior; in practice roots have OFFSET 0 so the two
/// readings only diverge on non-root position channels (e.g. Bandai-Namco exports).</item>
/// <item><b>End Sites:</b> synthesized as a channel-less leaf bone named
/// <c>"&lt;parent&gt;_end"</c> so chain tips keep their direction information (Blender instead
/// folds them into the parent bone's tail).</item>
/// </list>
/// <para><b>Units</b>: BVH files carry no unit declaration. Heuristic: compute the rest
/// skeleton height (max−min world Y over all joints); if it is &lt; 10 the file is assumed
/// to be in meters and all translations (offsets AND position channels, root included) are
/// scaled ×100 to centimeters, otherwise it is assumed to already be centimeters (×1).
/// Millimeter-scale files (height &gt; 400) are not special-cased — they are rare and
/// ambiguous against cm mocap of long ranges; <see cref="SourceScene.UnitScaleCm"/> records
/// whichever factor was applied for diagnostics.</para>
/// <para><b>Resampling</b>: motion frames are resampled from the file's <c>Frame Time</c>
/// grid onto <see cref="BvhImportOptions.SampleFps"/>. Each native frame's euler channels are
/// converted to a quaternion FIRST and bracketing frames are then slerped (positions lerped).
/// Interpolating raw euler angles across frames would mostly work at mocap densities
/// (30–120 fps, small per-frame deltas) but breaks down when an angle wraps ±180° between
/// frames; per-frame quaternion + slerp has no such failure mode, so that is what we do.</para>
/// <para><b>Axes</b>: BVH is conventionally Y-up / Z-forward / X-right. Native axes are
/// preserved (no conversion), matching the FBX importer's policy; the conventional axes are
/// recorded on the <see cref="SourceScene"/> (up = Y, front = Z, coord = X).</para>
/// </remarks>
public static class BvhImporter
{
    private const float MeterHeightThreshold = 10f;

    /// <summary>Parses BVH bytes and builds the source scene.</summary>
    /// <exception cref="FormatException">Malformed or truncated BVH.</exception>
    public static SourceScene Import(byte[] data, BvhImportOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(data);
        options ??= new BvhImportOptions();
        if (!(options.SampleFps > 0f) || !float.IsFinite(options.SampleFps))
            throw new ArgumentOutOfRangeException(nameof(options), "SampleFps must be positive.");

        var cursor = new TokenCursor(Encoding.UTF8.GetString(data));

        // ---- HIERARCHY -----------------------------------------------------------------
        cursor.ExpectKeyword("HIERARCHY");
        var joints = new List<Joint>();
        int channelCount = 0;
        if (!cursor.PeekIs("ROOT"))
            throw new FormatException("BVH: expected ROOT after HIERARCHY.");
        while (cursor.PeekIs("ROOT")) // multiple roots are out of spec but harmless to accept
        {
            cursor.Next();
            ParseJoint(cursor, joints, parent: -1, ref channelCount);
        }

        // ---- MOTION ---------------------------------------------------------------------
        cursor.ExpectKeyword("MOTION");
        cursor.ExpectKeyword("FRAMES:");
        int frameCount = cursor.NextInt();
        if (frameCount < 0)
            throw new FormatException($"BVH: negative frame count {frameCount}.");
        cursor.ExpectKeyword("FRAME");
        cursor.ExpectKeyword("TIME:");
        float frameTime = cursor.NextFloat();
        if (!(frameTime > 0f) || !float.IsFinite(frameTime))
            throw new FormatException($"BVH: invalid Frame Time {frameTime}.");

        var motion = new float[frameCount][];
        for (int f = 0; f < frameCount; f++)
        {
            var row = new float[channelCount];
            for (int c = 0; c < channelCount; c++)
                row[c] = cursor.NextFloat();
            motion[f] = row;
        }

        // ---- units heuristic --------------------------------------------------------------
        float unitScale = HeuristicUnitScale(joints);

        // ---- skeleton ----------------------------------------------------------------------
        var defs = new List<BoneDefinition>(joints.Count);
        foreach (var j in joints)
        {
            defs.Add(new BoneDefinition(
                j.Name,
                j.Parent < 0 ? null : joints[j.Parent].Name,
                new XForm(j.Offset * unitScale, Quaternion.Identity)));
        }
        var skeleton = Skeleton.Skeleton.Create(defs);

        // ---- clip ----------------------------------------------------------------------------
        var clips = new List<Clip>();
        if (frameCount > 0)
            clips.Add(ResampleClip(joints, skeleton, motion, frameTime, unitScale, options.SampleFps));

        // BVH conventional axes: Y-up (1), Z-front (2), X-coord (0) — recorded, not converted.
        return new SourceScene(
            skeleton, clips, unitScale,
            upAxis: 1, upAxisSign: 1,
            frontAxis: 2, frontAxisSign: 1,
            coordAxis: 0, coordAxisSign: 1,
            originalUpAxis: -1);
    }

    // =====================================================================================
    // hierarchy parsing
    // =====================================================================================

    private sealed class Joint
    {
        public required string Name;
        public required int Parent;          // index into the joint list, -1 for roots
        public Vector3 Offset;               // raw file units
        public int PosX = -1, PosY = -1, PosZ = -1;            // motion column per position axis
        public List<(int Axis, int Column)> Rot = new();        // rotation channels in file order
        public bool HasPos => PosX >= 0 || PosY >= 0 || PosZ >= 0;
    }

    private static void ParseJoint(TokenCursor cursor, List<Joint> joints, int parent, ref int channelCount)
    {
        // Joint name: tokens up to '{', joined with '_' (mirrors Blender's handling of
        // names containing spaces).
        var nameParts = new List<string>();
        while (!cursor.PeekIs("{"))
        {
            if (cursor.AtEnd)
                throw new FormatException("BVH: unexpected end of file in joint name.");
            nameParts.Add(cursor.Next());
        }
        if (nameParts.Count == 0)
            throw new FormatException("BVH: joint with no name.");
        string name = UniqueName(string.Join('_', nameParts), joints);

        cursor.ExpectKeyword("{");
        cursor.ExpectKeyword("OFFSET");
        var joint = new Joint { Name = name, Parent = parent };
        joint.Offset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());
        int index = joints.Count;
        joints.Add(joint);

        if (cursor.PeekIs("CHANNELS"))
        {
            cursor.Next();
            int n = cursor.NextInt();
            if (n < 0 || n > 6)
                throw new FormatException($"BVH: joint '{name}' has invalid channel count {n}.");
            for (int i = 0; i < n; i++)
            {
                string channel = cursor.Next();
                int column = channelCount++;
                switch (channel.ToUpperInvariant())
                {
                    case "XPOSITION": joint.PosX = column; break;
                    case "YPOSITION": joint.PosY = column; break;
                    case "ZPOSITION": joint.PosZ = column; break;
                    case "XROTATION": joint.Rot.Add((0, column)); break;
                    case "YROTATION": joint.Rot.Add((1, column)); break;
                    case "ZROTATION": joint.Rot.Add((2, column)); break;
                    default:
                        throw new FormatException($"BVH: unknown channel '{channel}' on joint '{name}'.");
                }
            }
        }

        while (!cursor.PeekIs("}"))
        {
            if (cursor.AtEnd)
                throw new FormatException($"BVH: unexpected end of file inside joint '{name}'.");
            if (cursor.PeekIs("JOINT"))
            {
                cursor.Next();
                ParseJoint(cursor, joints, index, ref channelCount);
            }
            else if (cursor.PeekIs("END"))
            {
                cursor.Next();
                cursor.ExpectKeyword("SITE");
                while (!cursor.PeekIs("{")) // a name after "End Site" is out of spec; skip it
                {
                    if (cursor.AtEnd)
                        throw new FormatException("BVH: unexpected end of file in End Site.");
                    cursor.Next();
                }
                cursor.ExpectKeyword("{");
                cursor.ExpectKeyword("OFFSET");
                var endOffset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());
                cursor.ExpectKeyword("}");

                // Synthesize a channel-less leaf so the chain tip's direction is kept.
                joints.Add(new Joint
                {
                    Name = UniqueName(name + "_end", joints),
                    Parent = index,
                    Offset = endOffset,
                });
            }
            else
            {
                throw new FormatException(
                    $"BVH: unexpected token '{cursor.Next()}' inside joint '{name}'.");
            }
        }
        cursor.ExpectKeyword("}");
    }

    private static string UniqueName(string name, List<Joint> joints)
    {
        bool Taken(string candidate)
        {
            foreach (var j in joints)
                if (string.Equals(j.Name, candidate, StringComparison.Ordinal))
                    return true;
            return false;
        }

        if (!Taken(name))
            return name;
        for (int i = 1; ; i++)
        {
            string candidate = $"{name}#{i}";
            if (!Taken(candidate))
                return candidate;
        }
    }

    // =====================================================================================
    // units
    // =====================================================================================

    /// <summary>
    /// Meters-vs-centimeters heuristic: rest skeleton height (max−min world Y over all
    /// joints, end sites included) &lt; 10 → meters → ×100; otherwise centimeters → ×1.
    /// </summary>
    private static float HeuristicUnitScale(List<Joint> joints)
    {
        Span<float> worldY = joints.Count <= 256 ? stackalloc float[joints.Count] : new float[joints.Count];
        float min = float.MaxValue, max = float.MinValue;
        for (int i = 0; i < joints.Count; i++)
        {
            worldY[i] = (joints[i].Parent < 0 ? 0f : worldY[joints[i].Parent]) + joints[i].Offset.Y;
            min = MathF.Min(min, worldY[i]);
            max = MathF.Max(max, worldY[i]);
        }
        float height = max - min;
        return height > 0f && height < MeterHeightThreshold ? 100f : 1f;
    }

    // =====================================================================================
    // motion sampling
    // =====================================================================================

    /// <summary>
    /// Decodes every native frame to per-joint local transforms (quaternions built per frame
    /// from the joint's channel order), then resamples onto the <paramref name="fps"/> grid —
    /// positions lerped, rotations slerped between the bracketing native frames.
    /// </summary>
    private static Clip ResampleClip(
        List<Joint> joints, Skeleton.Skeleton skeleton, float[][] motion,
        float frameTime, float unitScale, float fps)
    {
        int jointCount = joints.Count;
        int nativeCount = motion.Length;

        // Joint order may differ from skeleton bone order (topological sort) — map.
        var toSkeleton = new int[jointCount];
        for (int i = 0; i < jointCount; i++)
            toSkeleton[i] = skeleton.IndexOf(joints[i].Name);

        // Native-frame locals.
        var native = new XForm[nativeCount][];
        for (int f = 0; f < nativeCount; f++)
        {
            var row = motion[f];
            var locals = new XForm[jointCount];
            for (int i = 0; i < jointCount; i++)
                locals[i] = EvaluateLocal(joints[i], row, unitScale);
            native[f] = locals;
        }

        double duration = (nativeCount - 1) * (double)frameTime;
        int outCount = Math.Max(1, (int)Math.Round(duration * fps) + 1);

        var frames = new List<XForm[]>(outCount);
        for (int f = 0; f < outCount; f++)
        {
            double s = f / (double)fps / frameTime; // position on the native frame grid
            int i0 = Math.Clamp((int)Math.Floor(s), 0, nativeCount - 1);
            int i1 = Math.Min(i0 + 1, nativeCount - 1);
            float u = Math.Clamp((float)(s - i0), 0f, 1f);

            var frame = new XForm[skeleton.Count];
            var a = native[i0];
            var b = native[i1];
            for (int i = 0; i < jointCount; i++)
            {
                frame[toSkeleton[i]] = new XForm(
                    Vector3.Lerp(a[i].Pos, b[i].Pos, u),
                    MathQ.Normalize(Quaternion.Slerp(a[i].Rot, b[i].Rot, u)));
            }
            frames.Add(frame);
        }

        // NativeFps records the file's authored frame rate (1 / FrameTime): external frame
        // ranges (Unity .meta clipAnimations) are expressed in it.
        float nativeFps = frameTime > 0f ? (float)(1.0 / frameTime) : fps;
        return new Clip("motion", fps, looping: false, frames, nativeFps);
    }

    /// <summary>One joint's local transform from one motion row (see class remarks).</summary>
    private static XForm EvaluateLocal(Joint joint, float[] row, float unitScale)
    {
        // Position channels replace the OFFSET; absent channels (or no position channels at
        // all) fall back per Blender's semantics described in the class remarks.
        Vector3 pos = joint.HasPos
            ? new Vector3(
                joint.PosX >= 0 ? row[joint.PosX] : 0f,
                joint.PosY >= 0 ? row[joint.PosY] : 0f,
                joint.PosZ >= 0 ? row[joint.PosZ] : 0f)
            : joint.Offset;

        // R = R_chan1 * R_chan2 * R_chan3 (column-vector convention; degrees in the file).
        var rot = Quaternion.Identity;
        foreach (var (axis, column) in joint.Rot)
        {
            float radians = row[column] * (MathF.PI / 180f);
            var axisVector = axis switch
            {
                0 => Vector3.UnitX,
                1 => Vector3.UnitY,
                _ => Vector3.UnitZ,
            };
            rot *= Quaternion.CreateFromAxisAngle(axisVector, radians);
        }

        return new XForm(pos * unitScale, MathQ.Normalize(rot));
    }

    // =====================================================================================
    // tokenizer
    // =====================================================================================

    /// <summary>Whitespace token stream over the BVH text (BVH is line-format agnostic).</summary>
    private sealed class TokenCursor
    {
        private readonly string[] _tokens;
        private int _pos;

        public TokenCursor(string text)
            => _tokens = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);

        public bool AtEnd => _pos >= _tokens.Length;

        public bool PeekIs(string keywordUpper)
            => _pos < _tokens.Length &&
               string.Equals(_tokens[_pos], keywordUpper, StringComparison.OrdinalIgnoreCase);

        public string Next()
        {
            if (AtEnd)
                throw new FormatException("BVH: unexpected end of file.");
            return _tokens[_pos++];
        }

        public void ExpectKeyword(string keywordUpper)
        {
            string token = Next();
            if (!string.Equals(token, keywordUpper, StringComparison.OrdinalIgnoreCase))
                throw new FormatException($"BVH: expected '{keywordUpper}', found '{token}'.");
        }

        public int NextInt()
        {
            string token = Next();
            if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
                throw new FormatException($"BVH: expected an integer, found '{token}'.");
            return value;
        }

        public float NextFloat()
        {
            string token = Next();
            if (!float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value) ||
                !float.IsFinite(value))
                throw new FormatException($"BVH: expected a number, found '{token}'.");
            return value;
        }
    }
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using HumanoidRetargeter.Skeleton;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Formats.Dmx;

/// <summary>Options for <see cref="DmxWriter.Write"/>.</summary>
public sealed class DmxWriteOptions
{
    /// <summary>Model/clip name written into the DmeModel element (e.g. the sequence name).</summary>
    public string Name { get; set; } = "";

    /// <summary>Free-form provenance note written as the DmeDCCMakefile source name
    /// (fbx2dmx writes the source .fbx path here).</summary>
    public string SourceNote { get; set; } = "";

    /// <summary>When true (default, matching fbx2dmx output) the file declares a Y-up axis
    /// system; when false it declares Z-up. Data is written as-is either way.</summary>
    public bool UpAxisY { get; set; } = true;

    /// <summary>
    /// Skeleton bone indices that get NO DmeChannel pair: the bones keep their DmeJoint and
    /// bind (rest) transform, but no animation channels are written for them — the engine then
    /// drives them itself (e.g. ConstraintDriven twist/helper bones, design §3). Null (default)
    /// writes channels for every bone.
    /// </summary>
    public IReadOnlySet<int>? ChannelExcludedBones { get; set; }
}

/// <summary>
/// Writes an animation DMX in <c>keyvalues2_noids</c> text encoding, replicating the exact
/// element/attribute shape of fbx2dmx output (authoritative reference:
/// <c>dev/m0/ref_idlepose.dmx</c>): a root DmElement holding an inline DmeModel (joint GUID
/// refs + bind base state), a top-level DmeAnimationList with one DmeChannelsClip carrying a
/// position and an orientation channel per bone, and top-level DmeTransform/DmeJoint elements
/// the channels and joint lists reference by GUID. Output is fully deterministic: GUIDs are
/// MD5-derived from the options name and an element path, and export tags use fixed
/// placeholder strings.
/// </summary>
public static class DmxWriter
{
    private const string Header = "<!-- dmx encoding keyvalues2_noids 4 format model 22 -->";

    /// <summary>
    /// Serializes <paramref name="clip"/> on <paramref name="skeleton"/> to DMX text.
    /// Frames must contain one local transform per bone in skeleton order.
    /// </summary>
    /// <exception cref="ArgumentException">Thrown when the clip is empty or a frame's bone
    /// count does not match the skeleton.</exception>
    public static string Write(SkeletonModel skeleton, Clip clip, DmxWriteOptions options)
    {
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(clip);
        ArgumentNullException.ThrowIfNull(options);

        if (clip.FrameCount == 0)
            throw new ArgumentException("Clip has no frames.", nameof(clip));
        for (var f = 0; f < clip.FrameCount; f++)
        {
            if (clip.Frames[f].Length != skeleton.Count)
                throw new ArgumentException(
                    $"Frame {f} has {clip.Frames[f].Length} bone transforms, skeleton has {skeleton.Count}.",
                    nameof(clip));
        }

        var w = new Emitter();
        var animListGuid = GuidString(options.Name, "animationList");
        var jointGuids = new string[skeleton.Count];
        var transformGuids = new string[skeleton.Count];
        for (var i = 0; i < skeleton.Count; i++)
        {
            jointGuids[i] = GuidString(options.Name, "joint:" + skeleton[i].Name);
            transformGuids[i] = GuidString(options.Name, "transform:" + skeleton[i].Name);
        }

        w.Raw(Header);

        // ---- root DmElement -------------------------------------------------
        w.BeginTopLevel("DmElement");
        w.Attr("name", "string", "root");

        w.BeginInlineAttr("skeleton", "DmeModel");
        w.Attr("name", "string", options.Name);
        w.BeginInlineAttr("transform", "DmeTransform");
        w.Attr("position", "vector3", "0 0 0");
        w.Attr("orientation", "quaternion", "0 0 0 1");
        w.EndInlineAttr();
        w.Attr("shape", "element", "");
        w.Attr("visible", "bool", "1");

        w.BeginArray("children");
        var roots = new List<int>();
        for (var i = 0; i < skeleton.Count; i++)
        {
            if (skeleton[i].ParentIndex < 0)
                roots.Add(i);
        }
        for (var r = 0; r < roots.Count; r++)
            w.ElementRef(jointGuids[roots[r]], last: r == roots.Count - 1);
        w.EndArray();

        w.BeginArray("jointList");
        for (var i = 0; i < skeleton.Count; i++)
            w.ElementRef(jointGuids[i], last: i == skeleton.Count - 1);
        w.EndArray();

        w.BeginArray("baseStates");
        w.BeginArrayElement("DmeTransformList");
        w.Attr("name", "string", "bind");
        w.BeginArray("transforms");
        for (var i = 0; i < skeleton.Count; i++)
        {
            w.BeginArrayElement("DmeTransform");
            w.Attr("name", "string", skeleton[i].Name);
            w.Attr("position", "vector3", Vec(skeleton[i].RestLocal));
            w.Attr("orientation", "quaternion", Quat(skeleton[i].RestLocal));
            w.EndArrayElement(last: i == skeleton.Count - 1);
        }
        w.EndArray();
        w.EndArrayElement(last: true);
        w.EndArray();

        w.Attr("upAxis", "string", options.UpAxisY ? "Y" : "Z");
        w.BeginInlineAttr("axisSystem", "DmeAxisSystem");
        w.Attr("upAxis", "int", options.UpAxisY ? "2" : "3");
        w.Attr("forwardParity", "int", "2");
        w.Attr("coordSys", "int", "0");
        w.EndInlineAttr();
        w.Attr("animationList", "element", animListGuid);
        w.EndInlineAttr(); // skeleton DmeModel

        w.BeginInlineAttr("makefile", "DmeDCCMakefile");
        w.Attr("name", "string", "makefile");
        w.BeginArray("sources");
        w.BeginArrayElement("DmeSource");
        w.Attr("name", "string", options.SourceNote);
        w.EndArrayElement(last: true);
        w.EndArray();
        w.EndInlineAttr();

        // Deterministic placeholders — never wall-clock/user data, so output is reproducible.
        w.BeginInlineAttr("exportTags", "DmeExportTags");
        w.Attr("name", "string", "exportTags");
        w.Attr("date", "string", "2026/01/01");
        w.Attr("time", "string", "12:00:00 am");
        w.Attr("user", "string", "retargeter");
        w.Attr("machine", "string", "retargeter");
        w.Attr("app", "string", "humanoid-retargeter");
        w.Attr("appVersion", "string", "1.0");
        w.Attr("cmdLine", "string", "humanoid-retargeter");
        w.Attr("pwd", "string", "");
        w.EndInlineAttr();

        w.Attr("animationList", "element", animListGuid);
        w.EndTopLevel();

        // ---- DmeAnimationList ----------------------------------------------
        w.BeginTopLevel("DmeAnimationList");
        w.Attr("id", "elementid", animListGuid);
        w.Attr("name", "string", "anim");
        w.BeginArray("animations");
        w.BeginArrayElement("DmeChannelsClip");
        w.Attr("name", "string", "anim");

        w.BeginInlineAttr("timeFrame", "DmeTimeFrame");
        w.Attr("start", "time", Time(0.0));
        w.Attr("duration", "time", Time((clip.FrameCount - 1) / (double)clip.Fps));
        w.Attr("offset", "time", Time(0.0));
        w.Attr("scale", "float", "1");
        w.EndInlineAttr();

        w.Attr("color", "color", "0 0 0 0");
        w.Attr("text", "string", "");
        w.Attr("mute", "bool", "0");
        w.BeginArray("trackGroups");
        w.EndArray();
        w.Attr("displayScale", "float", "1");

        var channelBones = new List<int>(skeleton.Count);
        for (var i = 0; i < skeleton.Count; i++)
        {
            if (options.ChannelExcludedBones is null || !options.ChannelExcludedBones.Contains(i))
                channelBones.Add(i);
        }

        w.BeginArray("channels");
        for (var n = 0; n < channelBones.Count; n++)
        {
            var i = channelBones[n];
            WriteChannel(w, skeleton, clip, i, transformGuids[i], position: true, last: false);
            WriteChannel(w, skeleton, clip, i, transformGuids[i], position: false,
                last: n == channelBones.Count - 1);
        }
        w.EndArray();

        w.Attr("frameRate", "int",
            ((int)MathF.Round(clip.Fps)).ToString(CultureInfo.InvariantCulture));
        w.EndArrayElement(last: true);
        w.EndArray();
        w.EndTopLevel();

        // ---- top-level channel-target DmeTransforms (rest values) -----------
        for (var i = 0; i < skeleton.Count; i++)
        {
            w.BeginTopLevel("DmeTransform");
            w.Attr("id", "elementid", transformGuids[i]);
            w.Attr("name", "string", skeleton[i].Name);
            w.Attr("position", "vector3", Vec(skeleton[i].RestLocal));
            w.Attr("orientation", "quaternion", Quat(skeleton[i].RestLocal));
            w.EndTopLevel();
        }

        // ---- top-level DmeJoints --------------------------------------------
        for (var i = 0; i < skeleton.Count; i++)
        {
            w.BeginTopLevel("DmeJoint");
            w.Attr("id", "elementid", jointGuids[i]);
            w.Attr("name", "string", skeleton[i].Name);
            w.Attr("transform", "element", transformGuids[i]);
            w.Attr("shape", "element", "");
            w.Attr("visible", "bool", "1");
            w.BeginArray("children");
            var children = new List<int>();
            for (var c = 0; c < skeleton.Count; c++)
            {
                if (skeleton[c].ParentIndex == i)
                    children.Add(c);
            }
            for (var c = 0; c < children.Count; c++)
                w.ElementRef(jointGuids[children[c]], last: c == children.Count - 1);
            w.EndArray();
            w.EndTopLevel();
        }

        return w.ToString();
    }

    /// <summary>
    /// Deterministic element GUID: MD5 over <c>"&lt;name&gt;\n&lt;path&gt;"</c> (UTF-8)
    /// interpreted as <see cref="Guid"/> bytes. Exposed so tests can verify the scheme.
    /// </summary>
    public static Guid ElementGuid(string name, string path)
        => new(MD5.HashData(Encoding.UTF8.GetBytes(name + "\n" + path)));

    private static string GuidString(string name, string path)
        => ElementGuid(name, path).ToString("D", CultureInfo.InvariantCulture);

    // ---------------------------------------------------------------- channels

    private static void WriteChannel(Emitter w, SkeletonModel skeleton, Clip clip, int bone,
        string transformGuid, bool position, bool last)
    {
        var logClass = position ? "DmeVector3Log" : "DmeQuaternionLog";
        var layerClass = position ? "DmeVector3LogLayer" : "DmeQuaternionLogLayer";
        var logName = position ? "vector3 log" : "quaternion log";

        w.BeginArrayElement("DmeChannel");
        w.Attr("name", "string", skeleton[bone].Name + (position ? "_p" : "_o"));
        w.Attr("fromElement", "element", "");
        w.Attr("fromAttribute", "string", "");
        w.Attr("fromIndex", "int", "0");
        w.Attr("toElement", "element", transformGuid);
        w.Attr("toAttribute", "string", position ? "position" : "orientation");
        w.Attr("toIndex", "int", "0");
        w.Attr("mode", "int", "3");

        w.BeginInlineAttr("log", logClass);
        w.Attr("name", "string", logName);
        w.BeginArray("layers");
        w.BeginArrayElement(layerClass);
        w.Attr("name", "string", logName);

        w.BeginArray("times", "time_array");
        for (var f = 0; f < clip.FrameCount; f++)
            w.ArrayValue(Time(f / (double)clip.Fps), last: f == clip.FrameCount - 1);
        w.EndArray();

        w.BeginArray("curvetypes", "int_array");
        w.EndArray();

        w.BeginArray("values", position ? "vector3_array" : "quaternion_array");
        // Orientation values are hemisphere-aligned on the fly (q and -q are the same
        // rotation, but the engine interpolates between DMX samples numerically — see
        // QuaternionContinuity). The clip itself is never mutated.
        var prev = System.Numerics.Quaternion.Identity;
        for (var f = 0; f < clip.FrameCount; f++)
        {
            var x = clip.Frames[f][bone];
            string value;
            if (position)
            {
                value = Vec(x);
            }
            else
            {
                var q = x.Rot;
                if (f > 0 && System.Numerics.Quaternion.Dot(prev, q) < 0f)
                    q = System.Numerics.Quaternion.Negate(q);
                prev = q;
                value = Quat(q);
            }
            w.ArrayValue(value, last: f == clip.FrameCount - 1);
        }
        w.EndArray();

        w.EmptyBinaryAttr("compressed");
        w.EndArrayElement(last: true);
        w.EndArray(); // layers

        w.Attr("curveinfo", "element", "");
        w.Attr("usedefaultvalue", "bool", "0");
        w.Attr("defaultvalue", position ? "vector3" : "quaternion", position ? "0 0 0" : "0 0 0 1");
        w.BeginArray("bookmarksX", "time_array");
        w.EndArray();
        w.BeginArray("bookmarksY", "time_array");
        w.EndArray();
        w.BeginArray("bookmarksZ", "time_array");
        w.EndArray();
        w.EndInlineAttr(); // log

        w.EndArrayElement(last);
    }

    // ---------------------------------------------------------------- formatting

    /// <summary>fbx2dmx float style: up to 10 decimal places, trailing zeros stripped,
    /// invariant culture, negative zero normalized.</summary>
    private static string F(float value)
    {
        if (value == 0f)
            return "0";
        return ((double)value).ToString("0.##########", CultureInfo.InvariantCulture);
    }

    private static string Time(double seconds)
        => seconds.ToString("0.0000", CultureInfo.InvariantCulture);

    private static string Vec(in Maths.XForm x)
        => $"{F(x.Pos.X)} {F(x.Pos.Y)} {F(x.Pos.Z)}";

    private static string Quat(in Maths.XForm x) => Quat(x.Rot);

    private static string Quat(in System.Numerics.Quaternion q)
        => $"{F(q.X)} {F(q.Y)} {F(q.Z)} {F(q.W)}";

    // ---------------------------------------------------------------- emitter

    /// <summary>
    /// Low-level keyvalues2 text emitter reproducing fbx2dmx layout quirks: CRLF endings,
    /// tab indentation, a trailing space after array-typed attribute names, and an
    /// indentation-only line after every inline element attribute closes.
    /// </summary>
    private sealed class Emitter
    {
        private readonly StringBuilder _sb = new();
        private int _indent;

        public void Raw(string text)
        {
            _sb.Append(text).Append("\r\n");
        }

        private void Line(string text)
        {
            _sb.Append('\t', _indent).Append(text).Append("\r\n");
        }

        public void Attr(string name, string type, string value)
            => Line($"\"{name}\" \"{type}\" \"{value}\"");

        public void BeginTopLevel(string className)
        {
            Line($"\"{className}\"");
            Line("{");
            _indent++;
        }

        public void EndTopLevel()
        {
            _indent--;
            Line("}");
            _sb.Append("\r\n"); // blank separator after every top-level element (incl. the last)
        }

        public void BeginInlineAttr(string name, string className)
        {
            Line($"\"{name}\" \"{className}\"");
            Line("{");
            _indent++;
        }

        public void EndInlineAttr()
        {
            _indent--;
            Line("}");
            Line(""); // indentation-only line, as fbx2dmx emits
        }

        public void BeginArrayElement(string className)
        {
            Line($"\"{className}\"");
            Line("{");
            _indent++;
        }

        public void EndArrayElement(bool last)
        {
            _indent--;
            Line(last ? "}" : "},");
        }

        public void BeginArray(string name, string type = "element_array")
        {
            Line($"\"{name}\" \"{type}\" ");
            Line("[");
            _indent++;
        }

        public void EndArray()
        {
            _indent--;
            Line("]");
        }

        public void ElementRef(string guid, bool last)
            => Line($"\"element\" \"{guid}\"" + (last ? "" : ","));

        public void ArrayValue(string value, bool last)
            => Line($"\"{value}\"" + (last ? "" : ","));

        public void EmptyBinaryAttr(string name)
        {
            Line($"\"{name}\" \"binary\" ");
            Line("\"");
            Line("\"");
        }

        public override string ToString() => _sb.ToString();
    }
}
using System;
using System.Collections.Generic;
using HumanoidRetargeter.Cleanup;
using HumanoidRetargeter.Formats;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Solve;
using HumanoidRetargeter.Target;

namespace HumanoidRetargeter;

/// <summary>Which solver retargets a request's clips (design §10).</summary>
public enum SolverKind
{
    /// <summary>The deterministic <see cref="Solve.GeometricSolver"/> (default; better
    /// wherever a role mapping exists).</summary>
    Geometric,

    /// <summary>The experimental skeleton-agnostic deep-learning solver
    /// (<see cref="Dl.DlSolver"/>, SAME pretrained checkpoint) — the no-profile fallback.
    /// Requires <see cref="RetargetTargetSpec.DlWeights"/>; ignores per-role mapping
    /// (only hips/alignment heuristics consult it) and leaves fingers at rest.</summary>
    DeepLearning,
}

/// <summary>
/// One source animation file to retarget (engine-agnostic: bytes in, no file IO). Every
/// request runs its OWN profile detection, so a single batch may mix Mixamo + ActorCore +
/// BVH sources — unless <see cref="MappingOverride"/> supplies a mapping explicitly.
/// </summary>
public sealed class RetargetRequest
{
    /// <summary>Solver choice for this request's clips. <see cref="SolverKind.DeepLearning"/>
    /// requires the batch's <see cref="RetargetTargetSpec.DlWeights"/> to be set; the
    /// conversion fails per-clip with a clear error otherwise.</summary>
    public SolverKind Solver { get; init; } = SolverKind.Geometric;

    /// <summary>Raw bytes of the source file (.fbx, .bvh, .glb, .gltf or .vrm).</summary>
    public required byte[] SourceData { get; init; }

    /// <summary>
    /// Source file name (used for the report and DMX provenance). The extension drives the
    /// format choice (<c>.fbx</c> / <c>.bvh</c> / <c>.glb</c> / <c>.gltf</c> / <c>.vrm</c> —
    /// a VRM is a glTF container whose authored humanoid bone map becomes the mapping);
    /// when the extension is unknown the content is sniffed (FBX binary magic /
    /// "FBXHeaderExtension" / BVH "HIERARCHY" / GLB 'glTF' magic / glTF JSON).
    /// </summary>
    public required string SourceFileName { get; init; }

    /// <summary>
    /// Caller-supplied identity of this request, echoed verbatim on every produced
    /// <see cref="ClipResult.SourceId"/> so callers can join results back to their own
    /// entries unambiguously (e.g. the editor window passes the FULL file path here, since
    /// two files in different folders may share the same <see cref="SourceFileName"/>).
    /// Null = <see cref="SourceFileName"/>.
    /// </summary>
    public string? SourceId { get; init; }

    /// <summary>
    /// Import sample rate the source clips are resampled to (BVH native frames / FBX curves
    /// are evaluated on this grid). Null = the importer default (30 fps).
    /// </summary>
    public float? SampleFps { get; init; }

    /// <summary>
    /// Restricts the conversion to ONE take of the source file (0-based index into the
    /// imported scene's clips). Null = convert all takes. Out of range fails the request's
    /// clip result with a clear error (the batch continues). UI listings that expand a
    /// multi-take file into one entry per take submit one request per selected take.
    /// When <see cref="ClipDefinitions"/> is set this index addresses the DEFINITIONS
    /// instead (each definition is what a UI row represents then).
    /// </summary>
    public int? TakeIndex { get; init; }

    /// <summary>
    /// Optional external clip definitions, parsed from a Unity <c>&lt;file&gt;.fbx.meta</c>
    /// sidecar (<see cref="UnityMeta.ParseClipAnimations"/>): Unity animation packs ship FBX
    /// files whose clips are sub-ranges of ONE source timeline. When set (non-empty), the
    /// conversion produces one output clip per definition instead of one per take: the
    /// definition's take (matched by <see cref="ExternalClipDef.TakeName"/>, falling back to
    /// the file's first take) is sliced to the definition's native-frame range
    /// (<see cref="UnityMeta.Slice"/>), named <see cref="ExternalClipDef.Name"/> (sanitized
    /// like take names, collision-suffixed across the batch) and looped per
    /// <see cref="ExternalClipDef.Loop"/> unless <see cref="LoopingOverride"/> is set.
    /// <see cref="TakeIndex"/> then indexes INTO this list. Null = no definitions.
    /// </summary>
    public IReadOnlyList<ExternalClipDef>? ClipDefinitions { get; init; }

    /// <summary>
    /// UI-supplied mapping (manual mapping table or a user preset loaded Editor-side).
    /// Null = auto-detect per request: preset profiles via <see cref="ProfileDetector"/>,
    /// then the <see cref="AutoMapper"/> as best-effort fallback.
    /// </summary>
    public MappingResult? MappingOverride { get; init; }

    /// <summary>Solver tunables (hip scales, finger transfer). ClipIndex/ClipName are managed
    /// by the pipeline per take and ignored here. Null = defaults.</summary>
    public SolveOptions? Solve { get; init; }

    /// <summary>
    /// Root-motion handling. <see cref="RootMotionMode.Extract"/> on a target without a
    /// dedicated animated root bone (the s&amp;box rig: pelvis is parentless, root_IK is
    /// IkBaked) leaves the frames untouched and instead sets the ExtractMotion flag on the
    /// clip's vmdl AnimFile entry — Source 2's compile-time extraction replaces the missing
    /// bone-level extraction. <see cref="RootMotionMode.InPlace"/> always operates on the
    /// hips directly.
    /// </summary>
    public RootMotionMode RootMotion { get; init; } = RootMotionMode.Off;

    /// <summary>Run the Kovar foot-plant cleanup pass on the solved frames (default on).</summary>
    public bool FootPlantCleanup { get; init; } = true;

    /// <summary>
    /// Optional arm end-effector IK pass pulling the wrists onto limb-length-normalized
    /// source hand positions. Default OFF: the geometric solver already matches anatomical
    /// directions, so arm IK only helps reach-critical work (props, contact poses) and can
    /// otherwise disturb elbow styling.
    /// </summary>
    public bool ArmEffectorIk { get; init; }

    /// <summary>
    /// Generate <c>AE_FOOTSTEP</c> AnimEvent nodes on each produced clip's vmdl AnimFile
    /// entry (default OFF). After solving and cleanup, foot-plant intervals are detected on
    /// the SOLVED target clip (<see cref="Cleanup.FootPlant.DetectPlantIntervals"/>); each
    /// plant's start frame is a touchdown and becomes one footstep event, in the exact node
    /// shape the shipped citizen data uses (see <see cref="Target.FootstepEvents"/>).
    /// Skipped (with a report note) when the target rig lacks complete leg chains.
    /// </summary>
    public bool GenerateFootstepEvents { get; init; }

    /// <summary>
    /// Additionally produce a mirrored twin of every converted clip (default OFF), named
    /// <c>&lt;clip&gt;_M</c> (collision-suffixed across the batch as usual). Mirroring runs
    /// in TARGET space on the solved clip (<see cref="Solve.ClipMirror"/>): left/right role
    /// bone channels swap and everything is reflected across the target character's sagittal
    /// plane; IK-baked helper bones are re-baked from the mirrored body afterwards.
    /// </summary>
    public bool CreateMirroredVariant { get; init; }

    /// <summary>
    /// Additionally register an additive (delta) twin of every converted clip in the
    /// generated/augmented vmdl (default OFF), named <c>&lt;clip&gt;_delta</c> (the shipped
    /// citizen naming; collision-suffixed across the batch as usual). The twin is a second
    /// AnimFile entry REUSING the clip's DMX with an <c>AnimSubtract</c> child
    /// (<c>anim_name</c> = the base sequence, <c>frame</c> = 0) — exactly the shipped
    /// <c>IdleLayer_01</c>/<c>IdleLayer_01_delta</c> pattern, where resourcecompiler
    /// subtracts the reference frame at compile time (no frame math happens here). The
    /// resulting <c>_delta</c> sequence is what s&amp;box layered animation additively
    /// blends on top of a base pose.
    /// </summary>
    public bool CreateAdditiveVariant { get; init; }

    /// <summary>Output clip name override; with multiple takes an index suffix is appended.
    /// Null = the source take name.</summary>
    public string? ClipNameOverride { get; init; }

    /// <summary>Force the looping flag on the output sequence(s); null = the source clip's flag.</summary>
    public bool? LoopingOverride { get; init; }
}

/// <summary>
/// Axis/unit convention of a <see cref="RetargetTargetSpec"/>'s rig data — drives the DMX
/// axis-system declaration, foot-plant threshold units, and the editor preview's
/// rig-space → engine-space conversion.
/// </summary>
public enum TargetUpAxis
{
    /// <summary>
    /// The s&amp;box source convention: rig authored in centimeters, Y-up (the shipped
    /// citizen rig, FBX targets). The vmdl's ScaleAndMirror 0.3937 + resourcecompiler's
    /// Y-up→Z-up conversion take it to engine space at compile time. Default.
    /// </summary>
    YUpCm,

    /// <summary>
    /// Engine space already: rig read from a compiled model's <c>Model.Bones</c>
    /// (inches, Z-up). The DMX declares a Z-up axis system so the compiler performs no
    /// further axis conversion.
    /// </summary>
    ZUpEngine,
}

/// <summary>
/// The conversion target shared by all requests of one <see cref="Retargeter.Convert"/> /
/// <see cref="Retargeter.ConvertBatch"/> call: the rig plus the vmdl generation parameters.
/// </summary>
public sealed class RetargetTargetSpec
{
    /// <summary>The s&amp;box-source → engine-units vmdl scale (cm rigs like the citizen).</summary>
    public const float SboxSourceScale = 0.3937f;

    /// <summary>The committed asset path of the s&amp;box human male model.</summary>
    public const string SboxHumanMalePath = "models/citizen_human/citizen_human_male.vmdl";

    /// <summary>The committed asset path of the classic (4-finger) s&amp;box citizen model.</summary>
    public const string SboxCitizenPath = "models/citizen/citizen.vmdl";

    /// <summary>Target rig (skeleton + bone classes + roles).</summary>
    public required TargetRig Rig { get; init; }

    /// <summary>ModelModifier_ScaleAndMirror scale written into standalone vmdls:
    /// <c>0.3937</c> for cm-authored s&amp;box-source rigs, <c>1.0</c> for engine-unit rigs
    /// (the modifier node is omitted at 1.0).</summary>
    public required float VmdlScale { get; init; }

    /// <summary>base_model_name of generated standalone vmdls (the model that owns the mesh).</summary>
    public string BaseModelPath { get; init; } = "";

    /// <summary>default_root_bone_name of the generated AnimationList (also the bone vmdl
    /// ExtractMotion nodes operate on).</summary>
    public string DefaultRootBone { get; init; } = "pelvis";

    /// <summary>
    /// Axis/unit convention of <see cref="Rig"/>. <see cref="TargetUpAxis.YUpCm"/> (default)
    /// for cm Y-up source-space rigs (DMX declares Y-up, compiler converts);
    /// <see cref="TargetUpAxis.ZUpEngine"/> for rigs read from compiled engine models
    /// (DMX declares Z-up so no double conversion happens at compile, and cm-tuned cleanup
    /// thresholds are rescaled to inches).
    /// </summary>
    public TargetUpAxis UpAxis { get; init; } = TargetUpAxis.YUpCm;

    /// <summary>
    /// Raw bytes of the committed SAME weight blob
    /// (<c>Assets/humanoid_retargeter/dl/same_v1.weights</c>; callers do the file IO).
    /// Required only when a request selects <see cref="SolverKind.DeepLearning"/>; the
    /// solver instance is built once per batch from these bytes.
    /// </summary>
    public byte[]? DlWeights { get; init; }

    /// <summary>
    /// The shipped s&amp;box default target: rig parsed from the committed
    /// <c>Assets/humanoid_retargeter/target_rig_sbox.json</c> text (callers do the file IO),
    /// 0.3937 vmdl scale, citizen human male base model, pelvis root. Pass the committed
    /// SAME weight bytes as <paramref name="dlWeights"/> to enable the deep-learning solver.
    /// </summary>
    public static RetargetTargetSpec SboxDefault(string targetRigJson, byte[]? dlWeights = null) => new()
    {
        Rig = TargetRig.SboxDefault(targetRigJson),
        VmdlScale = SboxSourceScale,
        BaseModelPath = SboxHumanMalePath,
        DefaultRootBone = "pelvis",
        DlWeights = dlWeights,
    };

    /// <summary>
    /// The classic (4-finger) s&amp;box citizen target: rig parsed from the committed
    /// <c>Assets/humanoid_retargeter/target_rig_sbox_citizen.json</c> text (callers do the
    /// file IO), 0.3937 vmdl scale, citizen base model, pelvis root, Y-up cm. The rig has no
    /// pinky bones, so pinky roles stay unassigned — the engine's own constraints handle the
    /// pinky at runtime for models that have one. Pass the committed SAME weight bytes as
    /// <paramref name="dlWeights"/> to enable the deep-learning solver.
    /// </summary>
    public static RetargetTargetSpec SboxCitizen(string targetRigJson, byte[]? dlWeights = null) => new()
    {
        Rig = TargetRig.Load(targetRigJson),
        VmdlScale = SboxSourceScale,
        BaseModelPath = SboxCitizenPath,
        DefaultRootBone = "pelvis",
        UpAxis = TargetUpAxis.YUpCm,
        DlWeights = dlWeights,
    };
}

/// <summary>Options for <see cref="Retargeter.ConvertBatch"/> output assembly.</summary>
public sealed class BatchOptions
{
    /// <summary>
    /// When set, the batch additionally augments this existing vmdl text (all successful
    /// clips spliced into its AnimationList via <see cref="VmdlAugmenter"/>) and returns the
    /// result in <see cref="RetargetBatchResult.AugmentedVmdl"/>.
    /// </summary>
    public string? AugmentVmdlText { get; init; }

    /// <summary>Assets-relative folder the DMX files will be written to by the caller; used
    /// to build each AnimFile's <c>source_filename</c>.</summary>
    public string DmxFolderRelative { get; init; } = "animations/retargeted";

    /// <summary>Auto-suffix colliding clip names (<c>_2</c>, <c>_3</c>, …) across the whole
    /// batch (default on). When off, duplicate names are kept as-is.</summary>
    public bool AutoSuffixCollisions { get; init; } = true;

    /// <summary>
    /// After conversion, scan the batch's successful clip names for directional locomotion
    /// families (default OFF): <c>_N</c>/<c>_NE</c>/…/<c>_NW</c> compass suffixes and
    /// <c>_Forward</c>/<c>_Backward</c>(/<c>_Back</c>)/<c>_Left</c>/<c>_Right</c> word forms
    /// sharing a stem. Each complete family (all four cardinals) is grouped under a Folder
    /// node with a <c>2DBlend</c> wired to the citizen <c>move_x</c>/<c>move_y</c> pose
    /// parameters, replicating the shipped citizen locomotion layout (see
    /// <see cref="Target.LocomotionSetDetector"/>); detection results land on
    /// <see cref="RetargetBatchResult.LocomotionSets"/>. Custom (non-citizen) base models
    /// must declare <c>move_x</c>/<c>move_y</c> pose parameters themselves for the blends to
    /// be drivable.
    /// </summary>
    public bool DetectLocomotionSets { get; init; }
}
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;

namespace HumanoidRetargeter.Solve;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>
/// Hand rest-geometry helpers shared by <see cref="CanonicalFrames"/> (finger secondary axes)
/// and <see cref="RestNormalizer"/> (palm-down roll correction). Everything derives from joint
/// positions only — bone local axes carry no anatomical meaning on the s&amp;box rig.
/// </summary>
internal static class HandGeometry
{
    private static readonly BoneRole[] LeftProximals =
    {
        BoneRole.ThumbProxL, BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,
    };

    private static readonly BoneRole[] RightProximals =
    {
        BoneRole.ThumbProxR, BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,
    };

    // Index → pinky order; the knuckle line is taken from the first and last mapped of these.
    private static readonly BoneRole[] LeftNonThumbProximals =
    {
        BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,
    };

    private static readonly BoneRole[] RightNonThumbProximals =
    {
        BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,
    };

    /// <summary>
    /// Midpoint of all mapped finger proximal heads of one hand (the hand's anatomical
    /// "chain child" point), or null when no finger proximal is mapped.
    /// </summary>
    public static Vector3? FingerProximalMidpoint(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)
    {
        var sum = Vector3.Zero;
        var count = 0;
        foreach (var role in left ? LeftProximals : RightProximals)
        {
            if (map.RoleToBone.TryGetValue(role, out var index))
            {
                sum += worldRest[index].Pos;
                count++;
            }
        }
        return count > 0 ? sum / count : null;
    }

    /// <summary>
    /// Dorsal palm normal of one hand: the unit vector pointing out of the <b>back</b> of the
    /// hand (away from the palm), or null when the hand/finger geometry is unmapped or
    /// degenerate.
    /// </summary>
    /// <remarks>
    /// Formula (mirror-consistent by construction, verified on the ActorCore fixture by the
    /// finger-curl test): <c>dorsal = sideSign · cross(knuckle, fingerDir)</c> with
    /// <c>sideSign = +1</c> left / <c>−1</c> right, <c>knuckle = IndexProx.head −
    /// PinkyProx.head</c> (first/last mapped non-thumb proximal), and <c>fingerDir =
    /// FingerProximalMidpoint − Hand.head</c>. On every fixture rig the thumb proximal lies on
    /// the −dorsal (palmar) side of the hand plane, grounding the sign anatomically. A positive
    /// rotation about a finger frame's hinge axis (frame Y = cross(dorsal, fingerChainDir))
    /// curls the fingertip toward the palm on <b>both</b> hands.
    /// </remarks>
    public static Vector3? Dorsal(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)
    {
        if (!map.RoleToBone.TryGetValue(left ? BoneRole.HandL : BoneRole.HandR, out var handIndex))
            return null;
        var hand = worldRest[handIndex].Pos;

        var nonThumb = left ? LeftNonThumbProximals : RightNonThumbProximals;
        Vector3? first = null, last = null;
        foreach (var role in nonThumb)
        {
            if (!map.RoleToBone.TryGetValue(role, out var index))
                continue;
            first ??= worldRest[index].Pos;
            last = worldRest[index].Pos;
        }
        if (first is null || last is null || (first.Value - last.Value).LengthSquared() < 1e-8f)
            return null;

        var midpoint = FingerProximalMidpoint(map, worldRest, left);
        if (midpoint is null)
            return null;

        var knuckle = first.Value - last.Value;
        var fingerDir = midpoint.Value - hand;
        var raw = Vector3.Cross(knuckle, fingerDir) * (left ? 1f : -1f);
        return raw.LengthSquared() < 1e-8f ? null : Vector3.Normalize(raw);
    }
}
using Sandbox.UI;

namespace Sandbox;

public interface ICleanupEvents
{
	public void OnCleanup( int removedObjects, int restoredObjects );
}

/// <summary>
/// A system that tracks the baseline scene state and allows resetting the map to its original state.
/// Removes all spawned props and restores destroyed map objects while leaving players untouched.
/// </summary>
internal sealed class CleanupSystem : GameObjectSystem<CleanupSystem>, ISceneLoadingEvents
{
	/// <summary>
	/// Set of GameObjects that existed in the original scene baseline.
	/// </summary>
	private readonly HashSet<Guid> _baselineObjectIds = new();

	/// <summary>
	/// Serialized data of baseline objects so we can restore them if destroyed.
	/// </summary>
	private readonly Dictionary<Guid, string> _baselineObjectData = new();

	private static bool _restorePersistedBaseline;
	private static HashSet<Guid> _persistedBaselineIds;
	private static Dictionary<Guid, string> _persistedBaselineData;

	/// <summary>
	/// Whether a baseline has been captured.
	/// </summary>
	public bool HasBaseline => _baselineObjectIds.Count > 0;

	public CleanupSystem( Scene scene ) : base( scene )
	{
	}

	/// <summary>
	/// Call from SaveSystem before Game.ChangeScene() to snapshot the current baseline
	/// </summary>
	public static void PreserveBaselineForSaveLoad()
	{
		if ( Current is null || !Current.HasBaseline ) return;

		_restorePersistedBaseline = true;
		_persistedBaselineIds = new HashSet<Guid>( Current._baselineObjectIds );
		_persistedBaselineData = new Dictionary<Guid, string>( Current._baselineObjectData );
	}

	void ISceneLoadingEvents.BeforeLoad( Scene scene, SceneLoadOptions options )
	{
		// Clear any existing baseline when a new scene is loading
		_baselineObjectIds.Clear();
		_baselineObjectData.Clear();
	}

	async Task ISceneLoadingEvents.OnLoad( Scene scene, SceneLoadOptions options, LoadingContext context )
	{
		// We don't care if the game is not playing
		if ( !Game.IsPlaying ) return;

		// Wait for next frame to ensure all objects are spawned
		await Task.Yield();

		// Could be null if the scene was unloaded before this runs
		if ( !Scene.IsValid() ) return;

		// When loading a save, restore the baseline captured before the scene was destroyed
		if ( _restorePersistedBaseline && _persistedBaselineIds is not null )
		{
			_baselineObjectIds.UnionWith( _persistedBaselineIds );
			foreach ( var kvp in _persistedBaselineData )
				_baselineObjectData.TryAdd( kvp.Key, kvp.Value );

			_restorePersistedBaseline = false;
			Log.Info( $"CleanupSystem: Restored persisted baseline with {_baselineObjectIds.Count} objects." );
		}
		else
		{
			CaptureBaseline();
		}
	}

	/// <summary>
	/// Captures the current scene state as the baseline.
	/// All objects that exist at this point are considered part of the original map.
	/// </summary>
	public void CaptureBaseline()
	{
		_baselineObjectIds.Clear();
		_baselineObjectData.Clear();

		foreach ( var go in Scene.Children?.ToArray() ?? [] )
		{
			CaptureObjectRecursive( go );
		}

		Log.Info( $"CleanupSystem: Captured baseline with {_baselineObjectIds.Count} objects." );
	}

	private void CaptureObjectRecursive( GameObject go )
	{
		if ( !go.IsValid() )
			return;

		// Skip player objects
		if ( IsPlayerObject( go ) )
			return;

		if ( go.Flags.Contains( GameObjectFlags.DontDestroyOnLoad ) )
			return;

		_baselineObjectIds.Add( go.Id );

		var serialized = go.Serialize();
		if ( serialized is not null )
		{
			_baselineObjectData[go.Id] = serialized.ToJsonString();
		}

		foreach ( var child in go.Children?.ToArray() ?? [] )
		{
			CaptureObjectRecursive( child );
		}
	}

	/// <summary>
	/// Determines if a GameObject is a player or belongs to a player.
	/// </summary>
	private static bool IsPlayerObject( GameObject go )
	{
		if ( !go.IsValid() )
			return false;

		if ( go.Components.Get<Player>( true ) is not null )
			return true;

		if ( go.Components.Get<PlayerData>( true ) is not null )
			return true;

		var parent = go.Parent;
		while ( parent is not null && parent != go.Scene )
		{
			if ( parent.Components.Get<Player>( true ) is not null )
				return true;
			if ( parent.Components.Get<PlayerData>( true ) is not null )
				return true;
			parent = parent.Parent;
		}

		return false;
	}

	/// <summary>
	/// Cleans up the scene by removing all spawned objects and restoring destroyed baseline objects.
	/// Players and their belongings are preserved.
	/// </summary>
	public void Cleanup()
	{
		if ( !HasBaseline )
		{
			Log.Warning( "CleanupSystem: No baseline captured. Cannot cleanup." );
			return;
		}

		if ( !Networking.IsHost )
		{
			Log.Warning( "CleanupSystem: Only the host can perform cleanup." );
			return;
		}

		var removedCount = 0;
		var restoredCount = 0;
		var objectsToRemove = new List<GameObject>();
		var existingBaselineIds = new HashSet<Guid>();

		foreach ( var go in Scene.GetAllObjects( true ) )
		{
			if ( !go.IsValid() )
				continue;

			// Never remove player objects
			if ( IsPlayerObject( go ) )
				continue;

			if ( go.Flags.Contains( GameObjectFlags.DontDestroyOnLoad ) )
				continue;

			if ( _baselineObjectIds.Contains( go.Id ) )
			{
				existingBaselineIds.Add( go.Id );
			}
			else
			{
				if ( go.Parent == Scene )
				{
					objectsToRemove.Add( go );
				}
			}
		}

		// Remove spawned objects
		foreach ( var go in objectsToRemove )
		{
			if ( go.IsValid() )
			{
				go.Destroy();
				removedCount++;
			}
		}

		// Restore destroyed baseline objects
		foreach ( var kvp in _baselineObjectData )
		{
			var id = kvp.Key;

			// Skip if the object still exists
			if ( existingBaselineIds.Contains( id ) )
				continue;

			// Skip if we already processed the parent object
			var go = Scene.Directory.FindByGuid( id );
			if ( go.IsValid() )
				continue;

			try
			{
				var json = System.Text.Json.Nodes.JsonNode.Parse( kvp.Value );
				if ( json is System.Text.Json.Nodes.JsonObject jso )
				{
					var restored = new GameObject();
					restored.Deserialize( jso );
					restoredCount++;
				}
			}
			catch ( System.Exception ex )
			{
				Log.Warning( $"CleanupSystem: Failed to restore object {id}: {ex.Message}" );
			}
		}

		BroadcastCleanup( removedCount, restoredCount );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private static void BroadcastCleanup( int removedObjects, int restoredObjects )
	{
		Game.ActiveScene?.RunEvent<ICleanupEvents>( x => x.OnCleanup( removedObjects, restoredObjects ) );

		Log.Info( $"Cleanup complete. Removed {removedObjects} spawned objects, restored {restoredObjects} destroyed objects." );
	}

	/// <summary>
	/// Console command to cleanup the map.
	/// </summary>
	[ConCmd( "cleanup" )]
	public static void CleanupCommand( string targetName = null )
	{
		if ( !Networking.IsHost ) return;

		//
		// Targeted cleanup, doesn't use the same cleanup shit
		//
		if ( !string.IsNullOrEmpty( targetName ) )
		{
			var target = GameManager.FindPlayerWithName( targetName );
			if ( target is not null )
			{
				CleanupPlayer( target );
			}
			else
			{
				Notices.AddNotice( "cleaning_services", Color.Red, $"Can't find {targetName} to clean up" );
			}

			return;
		}

		if ( Current is null )
		{
			Log.Warning( "CleanupSystem: No active cleanup system." );
			return;
		}

		Current.Cleanup();
	}

	[Rpc.Host]
	public static void RpcCleanUpMine()
	{
		CleanupPlayer( Rpc.Caller );
	}

	[Rpc.Host]
	public static void RpcCleanUpAll()
	{
		if ( !Rpc.Caller.HasPermission( "admin" ) ) return;

		Current?.Cleanup();
	}

	[Rpc.Host]
	public static void RpcCleanUpTarget( Connection target )
	{
		if ( !Rpc.Caller.HasPermission( "admin" ) ) return;

		CleanupPlayer( target );
	}

	public static void CleanupPlayer( Connection caller )
	{
		Assert.True( Networking.IsHost, "Only the host may call this method!" );

		var removable = Game.ActiveScene.GetAllComponents<Ownable>()
			.Where( o => o.Owner == caller );

		var count = 0;
		foreach ( var ownable in removable.ToArray() )
		{
			ownable.GameObject.Destroy();
			count++;
		}

		Notices.SendNotice( caller, "cleaning_services", Color.Green, $"Cleaned up {count} objects" );
	}

}
[Alias( "dynamite" )]
public sealed class DynamiteEntity : Component, IPlayerControllable, Component.IDamageable
{
	[Property, Range( 1, 500 ), Step( 1 ), ClientEditable]
	public float Damage { get; set; } = 128;

	[Property, Range( 16, 4096 ), Step( 16 ), ClientEditable]
	public float Radius { get; set; } = 1024f;

	[Property, Range( 1, 100 ), Step( 1 ), ClientEditable]
	public float Force { get; set; } = 1;

	[Property, Sync, ClientEditable]
	public ClientInput Activate { get; set; }

	bool _isDead = false;

	[Rpc.Host]
	public void Explode()
	{
		_isDead = true;

		var explosionPrefab = ResourceLibrary.Get<PrefabFile>( "/prefabs/engine/explosion_med.prefab" );
		if ( explosionPrefab == null )
		{
			Log.Warning( "Can't find /prefabs/engine/explosion_med.prefab" );
			return;
		}

		var go = GameObject.Clone( explosionPrefab, new CloneConfig { Transform = WorldTransform.WithScale( 1 ), StartEnabled = false } );
		if ( !go.IsValid() ) return;

		go.RunEvent<RadiusDamage>( x =>
		{
			x.Radius = Radius;
			x.PhysicsForceScale = Force;
			x.DamageAmount = Damage;
			x.Attacker = go;
		}, FindMode.EverythingInSelfAndDescendants );

		go.Enabled = true;
		go.NetworkSpawn( true, null );

		GameObject.Destroy();
	}

	void IDamageable.OnDamage( in DamageInfo damage )
	{
		if ( _isDead ) return;
		if ( IsProxy ) return;

		Explode();
	}

	void IPlayerControllable.OnControl()
	{
		if ( Activate.Pressed() )
		{
			Explode();
		}
	}

	void IPlayerControllable.OnEndControl()
	{
		// nothing to do
	}

	void IPlayerControllable.OnStartControl()
	{
		// nothing to do
	}
}
/// <summary>
/// Whether the emitter fires while the input is held, or toggles on/off with a press.
/// </summary>
public enum EmitMode
{
	/// <summary>
	/// Press once to turn on, press again to turn off.
	/// </summary>
	Toggle,
	/// <summary>
	/// Emits only while the input is held down.
	/// </summary>
	Hold,
}

/// <summary>
/// A world-placed SENT that spawns and controls a particle/VFX emitter.
/// The emitter prefab is defined by a <see cref="ScriptedEmitter"/> resource.
/// </summary>
[Alias( "emitter" )]
public sealed class EmitterEntity : Component, IPlayerControllable
{
	/// <summary>
	/// The emitter definition points to a prefab containing a particle system.
	/// </summary>
	[Property, ClientEditable]
	public ScriptedEmitter Emitter { get; set; }

	/// <summary>
	/// Whether this emitter toggles on/off with a press, or emits only while held.
	/// </summary>
	[Property, ClientEditable]
	public EmitMode Mode { get; set; } = EmitMode.Toggle;

	/// <summary>
	/// Used when <see cref="Mode"/> is <see cref="EmitMode.Toggle"/>.
	/// </summary>
	[Property, Sync, ClientEditable, Group( "Input" )]
	public ClientInput ToggleInput { get; set; }

	/// <summary>
	/// Used when <see cref="Mode"/> is <see cref="EmitMode.Hold"/>.
	/// </summary>
	[Property, Sync, ClientEditable, Group( "Input" )]
	public ClientInput HoldInput { get; set; }

	/// <summary>
	/// Whether the emitter is currently active. Synced to all clients.
	/// </summary>
	[Sync] public bool IsEmitting { get; private set; }

	/// <summary>
	/// When enabled, forces the emitter on regardless of input or mode.
	/// Can be set from the editor or wired up externally.
	/// </summary>
	[Property, ClientEditable]
	public bool ManualOn
	{
		get => _manualOn;
		set { _manualOn = value; if ( !IsProxy ) UpdateEmitState(); }
	}
	private bool _manualOn;
	private bool _inputEmitting;

	private GameObject _particleInstance;
	private ScriptedEmitter _lastEmitter;

	protected override void OnStart() { }

	protected override void OnUpdate()
	{
		// Emitter resource changed — destroy existing instance so it gets recreated
		if ( _lastEmitter != Emitter && _particleInstance.IsValid() )
			DestroyParticle();

		_lastEmitter = Emitter;

		if ( IsEmitting && !_particleInstance.IsValid() )
			SpawnParticle();
		else if ( !IsEmitting && _particleInstance.IsValid() )
			DestroyParticle();
	}

	void IPlayerControllable.OnStartControl() { }
	void IPlayerControllable.OnEndControl()
	{
		if ( Mode == EmitMode.Hold )
		{
			_inputEmitting = false;
			UpdateEmitState();
		}
	}

	void IPlayerControllable.OnControl()
	{
		if ( Mode == EmitMode.Toggle )
		{
			if ( ToggleInput.Pressed() )
			{
				_inputEmitting = !_inputEmitting;
				UpdateEmitState();
			}
		}
		else
		{
			var held = HoldInput.Down();
			if ( held != _inputEmitting )
			{
				_inputEmitting = held;
				UpdateEmitState();
			}
		}
	}

	private void UpdateEmitState() => SetEmitting( _inputEmitting || _manualOn );

	[Rpc.Broadcast]
	private void SetEmitting( bool active )
	{
		IsEmitting = active;
	}

	private void SpawnParticle()
	{
		if ( !Emitter.IsValid() || Emitter.Prefab is null ) return;

		_particleInstance = GameObject.Clone( Emitter.Prefab, new CloneConfig
		{
			Parent = GameObject,
			Transform = new Transform( Vector3.Forward * 4f ),
			StartEnabled = true,
		} );
	}

	private void DestroyParticle()
	{
		_particleInstance.Destroy();
		_particleInstance = null;
	}
}


public sealed class SpotLightEntity : Component, IPlayerControllable
{
	[Property, ClientEditable, Group( "Light" )]
	public bool On { get; set { field = value; UpdateLight(); } } = true;

	[Property, ClientEditable, Group( "Light" )]
	public bool Shadows { get; set { field = value; UpdateLight(); } } = true;

	[Property, Range( 0, 1 ), ClientEditable, Group( "Light" )]
	public Color Color { get; set { field = value; UpdateLight(); } }

	[Property, Range( 0, 50 ), ClientEditable, Group( "Light" )]
	public float Brightness { get; set { field = value; UpdateLight(); } } = 2;

	[Property, Range( 0, 1000 ), ClientEditable, Group( "Light" )]
	public float Radius { get; set { field = value; UpdateLight(); } } = 500;

	[Property, Range( 0, 90 ), ClientEditable, Group( "Light" )]
	public float Angle { get; set { field = value; UpdateLight(); } } = 35;

	[Property, Range( 0, 16 ), ClientEditable, Group( "Light" )]
	public float Attenuation { get; set { field = value; UpdateLight(); } } = 2.4f;


	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput TurnOn { get; set; }

	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput TurnOff { get; set; }

	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput Toggle { get; set; }

	[Property]
	public GameObject OnGameObject { get; set; }

	[Property]
	public GameObject OffGameObject { get; set; }

	void IPlayerControllable.OnControl()
	{

		if ( Toggle.Pressed() )
		{
			On = !On;
		}

		if ( TurnOn.Pressed() )
		{
			On = true;
		}

		if ( TurnOff.Pressed() )
		{
			On = false;
		}
	}

	void IPlayerControllable.OnEndControl()
	{

	}

	void IPlayerControllable.OnStartControl()
	{

	}

	void UpdateLight()
	{
		OnGameObject?.Enabled = On;
		OffGameObject?.Enabled = !On;

		if ( GetComponentInChildren<SpotLight>( true ) is not SpotLight light )
			return;

		light.Enabled = On;

		var color = Color;
		color.r *= Brightness;
		color.g *= Brightness;
		color.b *= Brightness;

		light.Shadows = Shadows;
		light.LightColor = color;
		light.Radius = Radius;
		light.Attenuation = Attenuation;
		light.ConeOuter = Angle;
		light.ConeInner = Angle * 0.5f;

		Network.Refresh();
	}
}
public partial class BaseBulletWeapon : BaseWeapon
{
	[Property]
	public SoundEvent ShootSound { get; set; }

	[Property, Group( "Bullet" )]
	public BulletConfiguration Bullet { get; set; } = new()
	{
		Damage = 12f,
		BulletRadius = 1f,
		Range = 4096f,
		AimConeBase = new Vector2( 0.5f, 0.25f ),
		AimConeSpread = new Vector2( 3f, 3f ),
		AimConeRecovery = 0.2f,
		RecoilPitch = new Vector2( -0.3f, -0.1f ),
		RecoilYaw = new Vector2( -0.1f, 0.1f ),
		CameraRecoilStrength = 1f,
		CameraRecoilFrequency = 1f,
	};

	[Property, Group( "Bullet" ), ClientEditable, Range( 0f, 500000f ), Step( 10f )]
	public float ShootForce { get; set; } = 100000f;

	protected TimeSince TimeSinceShoot = 0;

	/// <summary>
	/// Returns 0 for no aim spread, 1 for full aim cone, based on time since last shot.
	/// </summary>
	protected float GetAimConeAmount( float recovery )
	{
		return TimeSinceShoot.Relative.Remap( 0, recovery, 1, 0 );
	}

	/// <summary>
	/// Returns the aim cone amount using the configured recovery time
	/// </summary>
	protected float GetAimConeAmount()
	{
		return GetAimConeAmount( Bullet.AimConeRecovery );
	}

	/// <inheritdoc cref="ShootBullet(float, in BulletConfiguration)"/>
	protected void ShootBullet( float fireRate )
	{
		ShootBullet( fireRate, Bullet );
	}

	/// <summary>
	/// Shoot a bullet out of the front of the gun.
	/// When held by a player, fires from the player's eye with aim cone and recoil.
	/// When standalone (no owner), fires straight from the weapon's muzzle.
	/// </summary>
	protected void ShootBullet( float fireRate, in BulletConfiguration config )
	{
		if ( HasOwner && ( !HasAmmo() || IsReloading() ) )
		{
			TryAutoReload();
			return;
		}

		if ( TimeUntilNextShotAllowed > 0 )
			return;

		// Only consume ammo when held by a player
		if ( HasOwner && !TakeAmmo( 1 ) )
		{
			AddShootDelay( 0.2f );
			return;
		}

		AddShootDelay( fireRate );

		var aimConeAmount = GetAimConeAmount( config.AimConeRecovery );
		var forward = AimRay.Forward
			.WithAimCone(
				config.AimConeBase.x + aimConeAmount * config.AimConeSpread.x,
				config.AimConeBase.y + aimConeAmount * config.AimConeSpread.y
			);
		var traceRay = AimRay with { Forward = forward };

		var tr = Scene.Trace.Ray( traceRay, config.Range )
			.IgnoreGameObjectHierarchy( AimIgnoreRoot )
			.WithCollisionRules( "bullet" )
			.WithoutTags( "playercontroller" )
			.Radius( config.BulletRadius )
			.UseHitboxes()
			.Run();

		ShootEffects( tr.EndPosition, tr.Hit, tr.Normal, tr.GameObject, tr.Surface );
		TraceAttack( TraceAttackInfo.From( tr, config.Damage ) );
		TimeSinceShoot = 0;

		// Recoil only applies when held by a player
		if ( !HasOwner )
		{
			// Simulate physical recoil by pushing the weapon opposite to its fire direction
			if ( ShootForce > 0f && GetComponent<Rigidbody>( true ) is var rb )
			{
				var muzzle = WeaponModel?.MuzzleTransform?.WorldTransform ?? WorldTransform;
				rb.ApplyForce( muzzle.Rotation.Up * ShootForce );
			}
			return;
		}

		Owner.Controller.EyeAngles += new Angles(
			Random.Shared.Float( config.RecoilPitch.x, config.RecoilPitch.y ),
			Random.Shared.Float( config.RecoilYaw.x, config.RecoilYaw.y ),
			0
		);

		if ( !Owner.Controller.ThirdPerson && Owner.IsLocalPlayer )
		{
			_ = new Sandbox.CameraNoise.Recoil( config.CameraRecoilStrength, config.CameraRecoilFrequency );
		}
	}

	[Rpc.Broadcast]
	public void ShootEffects( Vector3 hitpoint, bool hit, Vector3 normal, GameObject hitObject, Surface hitSurface, Vector3? origin = null, bool noEvents = false )
	{
		if ( Application.IsDedicatedServer ) return;
		if ( !hitSurface.IsValid() ) return;

		Owner?.Controller.Renderer.Set( "b_attack", true );

		if ( !noEvents )
		{
			if ( WeaponModel.IsValid() )
			{
				WeaponModel.GameObject.RunEvent<WeaponModel>( x => x.OnAttack() );
				WeaponModel.GameObject.RunEvent<WeaponModel>( x => x.CreateRangedEffects( this, hitpoint, origin ) );
			}

			if ( ShootSound.IsValid() )
			{
				var snd = GameObject.PlaySound( ShootSound );

				// If we're shooting, the sound should not be spatialized
				if ( HasOwner && Owner.IsLocalPlayer && snd.IsValid() )
				{
					snd.SpacialBlend = 0;
				}
			}
		}

		if ( !hit || !hitObject.IsValid() )
			return;

		var baseSurface = hitSurface.GetBaseSurface();
		var bulletSound = hitSurface.SoundCollection.Bullet ?? baseSurface?.SoundCollection.Bullet;
		if ( bulletSound.IsValid() )
		{
			Sound.Play( bulletSound, hitpoint );
		}

		var prefab = hitSurface.PrefabCollection.BulletImpact ?? baseSurface?.PrefabCollection.BulletImpact;

		// Still null?
		if ( prefab is null )
			return;

		var fwd = Rotation.LookAt( normal * -1.0f, Vector3.Random );

		var impact = prefab.Clone();
		impact.WorldPosition = hitpoint;
		impact.WorldRotation = fwd;
		impact.SetParent( hitObject, true );

		if ( hitObject.GetComponentInChildren<SkinnedModelRenderer>() is not { CreateBoneObjects: true } skinned )
			return;

		// find closest bone
		var bones = skinned.GetBoneTransforms( true );

		var closestDist = float.MaxValue;

		for ( var i = 0; i < bones.Length; i++ )
		{
			var bone = bones[i];
			var dist = bone.Position.Distance( hitpoint );
			if ( dist < closestDist )
			{
				closestDist = dist;
				impact.SetParent( skinned.GetBoneObject( i ), true );
			}
		}
	}

	public record struct BulletConfiguration
	{
		public float Damage { get; set; }
		public float BulletRadius { get; set; }
		public Vector2 AimConeBase { get; set; }
		public Vector2 AimConeSpread { get; set; }
		public float AimConeRecovery { get; set; }
		public Vector2 RecoilPitch { get; set; }
		public Vector2 RecoilYaw { get; set; }
		public float CameraRecoilStrength { get; set; }
		public float CameraRecoilFrequency { get; set; }
		public float Range { get; set; }
	}
}
using System.Threading;

public partial class BaseWeapon
{
	/// <summary>
	/// Should we consume 1 bullet per reload instead of filling the clip?
	/// </summary>
	[Property, Feature( "Ammo" )]
	public bool IncrementalReloading { get; set; } = false;

	/// <summary>
	/// Extra delay after the first shell reload before subsequent shells begin (e.g. longer carrier insertion animation).
	/// Only used with incremental reloading. If zero, no extra delay is added.
	/// </summary>
	[Property, Feature( "Ammo" ), ShowIf( nameof( IncrementalReloading ), true )]
	public float FirstShellReloadTime { get; set; } = 0f;

	/// <summary>
	/// Delay before the first shell is inserted during incremental reload.
	/// If zero, uses <see cref="ReloadTime"/>.
	/// </summary>
	[Property, Feature( "Ammo" ), ShowIf( nameof( IncrementalReloading ), true )]
	public float ReloadStartTime { get; set; } = 0f;

	/// <summary>
	/// Can we cancel reloads?
	/// </summary>
	[Property, Feature( "Ammo" )]
	public bool CanCancelReload { get; set; } = true;

	private CancellationTokenSource reloadToken;
	private bool isReloading;

	public bool CanReload()
	{
		if ( !UsesClips ) return false;
		if ( ClipContents >= ClipMaxSize ) return false;
		if ( isReloading ) return false;
		if ( !WeaponConVars.InfiniteReserves && ReserveAmmo <= 0 ) return false;

		return true;
	}

	public bool IsReloading() => isReloading;

	public virtual void CancelReload()
	{
		if ( reloadToken?.IsCancellationRequested == false )
		{
			reloadToken?.Cancel();
			isReloading = false;

			ViewModel?.RunEvent<ViewModel>( x => x.OnReloadCancel() );
		}
	}

	public virtual async void OnReloadStart()
	{
		if ( !CanReload() )
			return;

		CancelReload();

		var cts = new CancellationTokenSource();
		reloadToken = cts;
		isReloading = true;

		try
		{
			await ReloadAsync( cts.Token );
		}
		finally
		{
			// Only clean up our own reload
			if ( reloadToken == cts )
			{
				isReloading = false;
				reloadToken = null;
			}
			cts.Dispose();
		}
	}

	[Rpc.Broadcast]
	private void BroadcastReload()
	{
		if ( !HasOwner ) return;

		Assert.True( Owner.Controller.IsValid(), "BaseWeapon::BroadcastReload - Player Controller is invalid!" );
		Assert.True( Owner.Controller.Renderer.IsValid(), "BaseWeapon::BroadcastReload - Renderer is invalid!" );

		Owner.Controller.Renderer.Set( "b_reload", true );
	}

	protected virtual async Task ReloadAsync( CancellationToken ct )
	{
		// Capture so we can tell if a newer reload has replaced us by the time finally runs.
		var mySource = reloadToken;
		var isFirstShell = ClipContents == 0;

		try
		{
			ViewModel?.RunEvent<ViewModel>( x => x.OnReloadStart() );

			BroadcastReload();

			var firstIteration = true;

			while ( ClipContents < ClipMaxSize && !ct.IsCancellationRequested )
				{
					var delay = (firstIteration && IncrementalReloading && ReloadStartTime > 0f) ? ReloadStartTime : ReloadTime;
					firstIteration = false;
					await Task.DelaySeconds( delay, ct );

					var needed = IncrementalReloading ? 1 : (ClipMaxSize - ClipContents);

					if ( WeaponConVars.InfiniteReserves )
					{
						ViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );
						ClipContents += needed;
					}
					else
					{
						var available = Math.Min( needed, ReserveAmmo );

						if ( available <= 0 )
							break;

						ViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );

						ReserveAmmo -= available;
						ClipContents += available;
					}

					// After the first shell, wait longer before the next one starts
					if ( isFirstShell && FirstShellReloadTime > 0f )
					{
						await Task.DelaySeconds( FirstShellReloadTime, ct );
					}

					isFirstShell = false;
				}
		}
		finally
		{
			if ( reloadToken == mySource )
			{
				ViewModel?.RunEvent<ViewModel>( x => x.OnReloadFinish() );
			}
		}
	}
}
/// <summary>
/// The local user's preferences in Deathmatch
/// </summary>
internal static class GamePreferences
{
	/// <summary>
	/// Enables automatic switching to better weapons on item pickup
	/// </summary>
	[ConVar( "sb.autoswitch", ConVarFlags.UserInfo | ConVarFlags.Saved )]
	public static bool AutoSwitch { get; set; } = true;

	/// <summary>
	/// Enables fast switching between inventory weapons
	/// </summary>
	[ConVar( "sb.fastswitch", ConVarFlags.Saved )]
	public static bool FastSwitch { get; set; } = false;

	/// <summary>
	/// Intensity of your camera's screenshake
	/// </summary>
	[ConVar( "sb.viewbob", ConVarFlags.Saved )]
	[Group( "Camera" )]
	public static bool ViewBobbing { get; set; } = true;

	/// <summary>
	/// Intensity of your camera's screenshake
	/// </summary>
	[ConVar( "sb.screenshake", ConVarFlags.Saved )]
	[Range( 0.1f, 2f ), Step( 0.1f ), Group( "Camera" )]
	public static float Screenshake { get; set; } = 0.3f;
}
namespace Sandbox.Npcs;

/// <summary>
/// Console variables that control NPC AI behaviour globally.
/// </summary>
public static class NpcConVars
{
	/// <summary>
	/// When disabled, all NPC AI thinking is paused — they just stand idle.
	/// </summary>
	[ConVar( "sb.ai.enabled", ConVarFlags.Replicated | ConVarFlags.Saved, Help = "Enable or disable NPC AI thinking." )]
	public static bool Enabled { get; set; } = true;

	/// <summary>
	/// When enabled, NPCs cannot target players.
	/// </summary>
	[ConVar( "sb.ai.notarget", ConVarFlags.Replicated | ConVarFlags.Saved, Help = "When enabled, NPCs cannot target players." )]
	public static bool NoTarget { get; set; } = false;
}
using Sandbox.Npcs.Layers;
using Sandbox.Npcs.Tasks;

namespace Sandbox.Npcs.Schedules;

/// <summary>
/// Panic flee — scream while sprinting away from the source.
/// </summary>
public sealed class ScientistFleeSchedule : ScheduleBase
{
	private static readonly string[] PanicLines =
	[
		"AHHH!",
		"Don't hurt me!",
		"Help! HELP!",
		"Stay away from me!",
		"I'm just a scientist!",
		"Please, no!",
		"Somebody help!",
		"Oh god oh god oh god!",
		"What did I do?!",
		"Leave me alone!",
	];

	public GameObject Source { get; set; }

	/// <summary>
	/// 0–1 panic intensity. Higher values mean faster speed and longer flee distance.
	/// </summary>
	public float PanicLevel { get; set; } = 0.5f;

	protected override void OnStart()
	{
		if ( !Source.IsValid() ) return;

		// Sprint speed scales with panic (200–350)
		Npc.Navigation.WishSpeed = 200f + 150f * PanicLevel;

		// Don't stare at the player — look where we're running
		Npc.Animation.ClearLookTarget();

		// Scream immediately — but only if not already mid-speech
		if ( Npc.Speech.CanSpeak )
		{
			var line = PanicLines[Game.Random.Int( 0, PanicLines.Length - 1 )];
			Npc.Speech.Say( line, 2f );
		}

		// Flee direction — away from the attacker with some randomness
		var awayDir = (GameObject.WorldPosition - Source.WorldPosition).WithZ( 0 ).Normal;
		var randomAngle = Game.Random.Float( -40f, 40f );
		awayDir = Rotation.FromAxis( Vector3.Up, randomAngle ) * awayDir;

		// Distance scales with panic (200–500)
		var fleeDist = 512f + 1024f * PanicLevel;
		var fleeTarget = GameObject.WorldPosition + awayDir * fleeDist;

		// Snap to navmesh
		if ( Npc.Scene.NavMesh.GetClosestPoint( fleeTarget ) is { } navPoint )
		{
			AddTask( new MoveTo( navPoint, 15f ) );
		}
		else
		{
			AddTask( new MoveTo( fleeTarget, 15f ) );
		}
	}

	protected override void OnEnd()
	{
		// Reset to normal walk speed
		// TODO: this is shit, can we scope these somehow so the IDisposable handles all this ?
		Npc.Navigation.WishSpeed = 100f;
	}

	protected override bool ShouldCancel()
	{
		return !Source.IsValid();
	}
}
/// <summary>
/// Apply fall damage to the player
/// </summary>
public class PlayerFallDamage : Component, Local.IPlayerEvents
{
	[RequireComponent] public Player Player { get; set; }

	/// <summary>
	/// Fatal fall speed, you will die if you fall at or above this speed
	/// </summary>
	[Property] public float FatalFallSpeed { get; set; } = 1536.0f;

	/// <summary>
	/// Maximum safe fall speed, you won't take damage at or below this speed
	/// </summary>
	[Property] public float MaxSafeFallSpeed { get; set; } = 512.0f;

	/// <summary>
	/// Multiply damage amount by this much
	/// </summary>
	[Property] public float DamageMultiplier { get; set; } = 1.0f;

	/// <summary>
	/// Fall damage sound
	/// </summary>
	[Property] public SoundEvent FallSound { get; set; }

	[Rpc.Owner]
	private void PlayFallSound()
	{
		GameObject.PlaySound( FallSound );
	}

	void Local.IPlayerEvents.OnLand( float distance, Vector3 velocity )
	{
		var fallSpeed = Math.Abs( velocity.z );

		if ( fallSpeed <= MaxSafeFallSpeed )
			return;

		var damageAmount = MathX.Remap( fallSpeed, MaxSafeFallSpeed, FatalFallSpeed, 0f, 100f ) * DamageMultiplier;
		if ( damageAmount < 1 ) return;

		if ( Networking.IsHost && damageAmount >= Player.Health )
			Player.PlayerData?.AddStat( "player.fall.death" );

		TakeFallDamage( damageAmount );
	}


	[Rpc.Broadcast]
	public void TakeFallDamage( float amount )
	{
		if ( !Networking.IsHost ) return;


		if ( Player is IDamageable damage )
		{
			var dmg = new DamageInfo( amount.CeilToInt(), Player.GameObject, null );
			dmg.Tags.Add( DamageTags.Fall );
			damage.OnDamage( dmg );

			PlayFallSound();
		}
	}
}
/// <summary>
/// Manages loadout persistence, presets, and restoration for a player.
/// Lives on the Player GameObject alongside PlayerInventory.
/// Listens to inventory events to auto-save, and handles all loadout RPCs directly.
/// </summary>
public sealed class PlayerLoadout : Component, Local.IPlayerEvents, Global.IPlayerEvents, Global.ISaveEvents
{
	[RequireComponent] public Player Player { get; set; }
	[RequireComponent] public PlayerInventory Inventory { get; set; }

	private bool _isRestoringLoadout;

	/// <summary>
	/// One entry in a serialized loadout: the prefab resource path and the slot it occupies.
	/// </summary>
	public struct LoadoutEntry
	{
		public string PrefabPath { get; set; }
		public int Slot { get; set; }
		public string SpawnerDataPayload { get; set; }
	}

	public struct SavedPreset
	{
		public string Name { get; set; }
		public string LoadoutJson { get; set; }
	}

	public static IReadOnlyList<SavedPreset> GetLoadoutPresets()
	{
		return LocalData.Get<List<SavedPreset>>( "presets", new() );
	}

	public static void SaveLoadoutPreset( string name, string loadoutJson )
	{
		var presets = LocalData.Get<List<SavedPreset>>( "presets", new() );
		var idx = presets.FindIndex( p => p.Name == name );
		var entry = new SavedPreset { Name = name, LoadoutJson = loadoutJson };
		if ( idx >= 0 )
			presets[idx] = entry;
		else
			presets.Add( entry );
		LocalData.Set( "presets", presets );
	}

	public static void DeleteLoadoutPreset( string name )
	{
		var presets = LocalData.Get<List<SavedPreset>>( "presets", new() );
		presets.RemoveAll( p => p.Name == name );
		LocalData.Set( "presets", presets );
	}

	public string SerializeLoadout()
	{
		var entries = Inventory.Weapons
			.Where( w => !string.IsNullOrEmpty( w.GameObject.PrefabInstanceSource ) )
			.Select( w => new LoadoutEntry
			{
				PrefabPath = w.GameObject.PrefabInstanceSource,
				Slot = w.InventorySlot,
				SpawnerDataPayload = (w as SpawnerWeapon)?.SpawnerData
			} )
			.ToList();

		return entries.Count > 0 ? Json.Serialize( entries ) : null;
	}

	public void SaveLoadout()
	{
		if ( _isRestoringLoadout ) return;

		var json = SerializeLoadout();
		if ( string.IsNullOrEmpty( json ) ) return;

		if ( Player.IsLocalPlayer )
		{
			LocalData.Set( "hotbar", json );
		}
		else
		{
			PushLoadoutToClient( json );
		}
	}

	public void GiveLoadoutWeapons( string json )
	{
		var entries = Json.Deserialize<List<LoadoutEntry>>( json );
		if ( entries is null ) return;

		_isRestoringLoadout = true;
		try
		{
			foreach ( var entry in entries )
			{
				if ( !Inventory.Pickup( entry.PrefabPath, entry.Slot, false ) )
					continue;

				if ( !string.IsNullOrEmpty( entry.SpawnerDataPayload ) && Inventory.GetSlot( entry.Slot ) is SpawnerWeapon spawnerWeapon )
				{
					spawnerWeapon.RestoreSpawnerData( entry.SpawnerDataPayload );
				}
			}
		}
		finally
		{
			_isRestoringLoadout = false;
		}
	}

	private static async Task EnsureMountedAsync( string json )
	{
		var entries = Json.Deserialize<List<LoadoutEntry>>( json );
		if ( entries is null ) return;

		var needsMounts = entries.Any( e => !string.IsNullOrEmpty( e.SpawnerDataPayload )
			&& e.SpawnerDataPayload.EndsWith( ".vmdl", StringComparison.OrdinalIgnoreCase ) );

		if ( !needsMounts ) return;

		foreach ( var entry in Sandbox.Mounting.Directory.GetAll().Where( e => e.Available ) )
			await Sandbox.Mounting.Directory.Mount( entry.Ident );
	}

	public void SwitchToPreset( string loadoutJson )
	{
		if ( !Networking.IsHost )
		{
			HostSwitchToPreset( loadoutJson );
			return;
		}
		_ = SwitchToPresetAsync( loadoutJson );
	}

	public void ResetToDefault()
	{
		if ( !Networking.IsHost )
		{
			HostResetToDefault();
			return;
		}
		_ = ResetToDefaultAsync();
	}

	[Rpc.Host]
	private void HostSwitchToPreset( string loadoutJson )
	{
		_ = SwitchToPresetAsync( loadoutJson );
	}

	[Rpc.Host]
	private void HostResetToDefault()
	{
		_ = ResetToDefaultAsync();
	}

	private async Task SwitchToPresetAsync( string loadoutJson )
	{
		var previousSlot = Inventory.ActiveWeapon?.InventorySlot ?? 0;

		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		await EnsureMountedAsync( loadoutJson );
		GiveLoadoutWeapons( loadoutJson );

		var toEquip = Inventory.GetSlot( previousSlot ) ?? Inventory.GetBestWeapon();
		if ( toEquip.IsValid() )
			Inventory.SwitchWeapon( toEquip );

		SaveLoadout();
	}

	private async Task ResetToDefaultAsync()
	{
		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		Inventory.GiveDefaultWeapons();
		Inventory.SwitchWeapon( Inventory.GetBestWeapon() );
		SaveLoadout();
	}

	[Rpc.Owner]
	private void PushLoadoutToClient( string loadoutJson )
	{
		LocalData.Set( "hotbar", loadoutJson );
	}

	[Rpc.Owner]
	private void RequestClientLoadout()
	{
		var json = LocalData.Get<string>( "hotbar" );
		if ( !string.IsNullOrEmpty( json ) )
			HostRestoreLoadoutFromClient( json );
	}

	/// <summary>
	/// Clears the current inventory, waits a frame, then gives the loadout from JSON and equips the best weapon.
	/// </summary>
	private async Task ReplaceLoadoutAsync( string json )
	{
		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		await EnsureMountedAsync( json );
		GiveLoadoutWeapons( json );

		var best = Inventory.GetBestWeapon();
		if ( best.IsValid() )
			Inventory.SwitchWeapon( best );
	}

	[Rpc.Host]
	private async void HostRestoreLoadoutFromClient( string loadoutJson )
	{
		await ReplaceLoadoutAsync( loadoutJson );
	}

	void Global.IPlayerEvents.OnPlayerSpawned( Player player )
	{
		if ( player != Player ) return;
		if ( !Networking.IsHost ) return;

		_ = RestoreOnSpawnAsync();
	}

	private async Task RestoreOnSpawnAsync()
	{
		if ( Player.IsLocalPlayer )
		{
			var json = LocalData.Get<string>( "hotbar" );
			if ( !string.IsNullOrEmpty( json ) )
			{
				await ReplaceLoadoutAsync( json );
				return;
			}
		}
		else
		{
			RequestClientLoadout();
			return;
		}

		Inventory.GiveDefaultWeapons();
		var bestWeapon = Inventory.GetBestWeapon();
		if ( bestWeapon.IsValid() )
			Inventory.SwitchWeapon( bestWeapon );
	}

	void Local.IPlayerEvents.OnDied( PlayerDiedParams args )
	{
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	void Local.IPlayerEvents.OnPickup( PlayerPickupEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	void Local.IPlayerEvents.OnDrop( PlayerDropEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		_ = SaveLoadoutAfterYield();
	}

	void Local.IPlayerEvents.OnRemoveWeapon( PlayerRemoveWeaponEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		_ = SaveLoadoutAfterYield();
	}

	void Local.IPlayerEvents.OnMoveSlot( PlayerMoveSlotEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	private async Task SaveLoadoutAfterYield()
	{
		await Task.Yield();
		SaveLoadout();
	}

	void Global.ISaveEvents.BeforeSave( string filename )
	{
		if ( !Networking.IsHost ) return;

		var steamId = (long)(Player.Network.Owner?.SteamId ?? 0);
		if ( steamId == 0 ) return;

		var json = SerializeLoadout();
		if ( string.IsNullOrEmpty( json ) ) return;

		SaveSystem.Current?.SetMetadata( $"Loadout_{steamId}", json );
	}

	void Global.ISaveEvents.AfterLoad( string filename )
	{
		if ( !Networking.IsHost ) return;

		var steamId = (long)(Player.Network.Owner?.SteamId ?? 0);
		if ( steamId == 0 ) return;

		var json = SaveSystem.Current?.GetMetadata( $"Loadout_{steamId}" );
		if ( string.IsNullOrEmpty( json ) ) return;

		_ = RestoreLoadoutFromSaveAsync( json );
	}

	private async Task RestoreLoadoutFromSaveAsync( string json )
	{
		await ReplaceLoadoutAsync( json );
	}
}
/// <summary>
/// Dead players become these. They try to observe their last corpse. 
/// </summary>
internal sealed class PlayerObserver : Component
{
	Angles EyeAngles;
	TimeSince timeSinceStarted;
	DeathCameraTarget _cachedCorpse;
	float currentDistance;

	protected override void OnEnabled()
	{
		base.OnEnabled();

		EyeAngles = Scene.Camera.WorldRotation;
		timeSinceStarted = 0;
		currentDistance = 32;

		_cachedCorpse = Scene.GetAllComponents<DeathCameraTarget>()
					.Where( x => x.Connection == Network.Owner )
					.OrderByDescending( x => x.Created )
					.FirstOrDefault();
	}

	protected override void OnUpdate()
	{
		// Don't allow immediate respawn
		if ( timeSinceStarted < 1 )
			return;

		// If pressed a button, or has been too long
		if ( Input.Pressed( "attack1" ) || Input.Pressed( "jump" ) || timeSinceStarted > 4f )
		{
			GameManager.Current?.RequestRespawn();
			GameObject.Destroy();
		}
	}

	protected override void OnPreRender()
	{
		if ( IsProxy ) return;

		if ( _cachedCorpse.IsValid() )
		{
			RotateAround( _cachedCorpse );
		}
	}

	private void RotateAround( Component target )
	{
		// Find the corpse eyes
		if ( target.Components.Get<SkinnedModelRenderer>().TryGetBoneTransform( "pelvis", out var tx ) )
		{
			tx.Position += Vector3.Up * 25;
		}

		var e = EyeAngles;
		e += Input.AnalogLook;
		e.pitch = e.pitch.Clamp( -90, 90 );
		e.roll = 0.0f;
		EyeAngles = e;

		currentDistance = currentDistance.LerpTo( 150, Time.Delta * 5 );

		var center = tx.Position;
		var targetPos = center - EyeAngles.Forward * currentDistance;

		var tr = Scene.Trace.FromTo( center, targetPos ).Radius( 1.0f ).WithoutTags( "ragdoll", "effect" ).Run();

		Scene.Camera.WorldPosition = tr.EndPosition;
		Scene.Camera.WorldRotation = EyeAngles;
	}
}

namespace Sandbox.UI;


public sealed class ResourceSelectAttribute : System.Attribute
{
	public string Extension { get; set; }
	public bool AllowPackages { get; set; }
}
public interface ISpawnMenuTab
{

}

namespace Sandbox.UI;

public class NoticePanel : Panel
{
	bool initialized;
	Vector3.SpringDamped _springy;

	public RealTimeUntil TimeUntilDie;

	/// <summary>
	/// If true, the notice won't auto-dismiss. Call <see cref="Dismiss"/> to remove it.
	/// </summary>
	public bool Manual { get; set; }

	public bool IsDead => !Manual && TimeUntilDie < 0;
	public bool wasDead = false;

	/// <summary>
	/// Dismiss a manual notice, causing it to slide out and be deleted.
	/// </summary>
	public void Dismiss()
	{
		Manual = false;
		TimeUntilDie = 0;
	}

	internal void UpdatePosition( Vector2 vector2 )
	{
		if ( initialized == false )
		{
			_springy = new Vector3.SpringDamped( new Vector3( Screen.Width + 50, vector2.y + Random.Shared.Float( -10, 10 ), 0 ), 0.0f );
			_springy.Velocity = Vector3.Random * 1000;
			initialized = true;
		}

		if ( !Manual && TimeUntilDie < 0.4f )
		{
			vector2.x -= 50;
		}

		// we're dead, push us out to rhe right
		if ( IsDead )
		{
			vector2.x = Screen.Width + 50;

			// we've been dead for 2 seconds, get rid of us
			if ( TimeUntilDie < -2 )
			{
				Delete();
				return;
			}

			wasDead = true;
		}

		_springy.Target = new Vector3( vector2.x, vector2.y, 0 );
		_springy.Frequency = 4;
		_springy.Damping = 0.5f;
		_springy.Update( RealTime.Delta * 1.0f );

		Style.Left = _springy.Current.x * ScaleFromScreen;
		Style.Top = _springy.Current.y * ScaleFromScreen;
	}
}
public static class Extensions
{
	public static Vector3 WithAimCone( this Vector3 direction, float degrees )
	{
		var angle = Rotation.LookAt( direction );
		angle *= new Angles( Game.Random.Float( -degrees / 2.0f, degrees / 2.0f ), Game.Random.Float( -degrees / 2.0f, degrees / 2.0f ), 0 );
		return angle.Forward;
	}

	public static Vector3 WithAimCone( this Vector3 direction, float horizontalDegrees, float verticalDegrees )
	{
		var angle = Rotation.LookAt( direction );
		angle *= new Angles( Game.Random.Float( -verticalDegrees / 2.0f, verticalDegrees / 2.0f ), Game.Random.Float( -horizontalDegrees / 2.0f, horizontalDegrees / 2.0f ), 0 );
		return angle.Forward;
	}
}
using Sandbox.Rendering;

public sealed class CameraWeapon : BaseWeapon
{
	float fov;
	float roll = 0;

	bool focusing;

	[Property] SoundEvent CameraShoot { get; set; }

	/// <summary>
	/// The RT camera's resolution 
	/// </summary>
	private static int _cameraResolution = 512;

	/// <summary>
	/// The render target texture produced by this camera. Read by <see cref="TVEntity"/>.
	/// </summary>
	public Texture RenderTexture => _renderTexture;

	private Texture _renderTexture;
	private CameraComponent _rtCamera;

	public override bool WantsHideHud => true;

	protected override void OnEnabled()
	{
		base.OnEnabled();

		EnsureRTCamera();
		EnsureRenderTexture();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		CleanupRenderTexture();
		_rtCamera = null;
	}

	protected override void OnDestroy()
	{
		CleanupRenderTexture();
		_rtCamera = null;
	}

	protected override void OnPreRender()
	{
		if ( !_rtCamera.IsValid() ) return;

		EnsureRenderTexture();

		if ( HasOwner && Scene.Camera.IsValid() )
		{
			// When held, mirror the player's camera so the TV shows their POV.
			// TODO: network some props to the TV so they show up in the RT camera when held by a player other than the host.
			_rtCamera.WorldPosition = Scene.Camera.WorldPosition;
			_rtCamera.WorldRotation = Scene.Camera.WorldRotation;
			_rtCamera.FieldOfView = Scene.Camera.FieldOfView;

			if ( !_rtCamera.RenderExcludeTags.Has( "viewer" ) )
				_rtCamera.RenderExcludeTags.Add( "viewer" );
		}
		else
		{
			_rtCamera.RenderExcludeTags.Remove( "viewer" );
			_rtCamera.FieldOfView = 40f;
		}
	}

	/// <summary>
	/// We want to control the camera fov when held by a player.
	/// </summary>
	public override void OnCameraSetup( Player player, Sandbox.CameraComponent camera )
	{
		if ( !player.Network.IsOwner || !Network.IsOwner ) return;

		if ( fov > 0 )
			camera.FieldOfView = fov;

		camera.WorldRotation = camera.WorldRotation * new Angles( 0, 0, roll );
	}

	public override void OnCameraMove( Player player, ref Angles angles )
	{
		if ( Input.Down( "attack2" ) )
		{
			angles = default;
		}

		var currentFov = fov > 0 ? fov : Scene.Camera.FieldOfView;
		float sensitivity = currentFov.Remap( 1, 70, 0.01f, 1 );
		angles *= sensitivity;
	}

	public override void OnControl( Player player )
	{
		base.OnControl( player );

		if ( Input.Pressed( "reload" ) )
		{
			fov = 0;
			roll = 0;
		}

		if ( Input.Down( "attack2" ) )
		{
			fov = ((fov > 0 ? fov : Scene.Camera.FieldOfView) + Input.AnalogLook.pitch).Clamp( 1, 150 );
			roll -= Input.AnalogLook.yaw;
		}

		if ( focusing && Input.Released( "attack1" ) )
		{
			Game.TakeScreenshot();
			Sandbox.Services.Stats.Increment( "photos", 1 );

			GameObject?.PlaySound( CameraShoot );
		}

		focusing = Input.Down( "attack1" );
	}

	private void EnsureRTCamera()
	{
		_rtCamera = GetComponentInChildren<CameraComponent>( true );

		if ( _rtCamera is null )
		{
			var go = new GameObject( GameObject, true, "rt_camera" );
			_rtCamera = go.AddComponent<CameraComponent>();
		}

		_rtCamera.IsMainCamera = false;
		_rtCamera.BackgroundColor = Color.Black;
		_rtCamera.ClearFlags = ClearFlags.Color | ClearFlags.Depth | ClearFlags.Stencil;
		_rtCamera.FieldOfView = Scene.Camera.FieldOfView;
		_rtCamera.RenderExcludeTags.Add( "viewmodel" );
	}

	private void EnsureRenderTexture()
	{
		if ( _renderTexture.IsValid() && _renderTexture.Width == _cameraResolution && _renderTexture.Height == _cameraResolution )
			return;

		CleanupRenderTexture();

		_renderTexture = Texture.CreateRenderTarget()
			.WithSize( _cameraResolution, _cameraResolution )
			.Create();

		if ( _rtCamera.IsValid() )
		{
			_rtCamera.RenderTarget = _renderTexture;
		}
	}

	private void CleanupRenderTexture()
	{
		if ( _rtCamera.IsValid() )
		{
			_rtCamera.RenderTarget = null;
		}

		_renderTexture?.Dispose();
		_renderTexture = null;
	}

	public override void DrawHud( HudPainter painter, Vector2 crosshair )
	{
		// nothing!
	}
}
using Sandbox.Rendering;
using Sandbox.Utility;

public sealed class RpgWeapon : BaseWeapon
{
	[Property] public float TimeBetweenShots { get; set; } = 2f;
	[Property] public GameObject ProjectilePrefab { get; set; }
	[Property] public SoundEvent ShootSound { get; set; }
	[Property] public float ProjectileSpeed { get; set; } = 1024f;

	/// <summary>
	/// When enabled, fired rockets will continuously track toward the player's crosshair.
	/// Toggle with right-click (player) or SecondaryInput (standalone/seat).
	/// </summary>
	[Property, Sync, ClientEditable] public bool IsTrackedAim { get; set; } = false;

	public override bool IsTargetedAim => IsTrackedAim;

	[Sync( SyncFlags.FromHost )] RpgProjectile Projectile { get; set; }

	TimeSince TimeSinceShoot;
	private bool _hasFired;
	private bool _waitingForReload;

	/// <summary>
	/// Whether a live rocket is currently being guided toward the crosshair.
	/// </summary>
	public bool IsGuiding => IsTrackedAim && Projectile.IsValid();

	protected override float GetPrimaryFireRate() => TimeBetweenShots;

	public override bool CanSecondaryAttack() => false;

	public override void OnControl( Player player )
	{
		base.OnControl( player );

		if ( Input.Pressed( "attack2" ) )
			ToggleTrackedAim();

		// Auto-reload after firing
		if ( _hasFired && Input.Released( "attack1" ) )
		{
			_hasFired = false;

			if ( IsGuiding )
				_waitingForReload = true;
			else if ( CanReload() )
				OnReloadStart();
		}

		if ( IsGuiding )
		{
			var target = GetAimTarget();
			Projectile.UpdateWithTarget( target, ProjectileSpeed );
		}
		else if ( _waitingForReload )
		{
			_waitingForReload = false;
			if ( CanReload() )
				OnReloadStart();
		}
	}

	/// <summary>
	/// Standalone / seat control — uses SecondaryInput to toggle tracking.
	/// </summary>
	public override void OnControl()
	{
		base.OnControl();

		if ( HasOwner || IsProxy ) return;

		if ( SecondaryInput.Pressed() )
			ToggleTrackedAim();

		if ( IsGuiding )
		{
			var target = GetAimTarget();
			Projectile.UpdateWithTarget( target, ProjectileSpeed );
		}
	}

	[Rpc.Host]
	private void ToggleTrackedAim()
	{
		IsTrackedAim = !IsTrackedAim;
	}

	/// <summary>
	/// Traces from AimRay and returns the world-space point the player is looking at.
	/// </summary>
	private Vector3 GetAimTarget()
	{
		var ray = AimRay;
		var tr = Scene.Trace.Ray( ray, 16384f )
			.IgnoreGameObjectHierarchy( AimIgnoreRoot )
			.WithoutTags( "trigger", "projectile" )
			.Run();

		return tr.Hit ? tr.HitPosition : ray.Position + ray.Forward * 16384f;
	}

	public override void PrimaryAttack()
	{
		if ( HasOwner && !TakeAmmo( 1 ) )
		{
			TryAutoReload();
			return;
		}

		TimeSinceShoot = 0;
		AddShootDelay( TimeBetweenShots );

		if ( ViewModel.IsValid() )
			ViewModel.RunEvent<ViewModel>( x => x.OnAttack() );
		else if ( WorldModel.IsValid() )
			WorldModel.RunEvent<WorldModel>( x => x.OnAttack() );

		if ( ShootSound.IsValid() )
			GameObject.PlaySound( ShootSound );

		var ray = AimRay;
		var muzzlePos = MuzzleTransform.WorldTransform.Position;
		var spawnPos = muzzlePos + ray.Forward * 64f;

		if ( HasOwner )
		{
			spawnPos = CheckThrowPosition( Owner, muzzlePos, spawnPos );

			Owner.Controller.EyeAngles += new Angles( Random.Shared.Float( -0.2f, -0.3f ), Random.Shared.Float( -0.1f, 0.1f ), 0 );

			if ( !Owner.Controller.ThirdPerson && Owner.IsLocalPlayer )
			{
				new Sandbox.CameraNoise.Punch( new Vector3( Random.Shared.Float( 45, 35 ), Random.Shared.Float( -10, -5 ), 0 ), 1.5f, 2, 0.5f );
				new Sandbox.CameraNoise.Shake( 1f, 0.6f );

				_hasFired = true;
			}
		}

		CreateProjectile( spawnPos, ray.Forward, ProjectileSpeed );
	}

	private Vector3 CheckThrowPosition( Player player, Vector3 eyePosition, Vector3 grenadePosition )
	{
		var tr = Scene.Trace.Box( BBox.FromPositionAndSize( Vector3.Zero, 8.0f ), eyePosition, grenadePosition )
			.WithoutTags( "trigger", "ragdoll", "player", "effect" )
			.IgnoreGameObjectHierarchy( player.GameObject )
			.Run();

		if ( tr.Hit )
			return tr.EndPosition;

		return grenadePosition;
	}

	/// <summary>
	/// Creates the projectile with the host's permission
	/// </summary>
	[Rpc.Host]
	void CreateProjectile( Vector3 start, Vector3 direction, float speed )
	{
		var go = ProjectilePrefab?.Clone( start );

		var projectile = go.GetComponent<RpgProjectile>();
		Assert.True( projectile.IsValid(), "RpgProjectile not on projectile prefab" );

		if ( Owner.IsValid() )
			projectile.Instigator = Owner;
		else if ( ClientInput.Current.IsValid() )
			projectile.Instigator = ClientInput.Current;

		go.NetworkSpawn();

		Projectile = projectile;
		projectile.UpdateDirection( direction, speed );
	}

	public override void DrawCrosshair( HudPainter hud, Vector2 center )
	{
		var tss = TimeSinceShoot.Relative.Remap( 0, 0.2f, 1, 0 );
		var w = 2;

		hud.SetBlendMode( BlendMode.Lighten );

		if ( IsTrackedAim )
		{
			// Diamond crosshair when in tracked aim mode
			Color guideColor = IsGuiding ? new Color( 1f, 0.5f, 0.1f ) : CrosshairCanShoot;
			var size = 32f;

			hud.DrawLine( center + new Vector2( 0, -size ), center + new Vector2( size, 0 ), w, guideColor );
			hud.DrawLine( center + new Vector2( size, 0 ), center + new Vector2( 0, size ), w, guideColor );
			hud.DrawLine( center + new Vector2( 0, size ), center + new Vector2( -size, 0 ), w, guideColor );
			hud.DrawLine( center + new Vector2( -size, 0 ), center + new Vector2( 0, -size ), w, guideColor );

			return;
		}

		Color color = !CanPrimaryAttack() ? CrosshairNoShoot : CrosshairCanShoot;

		var squareSize = 64f;

		hud.DrawLine( center + new Vector2( -squareSize / 2, -squareSize / 2 ), center + new Vector2( squareSize / 2, -squareSize / 2 ), w, color );
		hud.DrawLine( center + new Vector2( squareSize / 2, -squareSize / 2 ), center + new Vector2( squareSize / 2, squareSize / 2 ), w, color );
		hud.DrawLine( center + new Vector2( squareSize / 2, squareSize / 2 ), center + new Vector2( -squareSize / 2, squareSize / 2 ), w, color );
		hud.DrawLine( center + new Vector2( -squareSize / 2, squareSize / 2 ), center + new Vector2( -squareSize / 2, -squareSize / 2 ), w, color );
	}
}
using System.Text.Json.Nodes;

/// <summary>
/// Holds a bunch of GameObject json, a bounding box, and some preview models for a
/// duplication. This is what gets serialized to a string and stored in the Duplicator tool.
/// The objects and the bounds are created in selection space. Where the user right clicked to 
/// select is 0,0,0, and the player's view yaw is the rotation identity.
/// </summary>
public class DuplicationData
{
	/// <summary>
	/// An array of JsonObject objects, which are serialzed GameObjects
	/// </summary>
	public JsonArray Objects { get; set; }

	/// <summary>
	/// The bounds are used to work out where to place the duplication, so it
	/// doesn't clip through the floor.
	/// </summary>
	public BBox Bounds { get; set; }

	/// <summary>
	/// Describes where to draw a model for the preview
	/// </summary>
	public record struct PreviewModel( Model Model, Transform Transform, Transform[] Bones, BBox Bounds );

	/// <summary>
	/// A list of preview models to help visualze where the duplication will be placed
	/// </summary>
	public List<PreviewModel> PreviewModels { get; set; }

	/// <summary>
	/// Packages used in this
	/// </summary>
	public List<string> Packages { get; set; }

	/// <summary>
	/// Create DuplicationData from a bunch of objects.
	/// center is the transform to use as the origin for the duplication.
	/// The rotation of center should be the player's view yaw when they made the selection.
	/// </summary>
	public static DuplicationData CreateFromObjects( IEnumerable<GameObject> objects, Transform center )
	{
		var dupe = new DuplicationData();
		dupe.Objects = new JsonArray();
		dupe.Bounds = BBox.FromPositionAndSize( 0, 0.01f );
		dupe.PreviewModels = new();

		List<BBox> worldBounds = new List<BBox>();

		foreach ( var obj in objects )
		{
			var entry = obj.Serialize();
			worldBounds.Add( GetWorldBounds( obj ) );

			var localized = center.ToLocal( obj.WorldTransform );
			entry["Position"] = JsonValue.Create( localized.Position );
			entry["Rotation"] = JsonValue.Create( localized.Rotation );
			entry["Scale"] = JsonValue.Create( localized.Scale );

			dupe.Objects.Add( entry );

			foreach ( var renderer in obj.GetComponentsInChildren<ModelRenderer>() )
			{
				var model = renderer.Model ?? Model.Cube;

				if ( model.IsError ) continue;

				Transform[] bones = null;

				if ( renderer is SkinnedModelRenderer skinned )
				{
					bones = skinned.GetBoneTransforms( false );
				}

				var modelTx = center.ToLocal( renderer.WorldTransform );
				dupe.PreviewModels.Add( new DuplicationData.PreviewModel( model, modelTx, bones, model.Bounds ) );
			}
		}

		if ( worldBounds.Count > 0 )
		{
			var txi = new Transform( -center.Position, center.Rotation.Inverse );

			dupe.Bounds = BBox.FromBoxes( worldBounds.Select( x => x.Transform( txi ) ) );
		}

		var packages = Cloud.ResolvePrimaryAssetsFromJson( dupe.Objects );
		dupe.Packages = packages.Select( x => x.FullIdent ).ToList();


		return dupe;
	}

	public static BBox GetWorldBounds( GameObject go )
	{
		BBox box = BBox.FromPositionAndSize( 0, 0.01f );

		var rb = go.GetComponentsInChildren<Collider>( false, true ).ToArray();
		if ( rb.Length > 0 )
		{
			box = rb[0].GetWorldBounds();

			foreach ( var b in rb )
			{
				box = box.AddBBox( b.GetWorldBounds() );
			}
		}

		return box;
	}
}

[Icon( "🔗" )]
[Title( "#tool.name.linker" )]
[ClassName( "linker" )]
[Group( "#tool.group.constraints" )]
public sealed class LinkerTool : BaseConstraintToolMode
{
	public override string Description => Stage == 1 ? "#tool.hint.linker.stage1" : "#tool.hint.linker.stage0";
	public override string PrimaryAction => Stage == 1 ? "#tool.hint.linker.finish" : "#tool.hint.linker.source";
	public override string ReloadAction => "#tool.hint.linker.remove";

	protected override IEnumerable<GameObject> FindConstraints( GameObject linked, GameObject target )
	{
		foreach ( var link in linked.GetComponentsInChildren<ManualLink>( true ) )
			if ( linked == target || link.Body?.Root == target )
				yield return link.GameObject;
	}

	protected override void CreateConstraint( SelectionPoint point1, SelectionPoint point2 )
	{
		var go1 = new GameObject( point1.GameObject, false, "link" );
		var go2 = new GameObject( point2.GameObject, false, "link" );

		var link1 = go1.AddComponent<ManualLink>();
		var link2 = go2.AddComponent<ManualLink>();

		link1.Body = go2;
		link2.Body = go1;

		go2.NetworkSpawn();
		go1.NetworkSpawn();

		Track( go1, go2 );

		var undo = Player.Undo.Create();
		undo.Name = "Link";
		undo.Add( go1 );
	}
}


[Icon( "➖" )]
[Title( "#tool.name.slider" )]
[ClassName( "slider" )]
[Group( "#tool.group.constraints" )]
public sealed class SliderTool : BaseConstraintToolMode
{
	public override string Description => Stage == 1 ? "#tool.hint.slider.stage1" : "#tool.hint.slider.stage0";
	public override string PrimaryAction => Stage == 1 ? "#tool.hint.slider.finish" : "#tool.hint.slider.source";
	public override string SecondaryAction => Stage == 1 ? "#tool.hint.slider.secondary.stage1" : "#tool.hint.slider.secondary";
	public override string ReloadAction => "#tool.hint.slider.remove";

	protected override IEnumerable<GameObject> FindConstraints( GameObject linked, GameObject target )
	{
		foreach ( var joint in linked.GetComponentsInChildren<SliderJoint>( true ) )
			if ( linked == target || joint.Body?.Root == target )
				yield return joint.GameObject;
	}

	protected override SelectionPoint? GetSecondaryPoint( SelectionPoint select )
	{
		return TraceFromRay( select.WorldTransform().ForwardRay, 4096, select.GameObject );
	}

	protected override void CreateConstraint( SelectionPoint point1, SelectionPoint point2 )
	{
		if ( point1.GameObject == point2.GameObject )
			return;

		var axis = Rotation.LookAt( Vector3.Direction( point1.WorldPosition(), point2.WorldPosition() ) );

		var go1 = new GameObject( false, "slider" );
		go1.Parent = point1.GameObject;
		go1.LocalTransform = point1.LocalTransform;
		go1.WorldRotation = axis;

		var go2 = new GameObject( false, "slider" );
		go2.Parent = point2.GameObject;
		go2.LocalTransform = point2.LocalTransform;
		go2.WorldRotation = axis;

		var cleanup = go1.AddComponent<ConstraintCleanup>();
		cleanup.Attachment = go2;

		var len = point1.WorldPosition().Distance( point2.WorldPosition() );

		var joint = go1.AddComponent<SliderJoint>();
		joint.Body = go2;
		joint.MinLength = 0;
		joint.MaxLength = len;
		joint.EnableCollision = true;

		var lineRenderer = go1.AddComponent<LineRenderer>();
		lineRenderer.Points = [go1, go2];
		lineRenderer.Width = 0.5f;
		lineRenderer.Color = Color.Black;
		lineRenderer.Lighting = true;
		lineRenderer.CastShadows = true;

		go2.NetworkSpawn();
		go1.NetworkSpawn();

		Track( go1, go2 );

		var undo = Player.Undo.Create();
		undo.Name = "Slider";
		undo.Add( go1 );
		undo.Add( go2 );
	}
}
public abstract partial class ToolMode
{
	[Rpc.Broadcast]
	public virtual void ShootEffects( SelectionPoint target )
	{
		if ( !Toolgun.IsValid() ) return;

		var player = Toolgun.Owner;
		if ( !player.IsValid() ) return;

		if ( !target.IsValid() )
		{
			Log.Warning( "ShootEffects: Unknown object" );
			return;
		}

		Toolgun.SpinCoil();

		var muzzle = Toolgun.MuzzleTransform;

		if ( Toolgun.SuccessImpactEffect is GameObject impactPrefab )
		{
			var wt = target.WorldTransform();
			wt.Rotation = wt.Rotation * new Angles( 90, 0, 0 );

			var impact = impactPrefab.Clone( wt, null, false );
			impact.Enabled = true;
		}

		if ( Toolgun.SuccessBeamEffect is GameObject beamEffect )
		{
			var wt = target.WorldTransform();

			var go = beamEffect.Clone( new Transform( muzzle.WorldTransform.Position ), null, false );

			foreach ( var beam in go.GetComponentsInChildren<BeamEffect>( true ) )
			{
				beam.TargetPosition = wt.Position;
			}

			go.Enabled = true;
		}

		Toolgun.ViewModel?.GetComponentInChildren<SkinnedModelRenderer>().Set( "b_attack", true );
	}

	public virtual void ShootFailEffects( SelectionPoint target )
	{

	}

}

global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "goo" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "goo" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "xaz" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "xaz.goo" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "6/14/2026 7:34:04 PM" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "25" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.116.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.116.0")]
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SmoothFloat
{
    public float Current;
    public float Target;
    public float Velocity;
    public float SmoothTime;

    public SmoothFloat(float initial, float smoothTime)
    {
        Current = initial;
        Target = initial;
        Velocity = 0f;
        SmoothTime = smoothTime;
    }

    public void Update(float dt) =>
        Current = MathX.SmoothDamp(Current, Target, ref Velocity, SmoothTime, dt);

    public bool IsSettled =>
        MathF.Abs(Target - Current) < 0.0001f && MathF.Abs(Velocity) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SpringColor
{
    public Color Current;
    public Color Target;
    public Color Velocity;
    public float Frequency;
    public float Damping;

    public SpringColor(Color initial, float frequency, float damping)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        Frequency = frequency;
        Damping = damping;
    }

    public void Update(float dt)
    {
        float vr = Velocity.r, vg = Velocity.g, vb = Velocity.b, va = Velocity.a;
        Current = new Color(
            MathX.SpringDamp(Current.r, Target.r, ref vr, dt, Frequency, Damping),
            MathX.SpringDamp(Current.g, Target.g, ref vg, dt, Frequency, Damping),
            MathX.SpringDamp(Current.b, Target.b, ref vb, dt, Frequency, Damping),
            MathX.SpringDamp(Current.a, Target.a, ref va, dt, Frequency, Damping));
        Velocity = new Color(vr, vg, vb, va);
    }

    public bool IsSettled =>
        MathF.Abs(Target.r - Current.r) < 0.0001f &&
        MathF.Abs(Target.g - Current.g) < 0.0001f &&
        MathF.Abs(Target.b - Current.b) < 0.0001f &&
        MathF.Abs(Target.a - Current.a) < 0.0001f &&
        MathF.Abs(Velocity.r) < 0.0001f &&
        MathF.Abs(Velocity.g) < 0.0001f &&
        MathF.Abs(Velocity.b) < 0.0001f &&
        MathF.Abs(Velocity.a) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SpringColor
{
    public Color Current;
    public Color Target;
    public Color Velocity;
    public float Frequency;
    public float Damping;

    public SpringColor(Color initial, float frequency, float damping)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        Frequency = frequency;
        Damping = damping;
    }

    public void Update(float dt)
    {
        float vr = Velocity.r, vg = Velocity.g, vb = Velocity.b, va = Velocity.a;
        Current = new Color(
            MathX.SpringDamp(Current.r, Target.r, ref vr, dt, Frequency, Damping),
            MathX.SpringDamp(Current.g, Target.g, ref vg, dt, Frequency, Damping),
            MathX.SpringDamp(Current.b, Target.b, ref vb, dt, Frequency, Damping),
            MathX.SpringDamp(Current.a, Target.a, ref va, dt, Frequency, Damping));
        Velocity = new Color(vr, vg, vb, va);
    }

    public bool IsSettled =>
        MathF.Abs(Target.r - Current.r) < 0.0001f &&
        MathF.Abs(Target.g - Current.g) < 0.0001f &&
        MathF.Abs(Target.b - Current.b) < 0.0001f &&
        MathF.Abs(Target.a - Current.a) < 0.0001f &&
        MathF.Abs(Velocity.r) < 0.0001f &&
        MathF.Abs(Velocity.g) < 0.0001f &&
        MathF.Abs(Velocity.b) < 0.0001f &&
        MathF.Abs(Velocity.a) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
namespace Goo.Animation;

public record struct TimelineAnimator
{
    public Timeline Timeline;
    public float    Elapsed;
    public bool     Paused;
    public float    Speed;

    public TimelineAnimator(Timeline timeline)
    {
        Timeline = timeline;
        Elapsed  = 0f;
        Paused   = false;
        Speed    = 1f;
    }

    public void Update(float dt) { if (!Paused) Elapsed += dt * Speed; }
    public void Pause()          { Paused = true; }
    public void Resume()         { Paused = false; }
    public void Restart()        { Elapsed = 0f; Paused = false; }
    public void Seek(float t)    { Elapsed = t; }

    public readonly TimelineSample Sample => Timeline.Eval(Elapsed);

    public readonly bool IsFinished
    {
        get
        {
            if (Timeline.Iterations <= 0) return false;
            if (Timeline.Duration   <= 0f) return true;
            return Elapsed >= Timeline.Duration * Timeline.Iterations;
        }
    }
}

namespace Goo;

/// <summary>Compile-time constraint for blob struct types. Never use as storage, return, or parameter type (boxes the struct, destroys per-Rebuild allocation profile); only valid in where T : struct, IBlob.</summary>
public interface IBlob
{
    static abstract BlobKind Kind { get; }
    string? Key { get; }
    internal void WriteTo(ref Frame frame);
}

/// <summary>Returns the single root Blob for a GooView build. A named delegate rather than
/// Func&lt;IBlob&gt; because IBlob has a static-abstract member (Kind) and C# bars such interfaces
/// as generic type arguments (CS8920). Consequence for Razor markup: a bare method group cannot
/// bind (its natural type is the illegal Func&lt;IBlob&gt;), so write
/// <c>Build=@(new BlobBuilder(MyBuild))</c>. See docs/site/docs/gotchas.md.</summary>
public delegate IBlob BlobBuilder();
using Sandbox.UI;

namespace Goo;

/// <summary>Length helpers for typography properties whose engine unit semantics are non-obvious.</summary>
public static class Typography
{
    /// <summary>CSS-style unitless LineHeight multiplier (e.g. 1.5 = 1.5x of FontSize).</summary>
    public static Length LineHeightMultiplier(float multiplier)
        => Length.Percent(multiplier * 100f).Value;

    /// <summary>CSS-style em-relative LetterSpacing (e.g. -0.02 for tight headings).</summary>
    public static Length LetterSpacingEm(float em)
        => Length.Em(em);
}
using System;
using System.Collections.Generic;
using Sandbox.UI;

namespace Goo.Input;

// Lazily caches one stable Action<MousePanelEvent> per key so successive Builds
// compare equal (Delegate.Equals) and emit no SetEvents op. 0 alloc on cache hit.
public sealed class HandlerTable<TKey> where TKey : notnull
{
    readonly Action<TKey, MousePanelEvent> _action;
    readonly Dictionary<TKey, Action<MousePanelEvent>> _cache = new();

    public HandlerTable(Action<TKey> action) : this((k, _) => action(k)) { }

    public HandlerTable(Action<TKey, MousePanelEvent> action) => _action = action;

    public Action<MousePanelEvent> this[TKey key]
    {
        get
        {
            if (!_cache.TryGetValue(key, out var h))
            {
                var k = key;
                h = e => _action(k, e);
                _cache[key] = h;
            }
            return h;
        }
    }
}
code xaz.goo Controls.csLibrary
using System;
using Sandbox;
using Sandbox.UI;

namespace Goo;

// Composed control factories (compose-list widgets). Stateless: each returns a
// Container subtree, following the Shapes/Skins idiom.
public static partial class Controls
{
    /// <summary>Goo primitive button: unstyled click target wrapping a text label. Pass a null onClick for a display-only button (no handler is wired). Distinct from Components.Button in the app project, which applies brand tokens and visual chrome. Use this to build custom-styled buttons without inheriting app-level styling. Style fields the factory sets (PointerEvents, FlexDirection, AlignItems, JustifyContent, and conditionally HoverBackgroundColor) cannot be overridden via <c>with</c> - first-declared wins; see Hud.Fill for the same constraint.</summary>
    public static Container Button(
        string  label,
        Action? onClick    = null,
        Color?  hoverColor = null,
        string? key        = null )
    {
        return new Container
        {
            Key                  = key,
            PointerEvents        = PointerEvents.All,
            FlexDirection        = FlexDirection.Row,
            AlignItems           = Align.Center,
            JustifyContent       = Justify.Center,
            HoverBackgroundColor = hoverColor,
            OnClick              = onClick is null ? null : _ => onClick(),
            Children             = { new Text( label ) },
        };
    }


    // Maps a cursor X (in the root's rendered pixel frame) to a snapped, clamped value.
    // Ratio-based (localX / width) so it is scale-invariant; engine UI scaling changes
    // the absolute pixels but not the ratio (engine-fact-mousepanelevent-rendered-frame).
    internal static float ValueAt(float localX, float width, float min, float max, float step)
    {
        if (width <= 0f || max <= min) return min;
        float norm = Math.Clamp(localX / width, 0f, 1f);
        float v = min + norm * (max - min);
        if (step > 0f) v = MathF.Round(v / step) * step;
        return Math.Clamp(v, min, max);
    }

    // Opacity multiplier applied to a Container when Disabled = true.
    public const float DisabledOpacity = 0.45f;

    // Canonical disabled dimming; multiplies any declared opacity.
    internal static float ResolveDisabledOpacity(float? declaredOpacity)
        => (declaredOpacity ?? 1f) * DisabledOpacity;

    // Dev-diagnostic sink for the degenerate max<=min case (mirrors Skins.OnZeroBorder).
    public static Action<string>? OnDegenerateRange;

    static readonly Color TrackBg = new( 0.28f, 0.28f, 0.34f, 1f );
    static readonly Color FillBg  = new( 0.55f, 0.78f, 0.95f, 1f );
    static readonly Color ThumbBg = new( 0.95f, 0.96f, 1.00f, 1f );

    // Controlled, stateless horizontal slider: the caller owns value, updates it in onChanged, and re-renders. Press-to-jump + drag within the bar (no engine move-capture; engine-fact-sbox-ui-input-and-drag); key pins reconciler identity so the Active pointer survives per-move re-renders.
    // disabled: when true, sets Disabled on the root container (forces pointer-off and opacity dim); onChanged is not called. Callers that wrap this in their own disabled container must pass disabled=true here and omit Disabled on the wrapper to avoid double-dim (0.45 x 0.45).
    public static Container Slider(
        float value, float min, float max, float step,
        Action<float> onChanged, string? key = null, bool disabled = false )
    {
        if ( max <= min )
            OnDegenerateRange?.Invoke( $"Goo.Controls.Slider: max ({max}) <= min ({min}); rendering an inert track." );

        float pct = (max > min ? Math.Clamp( (value - min) / (max - min), 0f, 1f ) : 0f) * 100f;

        void Set( MousePanelEvent e )
            => onChanged( ValueAt( e.LocalPosition.x, e.Target.Box.Rect.Size.x, min, max, step ) );

        return new Container
        {
            Key            = key,
            Disabled       = disabled ? (bool?)true : null,
            PointerEvents  = PointerEvents.All,
            Width          = Length.Percent( 100 ),
            Height         = 20f,
            FlexDirection  = FlexDirection.Column,
            JustifyContent = Justify.Center,
            OnMouseDown    = Set,                                   // press jumps to position
            OnMouseMove    = e => { if ( e.Target.HasActive ) Set( e ); }, // drag while pressed
            Children =
            {
                // track/fill/thumb are inert: handler-less, variant-less panels resolve to
                // PointerEvents.None, so the slider parent stays e.Target for press/drag.
                new Container
                {
                    Key             = "track",
                    Position        = PositionMode.Relative,        // positioned ancestor for fill/thumb
                    Width           = Length.Percent( 100 ),
                    Height          = 7f,
                    BorderRadius    = 4f,
                    BackgroundColor = TrackBg,
                    Children =
                    {
                        new Container
                        {
                            Key             = "fill",
                            Position        = PositionMode.Absolute,
                            Left            = 0f,
                            Height          = Length.Percent( 100 ),
                            Width           = Length.Percent( pct ),
                            BorderRadius    = 4f,
                            BackgroundColor = FillBg,
                        },
                        new Container
                        {
                            Key             = "thumb",
                            Position        = PositionMode.Absolute,
                            Left            = Length.Percent( pct ),
                            Top             = Length.Percent( 50 ),
                            Width           = 16f,
                            Height          = 16f,
                            BorderRadius    = Length.Percent( 50 ),
                            BackgroundColor = ThumbBg,
                            // Center the thumb on the (x = value, y = track-mid) point.
                            // Length.Percent returns Length?, never null here; ?? default unwraps
                            // it the same way Px.Of does (the codebase's nullable-Length idiom).
                            Transform       = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( -50 ) ?? default ),
                        },
                    },
                },
            },
        };
    }
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Sandbox;

namespace Goo;

internal sealed class Fiber
{
    public BlobKind Kind;
    public string? Key;
    public string Content = "";
    public StyleList Style = StyleList.Empty;
    public List<Fiber>? Children;
    public Texture? Texture;
    public string? Path;          // Image (texture path), ScenePanel (.scene path), SvgPanel (svg path), or WebPanel (URL)
    public Scene? Scene;
    public bool RenderOnce;
    public bool Paused;             // WebPanel only
    public string? Color;
    // TextEntry-only carry-fields. Path slot is reused for the text value.
    public string? Placeholder;
    public int?    MaxLength;
    public bool    Disabled;
    public bool    Numeric;
    public float?  MinValue;
    public float?  MaxValue;
    public string? NumberFormat;
    public bool    Multiline;
    public int?    MinLength;
    public string? CharacterRegex;
    public string? StringRegex;
    public Func<char, bool>?   CanEnterChar;
    public Func<string, bool>? Validate;
    public Action<bool>?       OnValidationChanged;
    public Action<string>? OnChange;
    public Action<string>? OnSubmit;
    public Action?         OnFocus;
    public Action<string>? OnBlur;
    public Action?         OnCancel;
    public bool    IsControlled;
    public BlobEvents PrevEvents;
    public ShapeParams Shape;       // Sector / Arc only
    public Vector2[]? Points;       // Polygon only
    // Custom-shader channel (Container only). Previous-render snapshot for delta comparison.
    public ShaderEffect? Effect;
    // Custom-draw callback (Container only). Previous-render snapshot for delta comparison.
    public DrawCallback? Draw;
    // Declared layout-move transition (Container only). Previous-render snapshot for delta comparison.
    public LayoutTransition? LayoutTransition;
    public object? Instance;        // Cell only: the persistent self-owning state instance
}
// <auto-generated />
// Generated by tools/StyleFacadeEmit. Do not edit by hand.
// Source of truth: tools/StyleFacadeEmit/style-manifest.json
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

public readonly partial record struct Sector
{
    public Length? Width { init => _style = StyleAccumulator.Add(_style, StyleField.Width, value); }
    public Length? Height { init => _style = StyleAccumulator.Add(_style, StyleField.Height, value); }
    public Length? Margin { init => _style = StyleAccumulator.Add(_style, StyleField.Margin, value); }
    public Length? MarginLeft { init => _style = StyleAccumulator.Add(_style, StyleField.MarginLeft, value); }
    public Length? MarginTop { init => _style = StyleAccumulator.Add(_style, StyleField.MarginTop, value); }
    public Length? MarginRight { init => _style = StyleAccumulator.Add(_style, StyleField.MarginRight, value); }
    public Length? MarginBottom { init => _style = StyleAccumulator.Add(_style, StyleField.MarginBottom, value); }
    public Color? BackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundColor, value, StyleValue.FromColor); }
    public Color? BackgroundTint { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundTint, value, StyleValue.FromColor); }
    public Length? Bottom { init => _style = StyleAccumulator.Add(_style, StyleField.Bottom, value); }
    public Length? Left { init => _style = StyleAccumulator.Add(_style, StyleField.Left, value); }
    public Length? Right { init => _style = StyleAccumulator.Add(_style, StyleField.Right, value); }
    public Length? Top { init => _style = StyleAccumulator.Add(_style, StyleField.Top, value); }
    public Length? FlexBasis { init => _style = StyleAccumulator.Add(_style, StyleField.FlexBasis, value); }
    public float? FlexGrow { init => _style = StyleAccumulator.Add(_style, StyleField.FlexGrow, value); }
    public float? FlexShrink { init => _style = StyleAccumulator.Add(_style, StyleField.FlexShrink, value); }
    public Length? MaxHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MaxHeight, value); }
    public Length? MaxWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MaxWidth, value); }
    public Length? MinHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MinHeight, value); }
    public Length? MinWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MinWidth, value); }
    public float? Opacity { init => _style = StyleAccumulator.Add(_style, StyleField.Opacity, value); }
    public PointerEvents? PointerEvents { init => _style = StyleAccumulator.Add(_style, StyleField.PointerEvents, value, StyleValue.FromPointerEvents); }
    public PositionMode? Position { init => _style = StyleAccumulator.Add(_style, StyleField.Position, value, StyleValue.FromPositionMode); }
    public Goo.PanelTransform? Transform { init => _style = StyleAccumulator.Add(_style, StyleField.Transform, value, StyleValue.FromPanelTransform); }
}
using System;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

internal enum StyleField : ushort
{
    None = 0,
    FlexDirection, JustifyContent, AlignItems, Display,
    Width, Height,
    Padding, PaddingLeft, PaddingTop, PaddingRight, PaddingBottom,
    Margin, MarginLeft, MarginTop, MarginRight, MarginBottom,
    Gap, RowGap, ColumnGap,
    BackgroundColor,
    BorderRadius, BorderTopLeftRadius, BorderTopRightRadius,
    BorderBottomRightRadius, BorderBottomLeftRadius,
    // Extended to cover remaining settable PanelStyle properties.
    AlignContent, AlignSelf,
    AspectRatio,
    BackdropFilterBlur, BackdropFilterBrightness, BackdropFilterContrast,
    BackdropFilterHueRotate, BackdropFilterInvert, BackdropFilterSaturate, BackdropFilterSepia,
    BackgroundAngle, BackgroundBlendMode, BackgroundImage, BackgroundPlaybackPaused,
    BackgroundPositionX, BackgroundPositionY, BackgroundRepeat,
    BackgroundSizeX, BackgroundSizeY, BackgroundTint,
    BorderBottomColor, BorderBottomWidth,
    BorderColor,
    BorderImageFill, BorderImageRepeat, BorderImageSource, BorderImageTint,
    BorderImageWidthBottom, BorderImageWidthLeft, BorderImageWidthRight, BorderImageWidthTop,
    BorderLeftColor, BorderLeftWidth,
    BorderRightColor, BorderRightWidth,
    BorderTopColor, BorderTopWidth,
    BorderWidth,
    Bottom,
    CaretColor,
    Cursor,
    FilterBlur, FilterBorderColor, FilterBorderWidth,
    FilterBrightness, FilterContrast, FilterHueRotate, FilterInvert, FilterSaturate,
    FilterSepia, FilterTint,
    FlexBasis, FlexGrow, FlexShrink, FlexWrap,
    FontColor, FontFamily, FontSize,
    FontSmooth, FontStyle, FontVariantNumeric, FontWeight,
    ImageRendering,
    Left, LetterSpacing, LineHeight,
    MaskAngle, MaskImage, MaskMode, MaskPositionX, MaskPositionY,
    MaskRepeat, MaskScope, MaskSizeX, MaskSizeY,
    MaxHeight, MaxWidth, MinHeight, MinWidth,
    MixBlendMode,
    ObjectFit,
    Opacity,
    Order,
    OutlineColor, OutlineOffset, OutlineWidth,
    Overflow, OverflowX, OverflowY,
    PerspectiveOriginX, PerspectiveOriginY,
    PointerEvents,
    Position,
    Right,
    SoundIn, SoundOut,
    TextAlign,
    TextBackgroundAngle,
    TextDecorationColor, TextDecorationLine, TextDecorationSkipInk,
    TextDecorationStyle, TextDecorationThickness,
    TextFilter,
    TextLineThroughOffset,
    TextOverflow, TextOverlineOffset,
    TextStrokeColor, TextStrokeWidth,
    TextTransform,
    TextUnderlineOffset,
    Top,
    Transform,
    TransformOriginX, TransformOriginY,
    WhiteSpace,
    WordBreak, WordSpacing,
    ZIndex,
    HoverBackgroundColor, ActiveBackgroundColor, FocusBackgroundColor,
    HoverFontColor,       ActiveFontColor,       FocusFontColor,
    TransitionMs,
    Disabled,
}

internal enum StyleValueKind : byte
{
    None = 0,
    Length,
    Color,
    FlexDirection,
    Justify,
    Align,
    DisplayMode,
    String,
    Single,
    Boolean,
    Texture,
    Int32,
    Wrap,
    BackgroundRepeat,
    BorderImageFill,
    BorderImageRepeat,
    FontSmooth,
    FontStyle,
    FontVariantNumeric,
    ImageRendering,
    MaskMode,
    MaskScope,
    ObjectFit,
    OverflowMode,
    PointerEvents,
    PositionMode,
    TextAlign,
    TextDecoration,
    TextDecorationStyle,
    TextSkipInk,
    FilterMode,
    TextOverflow,
    TextTransform,
    WhiteSpace,
    WordBreak,
    PanelTransform,
}

internal struct StyleValue : IEquatable<StyleValue>
{
    public StyleValueKind Kind;
    public Length LengthVal;     // valid when Kind == Length
    public Color ColorVal;       // valid when Kind == Color
    public int EnumVal;          // packed enum value for any enum-typed kind
    public int RefSlot;          // packed value for Single (float bits), Boolean (0/1), Int32 (raw)
    public object? RefVal;       // String or Texture reference

    public static StyleValue FromLength(Length v) => new() { Kind = StyleValueKind.Length, LengthVal = v };
    public static StyleValue FromColor(Color v)   => new() { Kind = StyleValueKind.Color, ColorVal = v };
    public static StyleValue FromFlexDirection(FlexDirection v) => new() { Kind = StyleValueKind.FlexDirection, EnumVal = (int)v };
    public static StyleValue FromJustify(Justify v) => new() { Kind = StyleValueKind.Justify, EnumVal = (int)v };
    public static StyleValue FromAlign(Align v)     => new() { Kind = StyleValueKind.Align, EnumVal = (int)v };
    public static StyleValue FromDisplay(DisplayMode v) => new() { Kind = StyleValueKind.DisplayMode, EnumVal = (int)v };

    public static StyleValue FromString(string v) => new() { Kind = StyleValueKind.String, RefVal = v };
    public static StyleValue FromSingle(float v)  => new() { Kind = StyleValueKind.Single, RefSlot = BitConverter.SingleToInt32Bits(v) };
    public static StyleValue FromBoolean(bool v)  => new() { Kind = StyleValueKind.Boolean, RefSlot = v ? 1 : 0 };
    public static StyleValue FromInt32(int v)     => new() { Kind = StyleValueKind.Int32, RefSlot = v };
    public static StyleValue FromTexture(Texture v) => new() { Kind = StyleValueKind.Texture, RefVal = v };
    public static StyleValue FromWrap(Wrap v) => new() { Kind = StyleValueKind.Wrap, EnumVal = (int)v };
    public static StyleValue FromBackgroundRepeat(BackgroundRepeat v) => new() { Kind = StyleValueKind.BackgroundRepeat, EnumVal = (int)v };
    public static StyleValue FromBorderImageFill(BorderImageFill v) => new() { Kind = StyleValueKind.BorderImageFill, EnumVal = (int)v };
    public static StyleValue FromBorderImageRepeat(BorderImageRepeat v) => new() { Kind = StyleValueKind.BorderImageRepeat, EnumVal = (int)v };
    public static StyleValue FromFontSmooth(FontSmooth v) => new() { Kind = StyleValueKind.FontSmooth, EnumVal = (int)v };
    public static StyleValue FromFontStyle(FontStyle v) => new() { Kind = StyleValueKind.FontStyle, EnumVal = (int)v };
    public static StyleValue FromFontVariantNumeric(FontVariantNumeric v) => new() { Kind = StyleValueKind.FontVariantNumeric, EnumVal = (int)v };
    public static StyleValue FromImageRendering(ImageRendering v) => new() { Kind = StyleValueKind.ImageRendering, EnumVal = (int)v };
    public static StyleValue FromMaskMode(MaskMode v) => new() { Kind = StyleValueKind.MaskMode, EnumVal = (int)v };
    public static StyleValue FromMaskScope(MaskScope v) => new() { Kind = StyleValueKind.MaskScope, EnumVal = (int)v };
    public static StyleValue FromObjectFit(ObjectFit v) => new() { Kind = StyleValueKind.ObjectFit, EnumVal = (int)v };
    public static StyleValue FromOverflowMode(OverflowMode v) => new() { Kind = StyleValueKind.OverflowMode, EnumVal = (int)v };
    public static StyleValue FromPointerEvents(PointerEvents v) => new() { Kind = StyleValueKind.PointerEvents, EnumVal = (int)v };
    public static StyleValue FromPositionMode(PositionMode v) => new() { Kind = StyleValueKind.PositionMode, EnumVal = (int)v };
    public static StyleValue FromTextAlign(TextAlign v) => new() { Kind = StyleValueKind.TextAlign, EnumVal = (int)v };
    public static StyleValue FromTextDecoration(TextDecoration v) => new() { Kind = StyleValueKind.TextDecoration, EnumVal = (int)v };
    public static StyleValue FromTextDecorationStyle(TextDecorationStyle v) => new() { Kind = StyleValueKind.TextDecorationStyle, EnumVal = (int)v };
    public static StyleValue FromTextSkipInk(TextSkipInk v) => new() { Kind = StyleValueKind.TextSkipInk, EnumVal = (int)v };
    public static StyleValue FromFilterMode(FilterMode v) => new() { Kind = StyleValueKind.FilterMode, EnumVal = (int)v };
    public static StyleValue FromTextOverflow(TextOverflow v) => new() { Kind = StyleValueKind.TextOverflow, EnumVal = (int)v };
    public static StyleValue FromTextTransform(TextTransform v) => new() { Kind = StyleValueKind.TextTransform, EnumVal = (int)v };
    public static StyleValue FromWhiteSpace(WhiteSpace v) => new() { Kind = StyleValueKind.WhiteSpace, EnumVal = (int)v };
    public static StyleValue FromWordBreak(WordBreak v) => new() { Kind = StyleValueKind.WordBreak, EnumVal = (int)v };

    public static StyleValue FromPanelTransform(Goo.PanelTransform v)
        => new() { Kind = StyleValueKind.PanelTransform, RefVal = v._entries };

    public bool Equals(StyleValue other)
    {
        if (Kind != other.Kind) return false;
        return Kind switch
        {
            StyleValueKind.Length         => LengthVal.Equals(other.LengthVal),
            StyleValueKind.Color          => ColorVal.Equals(other.ColorVal),
            StyleValueKind.String         => ReferenceEquals(RefVal, other.RefVal),
            StyleValueKind.Texture        => ReferenceEquals(RefVal, other.RefVal),
            StyleValueKind.Single         => RefSlot == other.RefSlot,
            StyleValueKind.Boolean        => RefSlot == other.RefSlot,
            StyleValueKind.Int32          => RefSlot == other.RefSlot,
            StyleValueKind.PanelTransform => Goo.PanelTransform.EntriesEqual(
                RefVal as System.Collections.Immutable.ImmutableList<Sandbox.UI.PanelTransform.Entry>,
                other.RefVal as System.Collections.Immutable.ImmutableList<Sandbox.UI.PanelTransform.Entry>),
            _                             => EnumVal == other.EnumVal,
        };
    }

    public override bool Equals(object? obj) => obj is StyleValue v && Equals(v);
    public override int GetHashCode() => Kind switch
    {
        // PanelTransform stores an ImmutableList in RefVal; Equals is structural via
        // EntriesEqual, so the hash must be structural too, hashing RefVal as object
        // would give reference identity and break the Equals/GetHashCode contract.
        StyleValueKind.PanelTransform => HashCode.Combine(
            (byte)Kind,
            Goo.PanelTransform.ComputeEntriesHash(
                RefVal as System.Collections.Immutable.ImmutableList<Sandbox.UI.PanelTransform.Entry>)),
        _ => HashCode.Combine((byte)Kind, LengthVal.GetHashCode(), ColorVal.GetHashCode(), EnumVal, RefSlot, RefVal),
    };
}

internal struct StyleEntry
{
    public StyleField Field;
    public StyleValue Value;
}

internal sealed class StyleList
{
    internal static readonly StyleList Empty = new(readOnly: true);

    StyleEntry[] _items;
    int _count;
    readonly bool _readonly;

    public StyleList() : this(readOnly: false) { }
    StyleList(bool readOnly) { _items = new StyleEntry[4]; _count = 0; _readonly = readOnly; }

    public int Count => _count;
    public ref StyleEntry this[int i] => ref _items[i];

    public void Add(StyleField field, StyleValue value)
    {
        if (_readonly) throw new InvalidOperationException("Cannot mutate StyleList.Empty sentinel.");
        if (_count == _items.Length) Array.Resize(ref _items, _items.Length * 2);
        _items[_count++] = new StyleEntry { Field = field, Value = value };
    }

    public bool TryGet(StyleField field, out StyleValue value)
    {
        for (int i = 0; i < _count; i++)
            if (_items[i].Field == field) { value = _items[i].Value; return true; }
        value = default;
        return false;
    }

    internal void Reset()
    {
        if (_readonly) return;
        Array.Clear(_items, 0, _count);
        _count = 0;
    }

    internal void CopyFrom(StyleList src)
    {
        if (_readonly) throw new InvalidOperationException("Cannot mutate StyleList.Empty sentinel.");
        if (_items.Length < src._count) _items = new StyleEntry[src._items.Length];
        Array.Copy(src._items, _items, src._count);
        _count = src._count;
    }

    public static bool ContentsEqual(StyleList a, StyleList b)
    {
        if (ReferenceEquals(a, b)) return true;
        if (a._count != b._count) return false;
        for (int i = 0; i < a._count; i++)
            if (a._items[i].Field != b._items[i].Field || !a._items[i].Value.Equals(b._items[i].Value))
                return false;
        return true;
    }
}
using System;
using System.Collections.Generic;

namespace Goo;

// Lexically-scoped dynamic token lookup. Push a dict via Scope, read via Get inside the body.
public static class Tokens
{
    [ThreadStatic] private static Stack<IReadOnlyDictionary<string, object>>? _stack;

    public static T Scope<T>(IReadOnlyDictionary<string, object> tokens, Func<T> body)
    {
        var stack = _stack ??= new Stack<IReadOnlyDictionary<string, object>>();
        stack.Push(tokens);
        try { return body(); }
        finally { stack.Pop(); }
    }

    public static T Get<T>(string key)
    {
        if (_stack != null)
        {
            foreach (var dict in _stack)
            {
                if (dict.TryGetValue(key, out var value))
                    return (T)value;
            }
        }
        throw new KeyNotFoundException($"Token '{key}' not found in any active Scope.");
    }

    public static bool TryGet<T>(string key, out T value)
    {
        if (_stack != null)
        {
            foreach (var dict in _stack)
            {
                if (dict.TryGetValue(key, out var obj) && obj is T typed)
                {
                    value = typed;
                    return true;
                }
            }
        }
        value = default!;
        return false;
    }
}
using System;
using System.Linq;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StatefulTextEntry : Sandbox.UI.TextEntry, IStatefulEventHost
{
    internal Action<string>? _onChange;
    internal Action<string>? _onSubmit;
    internal Action? _onFocus;
    internal Action<string>? _onBlur;
    internal Action? _onCancel;

    internal Func<char, bool>?   _canEnterChar;
    internal Func<string, bool>? _validate;
    internal Action<bool>?       _onValidationChanged;
    bool _lastInvalid;

    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal bool _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    public StatefulTextEntry()
    {
        // Wire the engine's OnTextEdited through _onChange for per-keystroke notifications.
        OnTextEdited = newValue => { _onChange?.Invoke(newValue); if (_onChange != null) _requestRebuild?.Invoke(); };
    }

    // AND-compose the Goo predicate after the engine's rules (CharacterRegex / Numeric / Multiline).
    public override bool CanEnterCharacter(char c)
        => base.CanEnterCharacter(c) && (_canEnterChar?.Invoke(c) ?? true);

    // Engine OnPaste has two MaxLength bugs (negative-length crash + under-limit bypass); reimplemented with a correct room clamp.
    public override void OnPaste(string text)
    {
        if (Label.HasSelection())
            Label.ReplaceSelection("");

        // Filter per-character through CanEnterCharacter (also runs the Goo CanEnterChar predicate).
        var pasteResult = new string(text.Where(CanEnterCharacter).ToArray());

        if (MaxLength.HasValue)
        {
            // TextLength here reflects the post-selection-removal length, matching engine ordering.
            int room = MaxLength.Value - TextLength;
            if (room <= 0)
                return;
            if (pasteResult.Length > room)
                pasteResult = pasteResult.Substring(0, room);
        }

        Text ??= "";
        Label.InsertText(pasteResult, CaretPosition);
        Label.MoveCaretPos(pasteResult.Length);

        OnValueChanged();
    }

    // Engine runs UpdateValidation() + OnTextEdited() here; merge the Goo Validate predicate and fire OnValidationChanged on a flip.
    public override void OnValueChanged()
    {
        base.OnValueChanged();
        ApplyPredicateAndNotify();
    }

    // Tighten HasValidationErrors with the Goo predicate, then fire OnValidationChanged (and rebuild) only on a validity transition.
    internal void ApplyPredicateAndNotify()
    {
        if (_validate != null && !_validate(Text ?? string.Empty))
        {
            HasValidationErrors = true;
            SetClass("invalid", true);
        }

        if (HasValidationErrors != _lastInvalid)
        {
            _lastInvalid = HasValidationErrors;
            _onValidationChanged?.Invoke(HasValidationErrors);
            if (_onValidationChanged != null) _requestRebuild?.Invoke();
        }
    }

    // Recompute validity (engine rules + predicate) without an edit event; the Applier calls this after props change.
    internal void RecomputeValidation()
    {
        UpdateValidation();
        ApplyPredicateAndNotify();
    }

    // Engine fires "onsubmit" itself on Enter (no Submit method); hook OnEvent to react.
    protected override void OnEvent(PanelEvent e)
    {
        base.OnEvent(e);
        if (e.Name == "onsubmit") { _onSubmit?.Invoke(Text ?? string.Empty); if (_onSubmit != null) _requestRebuild?.Invoke(); }
        // Escape fires "oncancel" via the engine's Cancel(); value-less, same path as onsubmit.
        if (e.Name == "oncancel") { _onCancel?.Invoke(); if (_onCancel != null) _requestRebuild?.Invoke(); }
    }

    // Call base first so the engine's focus/blur work runs before we observe the committed Text.
    protected override void OnFocus(PanelEvent e)
    {
        base.OnFocus(e);
        if (_onFocus != null) { _onFocus.Invoke(); _requestRebuild?.Invoke(); }
    }

    protected override void OnBlur(PanelEvent e)
    {
        base.OnBlur(e);
        if (_onBlur != null) { _onBlur.Invoke(Text ?? string.Empty); _requestRebuild?.Invoke(); }
    }

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null ||
        _onChange != null || _onSubmit != null ||
        _onFocus != null || _onBlur != null || _onCancel != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    protected override void OnClick(MousePanelEvent e)       { base.OnClick(e);       EventDispatch.Fire(_onClick, e, _requestRebuild); }
    protected override void OnRightClick(MousePanelEvent e)  { base.OnRightClick(e);  EventDispatch.Fire(_onRightClick, e, _requestRebuild); }
    protected override void OnMiddleClick(MousePanelEvent e) { base.OnMiddleClick(e); EventDispatch.Fire(_onMiddleClick, e, _requestRebuild); }
    protected override void OnMouseOver(MousePanelEvent e)   { base.OnMouseOver(e);   EventDispatch.Fire(_onMouseEnter, e, _requestRebuild); }
    protected override void OnMouseOut(MousePanelEvent e)    { base.OnMouseOut(e);    EventDispatch.Fire(_onMouseLeave, e, _requestRebuild); }
    protected override void OnMouseDown(MousePanelEvent e)   { base.OnMouseDown(e);   EventDispatch.Fire(_onMouseDown, e, _requestRebuild); }
    protected override void OnMouseUp(MousePanelEvent e)     { base.OnMouseUp(e);     EventDispatch.Fire(_onMouseUp, e, _requestRebuild); }
    protected override void OnMouseMove(MousePanelEvent e)   { base.OnMouseMove(e);   EventDispatch.Fire(_onMouseMove, e, _requestRebuild); }
}
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SmoothVector2
{
    public Vector2 Current;
    public Vector2 Target;
    public Vector2 Velocity;
    public float SmoothTime;

    public SmoothVector2(Vector2 initial, float smoothTime)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        SmoothTime = smoothTime;
    }

    public void Update(float dt)
    {
        float vx = Velocity.x, vy = Velocity.y;
        Current = new Vector2(
            MathX.SmoothDamp(Current.x, Target.x, ref vx, SmoothTime, dt),
            MathX.SmoothDamp(Current.y, Target.y, ref vy, SmoothTime, dt));
        Velocity = new Vector2(vx, vy);
    }

    public bool IsSettled =>
        MathF.Abs(Target.x - Current.x) < 0.0001f &&
        MathF.Abs(Target.y - Current.y) < 0.0001f &&
        MathF.Abs(Velocity.x) < 0.0001f &&
        MathF.Abs(Velocity.y) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}