{"TotalCount":136,"Files":[{"Ident":"notpointless.chomnr_weapon_importer","Path":"Code/WeaponImporter/WeaponHoldSystem.cs","FileName":"WeaponHoldSystem.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":397047,"IsPrivate":false,"Code":"namespace WeaponImporter;\r\n\r\n/// \u003Csummary\u003E\r\n/// Reads every \u003Csee cref=\u0022WeaponHold\u0022/\u003E\u0027s action triggers after components update and before\r\n/// the animgraph evaluates (Stage.UpdateBones, order -1), so auto-reset graph triggers set by\r\n/// game code in OnUpdate are never missed.\r\n/// \u003C/summary\u003E\r\npublic sealed class WeaponHoldSystem : GameObjectSystem\r\n{\r\n    public WeaponHoldSystem( Scene scene ) : base( scene )\r\n    {\r\n        Listen( Stage.UpdateBones, -1, ReadTriggers, \u0022WeaponHold.ReadTriggers\u0022 );\r\n    }\r\n\r\n    private void ReadTriggers()\r\n    {\r\n        foreach ( var hold in Scene.GetAllComponents\u003CWeaponHold\u003E() )\r\n        {\r\n            if ( hold.Active )\r\n                hold.ReadTriggers();\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Core/Grip/LibraryGripGenerators.cs","FileName":"LibraryGripGenerators.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Numerics;\r\nusing WeaponImporter.Core.Analysis;\r\nusing WeaponImporter.Core.Geometry;\r\nusing WeaponImporter.Core.Grasp;\r\nusing WeaponImporter.Core.Hands;\r\nusing WeaponImporter.Core.Maths;\r\nusing WeaponImporter.Core.Rig;\r\nusing WeaponImporter.Core.Weapon;\r\n\r\nnamespace WeaponImporter.Core.Grip;\r\n\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003E\r\n/// Grips from the library (captured from animations): each preset for the requested hand is\r\n/// placed on the grip surface, refined against the geometry for this hand\u0027s size, and scored.\r\n/// \u003C/summary\u003E\r\npublic sealed class PresetGripGenerator : IGripGenerator\r\n{\r\n    private readonly IReadOnlyList\u003CGripPreset\u003E _presets;\r\n\r\n    /// \u003Csummary\u003EPer hand: only this preset is offered (the user picked it).\u003C/summary\u003E\r\n    public string? OnlyRight { get; init; }\r\n    public string? OnlyLeft { get; init; }\r\n\r\n    public PresetGripGenerator(IEnumerable\u003CGripPreset\u003E presets) =\u003E _presets = presets.ToList();\r\n\r\n    public string Name =\u003E \u0022Library\u0022;\r\n    public bool IsAvailable =\u003E _presets.Count \u003E 0;\r\n\r\n    public IEnumerable\u003CGraspCandidate\u003E Generate(GraspRequest request, CancellationToken cancel = default)\r\n    {\r\n        var list = new List\u003CGraspCandidate\u003E();\r\n        foreach (var preset in _presets)\r\n        {\r\n            cancel.ThrowIfCancellationRequested();\r\n            if (preset.Side != request.Hand.Side || !Compatible(preset.Style, request.Style))\r\n                continue;\r\n            var only = request.Hand.Side == Side.Right ? OnlyRight : OnlyLeft;\r\n            if (!string.IsNullOrEmpty(only) \u0026\u0026 preset.Name != only)\r\n                continue;\r\n            var pose = GripLibrary.Apply(preset, request.Hand, request.Surface);\r\n            ContactRefiner.Refine(request, pose);\r\n            var quality = GraspEvaluator.Evaluate(request, pose);\r\n            list.Add(new GraspCandidate(pose, quality, preset.Name) { Bonus = preset.Priority });\r\n        }\r\n        return list.OrderByDescending(c =\u003E c.Rank);\r\n    }\r\n\r\n    /// \u003Csummary\u003EA handguard grip can cradle a pump and vice versa; a pistol grip grip is a wrap.\u003C/summary\u003E\r\n    public static bool Compatible(GripStyle preset, GripStyle surface) =\u003E preset == surface\r\n        || (preset, surface) is (GripStyle.Cradle, GripStyle.Pump) or (GripStyle.Pump, GripStyle.Cradle)\r\n        || (preset, surface) is (GripStyle.Wrap, GripStyle.Handle) or (GripStyle.Handle, GripStyle.Wrap);\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Several generators competing: their candidates are pooled and ranked together (the solver\r\n/// then re-ranks the best ones with wrist bend and reach). A learned generator such as GrabNet\r\n/// plugs in here the same way; it runs inside the background grip solve, never on the UI thread.\r\n/// \u003C/summary\u003E\r\npublic sealed class CompositeGripGenerator : IGripGenerator\r\n{\r\n    private readonly IReadOnlyList\u003CIGripGenerator\u003E _parts;\r\n\r\n    public CompositeGripGenerator(params IGripGenerator[] parts) =\u003E _parts = parts.Where(p =\u003E p.IsAvailable).ToList();\r\n\r\n    public string Name =\u003E string.Join(\u0022 \u002B \u0022, _parts.Select(p =\u003E p.Name));\r\n    public bool IsAvailable =\u003E _parts.Count \u003E 0;\r\n\r\n    public IEnumerable\u003CGraspCandidate\u003E Generate(GraspRequest request, CancellationToken cancel = default)\r\n        =\u003E _parts.SelectMany(p =\u003E p.Generate(request, cancel)).OrderByDescending(c =\u003E c.Rank).ToList();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The grip source per hand from the user\u0027s choice: \u0022\u0022 = automatic (library grips and the\r\n/// procedural fit compete), \u0022Procedural\u0022 = only the procedural fit, a preset name = that grip.\r\n/// \u003C/summary\u003E\r\npublic sealed class ChoiceGripGenerator : IGripGenerator\r\n{\r\n    public const string Procedural = \u0022Procedural\u0022;\r\n\r\n    private readonly IReadOnlyList\u003CGripPreset\u003E _presets;\r\n    private readonly string _right, _left;\r\n    private readonly ProceduralGripGenerator _procedural = new();\r\n\r\n    public ChoiceGripGenerator(IEnumerable\u003CGripPreset\u003E presets, string? right, string? left)\r\n    {\r\n        _presets = presets.ToList();\r\n        _right = right ?? \u0022\u0022;\r\n        _left = left ?? \u0022\u0022;\r\n    }\r\n\r\n    public string Name =\u003E \u0022Grips\u0022;\r\n    public bool IsAvailable =\u003E true;\r\n\r\n    public IEnumerable\u003CGraspCandidate\u003E Generate(GraspRequest request, CancellationToken cancel = default)\r\n    {\r\n        var choice = request.Hand.Side == Side.Right ? _right : _left;\r\n        if (choice == Procedural)\r\n            return _procedural.Generate(request, cancel);\r\n        if (choice.Length \u003E 0)\r\n        {\r\n            var chosen = new PresetGripGenerator(_presets.Where(p =\u003E p.Name == choice)).Generate(request, cancel).ToList();\r\n            if (chosen.Count \u003E 0)\r\n                return chosen;\r\n        }\r\n        return new CompositeGripGenerator(new PresetGripGenerator(_presets), _procedural).Generate(request, cancel);\r\n    }\r\n\r\n    /// \u003Csummary\u003EOnly the library grips this hand may use (none when the procedural fit is chosen).\u003C/summary\u003E\r\n    public IEnumerable\u003CGraspCandidate\u003E GenerateLibrary(GraspRequest request, CancellationToken cancel = default)\r\n    {\r\n        var choice = request.Hand.Side == Side.Right ? _right : _left;\r\n        if (choice == Procedural)\r\n            return Array.Empty\u003CGraspCandidate\u003E();\r\n        var presets = choice.Length \u003E 0 ? _presets.Where(p =\u003E p.Name == choice) : _presets;\r\n        return new PresetGripGenerator(presets).Generate(request, cancel);\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003ECaptures the grips of a file\u0027s own arms (first-person rigs, animation packs).\u003C/summary\u003E\r\npublic static class GripExtractor\r\n{\r\n    /// \u003Csummary\u003EA hand counts as holding when its palm is this close to the weapon surface (inches).\u003C/summary\u003E\r\n    public const float HoldingDistance = 2.5f;\r\n\r\n    /// \u003Csummary\u003ERanking bonus of grips taken from the weapon\u0027s own animation (see \u003Csee cref=\u0022GripPreset.Priority\u0022/\u003E).\u003C/summary\u003E\r\n    public const float OwnPriority = 2.5f;\r\n\r\n    /// \u003Csummary\u003EThe weapon\u0027s own grips, preferred when grips compete.\u003C/summary\u003E\r\n    public static List\u003CGripPreset\u003E ExtractOwn(WeaponAnalysis a, string source)\r\n    {\r\n        var list = Extract(a, source);\r\n        foreach (var p in list)\r\n            p.Priority = OwnPriority;\r\n        return list;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The hands in the analysed file\u0027s reference pose (canonical weapon space), captured on the\r\n    /// grip surface each one holds. Hands that aren\u0027t on the weapon are skipped.\r\n    /// \u003C/summary\u003E\r\n    public static List\u003CGripPreset\u003E Extract(WeaponAnalysis a, string source)\r\n    {\r\n        var list = new List\u003CGripPreset\u003E();\r\n        var skeleton = a.Asset.Skeleton;\r\n        if (skeleton.Count == 0 || a.WeaponBvh.IsEmpty)\r\n            return list;\r\n        var world = ReferenceWorld(a);\r\n        foreach (var side in new[] { Side.Right, Side.Left })\r\n        {\r\n            var rig = HandRig.Build(skeleton, side);\r\n            if (rig is null || rig.Fingers.Count \u003C 3)\r\n                continue;\r\n            var palm = world[rig.Hand].TransformPoint(rig.PalmCenter);\r\n            var candidate = side == Side.Right ? a.Primary : a.Support;\r\n            GripSurface? surface = null;\r\n            var style = side == Side.Right ? (a.Type.Type == WeaponType.Melee ? GripStyle.Handle : GripStyle.Wrap) : GripStyle.Cradle;\r\n            if (candidate is not null \u0026\u0026 Vector3.Distance(candidate.Surface.Contact, palm) \u003C rig.HandLength)\r\n            {\r\n                surface = candidate.Surface;\r\n                style = candidate.Style;\r\n            }\r\n            else\r\n            {\r\n                var hint = side == Side.Right ? Vector3.UnitZ : Vector3.UnitX;\r\n                surface = SurfaceProbe.FromPoint(a.WeaponBvh, palm, hint);\r\n            }\r\n            if (surface is null || Vector3.Distance(surface.Contact, palm) \u003E HoldingDistance \u002B rig.PalmThickness)\r\n                continue;\r\n            var name = $\u0022{source} \u00B7 {(side == Side.Right ? \u0022right\u0022 : \u0022left\u0022)}\u0022;\r\n            var preset = GripLibrary.Capture(rig, world, surface, style, name, source);\r\n            // A palm inside the surface means the \u0022weapon\u0022 is the arms themselves (an arms-only\r\n            // file) or the hand isn\u0027t really holding it.\r\n            if (preset.Palm[2] \u003C -0.12f)\r\n                continue;\r\n            preset.WeaponType = a.Type.Type.ToString();\r\n            list.Add(preset);\r\n        }\r\n        return list;\r\n    }\r\n\r\n    /// \u003Csummary\u003EWorld transforms of the analysis reference frame (bind pose without a clip).\u003C/summary\u003E\r\n    public static XForm[] ReferenceWorld(WeaponAnalysis a)\r\n    {\r\n        var skeleton = a.Asset.Skeleton;\r\n        var clip = a.Asset.FindClip(a.ReferenceClip);\r\n        if (clip is { FrameCount: \u003E 0 })\r\n        {\r\n            var frame = Math.Clamp(a.ReferenceFrame, 0, clip.FrameCount - 1);\r\n            return new Pose(clip.Frames[frame]).ToWorld(skeleton);\r\n        }\r\n        return skeleton.RestWorld.ToArray();\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Core/Setup/ActionContacts.cs","FileName":"ActionContacts.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Numerics;\r\nusing WeaponImporter.Core.Analysis;\r\nusing WeaponImporter.Core.Grip;\r\nusing WeaponImporter.Core.Hands;\r\nusing WeaponImporter.Core.Ik;\r\nusing WeaponImporter.Core.Maths;\r\n\r\nnamespace WeaponImporter.Core.Setup;\r\n\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003EA character action sampled over its normalized time (see CharacterPoser.SampleTrack).\u003C/summary\u003E\r\npublic sealed record ActionTrack(AnimationRole Role, float Seconds, IReadOnlyList\u003C(float Time, CharacterPose Pose)\u003E Samples);\r\n\r\n/// \u003Csummary\u003EResult of planning the support hand over the actions.\u003C/summary\u003E\r\npublic sealed class ActionContactPlan\r\n{\r\n    /// \u003Csummary\u003ESupport hand on the magazine (weapon space), when the weapon has one.\u003C/summary\u003E\r\n    public HandPose? Magazine { get; set; }\r\n    public GripRegion? MagazineRegion { get; set; }\r\n    public Dictionary\u003CAnimationRole, ContactPlanner.Plan\u003E Plans { get; } = new();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Plans the support hand over the character\u0027s actions once the grip is solved: where the\r\n/// animated hand goes relative to the weapon (it follows the hold bone), where it should touch\r\n/// this weapon instead (the magazine during reloads), and the contact keys that get it there with\r\n/// the smallest change to the animation. Pure maths; the samples come from the editor.\r\n/// \u003C/summary\u003E\r\npublic static class ActionContacts\r\n{\r\n    public static ActionContactPlan Plan(WeaponAnalysis weapon, WeaponSetup setup, CharacterRig character, GripSolution grip, IReadOnlyList\u003CActionTrack\u003E tracks, GripOptions? options = null, CancellationToken cancel = default)\r\n    {\r\n        var plan = new ActionContactPlan();\r\n        if (grip.Left is null || character.Left is not { } leftRig || !setup.UseSupportHand)\r\n            return plan;\r\n        var region = GripRegions.Magazine(weapon);\r\n        plan.MagazineRegion = region;\r\n\r\n        // Animated support wrist in weapon space, per sample.\r\n        List\u003CContactPlanner.Sample\u003E Samples(ActionTrack track) =\u003E track.Samples\r\n            .Select(s =\u003E new ContactPlanner.Sample(s.Time, XForm.ToLocal(GripSolver.WeaponWorld(s.Pose, character, grip.WeaponInHold), s.Pose.World[leftRig.Hand])))\r\n            .ToList();\r\n\r\n        var reloads = tracks.Where(t =\u003E ActionTiming.ReloadRoles.Contains(t.Role)).ToList();\r\n        if (!setup.ReloadTouchesMagazine)\r\n        {\r\n            // Default: the character reloads as animated; the hand only lets go and comes back.\r\n            foreach (var track in reloads)\r\n                plan.Plans[track.Role] = ContactPlanner.PlanRelease(Samples(track), track.Seconds);\r\n            return plan;\r\n        }\r\n        if (region?.Surface is not null \u0026\u0026 reloads.Count \u003E 0)\r\n        {\r\n            // Fit the magazine grab closest to where the animation brings the hand.\r\n            var first = Samples(reloads[0]);\r\n            var nearest = first.OrderBy(s =\u003E Vector3.Distance(s.Wrist.Pos, region.Surface.Contact)).First();\r\n            plan.Magazine = GripSolver.SolveOnRegion(weapon, region, character, grip, nearest.Wrist, options, cancel);\r\n        }\r\n\r\n        foreach (var track in reloads)\r\n        {\r\n            cancel.ThrowIfCancellationRequested();\r\n            if (plan.Magazine is null)\r\n                break;\r\n            plan.Plans[track.Role] = ContactPlanner.PlanReload(Samples(track), grip.Left.Wrist, plan.Magazine.Wrist, track.Seconds, weapon.WeaponBounds);\r\n        }\r\n        return plan;\r\n    }\r\n\r\n    /// \u003Csummary\u003EWrites the planned tracks into the setup, leaving tracks the user edited alone.\u003C/summary\u003E\r\n    public static void Apply(WeaponSetup setup, ActionContactPlan plan)\r\n    {\r\n        foreach (var (role, p) in plan.Plans)\r\n        {\r\n            if (setup.Contacts.TryGetValue(role, out var existing) \u0026\u0026 existing.Keys.Count \u003E 0 \u0026\u0026 !existing.Generated)\r\n                continue;\r\n            setup.Contacts[role] = new ContactTrack { Keys = p.Keys, Generated = true, Note = p.Note };\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Tool/UI/Steps/CheckStep.cs","FileName":"CheckStep.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"using Editor;\r\nusing Sandbox;\r\nusing WeaponImporter.Core.Setup;\r\n\r\nnamespace WeaponImporter.Tool.UI;\r\n\r\n/// \u003Csummary\u003EStep 4: what still needs attention, then Bake and the generated files.\u003C/summary\u003E\r\npublic sealed class CheckStep : StepPanel\r\n{\r\n\tpublic CheckStep( Widget parent, ImporterController controller ) : base( parent, controller )\r\n\t{\r\n\t\tcontroller.BusyChanged \u002B= MarkDirty;\r\n\t}\r\n\r\n\tpublic override string Title =\u003E \u0022Export\u0022;\r\n\r\n\tprotected override string StructureKey()\r\n\t{\r\n\t\tvar checks = string.Join( \u0022;\u0022, C.Session.Checks.Select( c =\u003E $\u0022{c.Name}:{c.Severity}:{c.Message}\u0022 ) );\r\n\t\treturn $\u0022{checks}|{C.Busy}|{C.LastBake?.GetHashCode()}\u0022;\r\n\t}\r\n\r\n\tprotected override void Build()\r\n\t{\r\n\t\tBuildChecks();\r\n\t\tBuildBake();\r\n\t}\r\n\r\n\tprivate void BuildChecks()\r\n\t{\r\n\t\tvar checks = C.Session.Checks;\r\n\t\tvar problems = Validation.Problems( checks ).ToList();\r\n\t\tvar card = AddCard( \u0022fact_check\u0022, \u0022Validation\u0022, out var header, \u0022Everything the importer verified\u0022 );\r\n\t\theader.AddStretchCell();\r\n\t\tif ( problems.Count == 0 )\r\n\t\t\theader.Add( new Pill( card, \u0022ALL GOOD\u0022, Theme.Green ) );\r\n\t\telse\r\n\t\t\theader.Add( new Pill( card, $\u0022{problems.Count} TO LOOK AT\u0022, problems.Any( p =\u003E p.Severity == CheckSeverity.Error ) ? Theme.Red : Theme.Yellow ) );\r\n\r\n\t\tforeach ( var check in problems )\r\n\t\t\tcard.Layout.Add( new CheckLine( card, check, expanded: true, () =\u003E Fix( check ) ) );\r\n\t\tif ( problems.Count \u003E 0 )\r\n\t\t\tcard.Layout.AddSpacingCell( 4 );\r\n\t\tforeach ( var check in checks.Where( c =\u003E !c.NeedsAttention ) )\r\n\t\t\tcard.Layout.Add( new CheckLine( card, check, expanded: false, check.AutoFix is null ? null : () =\u003E Fix( check ) ) );\r\n\t\tif ( checks.Count == 0 )\r\n\t\t\tcard.Layout.Add( UiStyle.Muted( new Label( \u0022Nothing checked yet.\u0022, card ) ) );\r\n\t}\r\n\r\n\tprivate void Fix( CheckResult check )\r\n\t{\r\n\t\tSafe( () =\u003E\r\n\t\t{\r\n\t\t\tcheck.AutoFix?.Invoke();\r\n\t\t\tC.Session.Revalidate();\r\n\t\t\tC.MarkChanged();\r\n\t\t\tC.ScheduleResolve( 0.01f );\r\n\t\t\tC.SetStatus( $\u0022{check.Name}: {check.AutoFixLabel.ToLowerInvariant()} applied.\u0022, Theme.Green );\r\n\t\t} );\r\n\t}\r\n\r\n\tprivate void BuildBake()\r\n\t{\r\n\t\tvar session = C.Session;\r\n\t\tvar card = AddCard( \u0022inventory_2\u0022, \u0022Bake\u0022, out _, \u0022Write the model, animations, attachments and a ready prefab\u0022 );\r\n\t\tcard.Layout.Add( UiStyle.Muted( new Label( $\u0022Output: {session.OutputFolder}/\u0022, card ) { WordWrap = true, ToolTip = \u0022Files are written here inside the project\u0027s assets\u0022 }, small: true ) );\r\n\t\tvar s = session.Setup;\r\n\t\tif ( session.Analysis?.ArmBones.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar fp = card.Layout.Add( new Checkbox( \u0022Bake the first-person viewmodel\u0022, card ) { Value = s.ExportFirstPerson } );\r\n\t\t\tfp.ToolTip = \u0022Also write \u003Cname\u003E_fp.vmdl: this file\u0027s own arms, weapon and camera with every animation as authored. The prefab shows it to the player holding the weapon, in sync with the third-person hold.\u0022;\r\n\t\t\tfp.StateChanged = _ =\u003E s.ExportFirstPerson = fp.Value;\r\n\t\t}\r\n\t\tvar sizeRow = UiStyle.FieldRow( card, card.Layout, \u0022Textures\u0022, \u0022Largest texture size in the baked weapon; bigger images are scaled down (smaller files, same look at normal distances)\u0022 );\r\n\t\tvar size = sizeRow.Add( UiStyle.Framed( new ComboBox( card ) ), 1 );\r\n\t\tforeach ( var (label, value) in new[] { (\u0022Original size\u0022, 0), (\u00224096\u0022, 4096), (\u00222048\u0022, 2048), (\u00221024\u0022, 1024) } )\r\n\t\t{\r\n\t\t\tvar v = value;\r\n\t\t\tsize.AddItem( label, \u0022photo_size_select_large\u0022, () =\u003E s.MaxTextureSize = v, selected: s.MaxTextureSize == value );\r\n\t\t}\r\n\t\tvar corrected = card.Layout.Add( new Checkbox( \u0022Bake corrected third-person animations\u0022, card ) { Value = s.BakeCorrectedAnimations } );\r\n\t\tcorrected.ToolTip = \u0022Also record the character\u0027s actions with the hands on this weapon as new clips (\u003Cname\u003E_corrected.vmdl). The character\u0027s own animations are never changed.\u0022;\r\n\t\tcorrected.StateChanged = _ =\u003E s.BakeCorrectedAnimations = corrected.Value;\r\n\t\tvar use = card.Layout.Add( new Checkbox( \u0022Play the corrected clips in the prefab\u0022, card ) { Value = s.UseCorrectedAnimations } );\r\n\t\tuse.ToolTip = \u0022Off: the prefab corrects the character\u0027s live animation every frame (works with any animgraph blend). On: it plays the baked corrected clips over the upper body.\u0022;\r\n\t\tuse.StateChanged = _ =\u003E s.UseCorrectedAnimations = use.Value;\r\n\t\tvar bake = card.Layout.Add( new Button.Primary( \u0022Bake\u0022, card ) { Icon = \u0022inventory_2\u0022, Tint = Theme.Green, FixedHeight = 36, Enabled = !C.Busy, ToolTip = \u0022Write and compile the weapon model and its prefab\u0022 } );\r\n\t\tbake.Clicked = () =\u003E _ = C.BakeAsync();\r\n\r\n\t\tvar result = C.LastBake;\r\n\t\tif ( result is null )\r\n\t\t\treturn;\r\n\r\n\t\tcard.Layout.Add( new SectionHeader( card, result.Success ? \u0022Baked\u0022 : \u0022Bake finished with errors\u0022 ) );\r\n\t\tvar links = card.Layout.AddRow();\r\n\t\tlinks.Spacing = 6;\r\n\t\tlinks.Add( new UiButton( card, \u0022Open model\u0022, \u0022view_in_ar\u0022, () =\u003E Open( result.ModelPath ), result.ModelPath ) { Enabled = FindAsset( result.ModelPath ) is not null } );\r\n\t\tlinks.Add( new UiButton( card, \u0022Open prefab\u0022, \u0022inventory_2\u0022, () =\u003E Open( result.PrefabPath ), result.PrefabPath ) { Enabled = FindAsset( result.PrefabPath ) is not null } );\r\n\t\tlinks.Add( new UiButton( card, \u0022Show in asset browser\u0022, \u0022folder_open\u0022, () =\u003E Reveal( result.ModelPath ), \u0022Reveal the generated model in the asset browser\u0022 ) );\r\n\t\tlinks.AddStretchCell();\r\n\r\n\t\tforeach ( var file in result.Files )\r\n\t\t{\r\n\t\t\tvar errors = result.Errors.TryGetValue( file, out var list ) ? list : null;\r\n\t\t\tvar row = card.Layout.AddRow();\r\n\t\t\trow.Spacing = 6;\r\n\t\t\trow.Add( new IconLabel( card, errors is null ? \u0022check_circle\u0022 : \u0022error\u0022, errors is null ? Theme.Green : Theme.Red ) );\r\n\t\t\trow.Add( new Label( file, card ) { ToolTip = AssetCompiler.Absolute( file ) }, 1 );\r\n\t\t\tif ( errors is not null )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var e in errors.Take( 6 ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar er = card.Layout.AddRow();\r\n\t\t\t\t\ter.AddSpacingCell( 26 );\r\n\t\t\t\t\ter.Add( UiStyle.Colored( new Label( e, card ) { WordWrap = true }, Theme.Red ), 1 );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tforeach ( var (file, errors) in result.Errors.Where( kv =\u003E !result.Files.Contains( kv.Key ) ) )\r\n\t\t\tcard.Layout.Add( UiStyle.Colored( new Label( $\u0022{file}: {errors.FirstOrDefault()}\u0022, card ) { WordWrap = true }, Theme.Red ) );\r\n\t\tforeach ( var note in result.Notes )\r\n\t\t\tcard.Layout.Add( UiStyle.Muted( new Label( note, card ) { WordWrap = true }, small: true ) );\r\n\t}\r\n\r\n\tprivate static Asset FindAsset( string relative ) =\u003E string.IsNullOrEmpty( relative ) ? null : AssetSystem.FindByPath( relative );\r\n\r\n\tprivate void Open( string relative )\r\n\t{\r\n\t\tSafe( () =\u003E\r\n\t\t{\r\n\t\t\tvar asset = FindAsset( relative );\r\n\t\t\tif ( asset is null )\r\n\t\t\t\tC.SetStatus( $\u0022{relative} is not in the asset system yet.\u0022, Theme.Yellow );\r\n\t\t\telse\r\n\t\t\t\tasset.OpenInEditor();\r\n\t\t} );\r\n\t}\r\n\r\n\tprivate void Reveal( string relative )\r\n\t{\r\n\t\tSafe( () =\u003E\r\n\t\t{\r\n\t\t\tvar asset = FindAsset( relative );\r\n\t\t\tif ( asset is not null )\r\n\t\t\t\tAssetBrowser.OpenTo( asset );\r\n\t\t\telse\r\n\t\t\t\tEditorUtility.OpenFolder( System.IO.Path.GetDirectoryName( AssetCompiler.Absolute( relative ) ) );\r\n\t\t} );\r\n\t}\r\n\r\n\tpublic override void OnDestroyed()\r\n\t{\r\n\t\tC.BusyChanged -= MarkDirty;\r\n\t\tbase.OnDestroyed();\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\u0022\u2713 Skeleton\u0022 / \u0022! Left wrist penetration detected\u0022 / \u0022\u00D7 Muzzle missing\u0022.\u003C/summary\u003E\r\npublic sealed class CheckLine : Widget\r\n{\r\n\tprivate readonly CheckResult _check;\r\n\tprivate readonly bool _expanded;\r\n\r\n\tpublic CheckLine( Widget parent, CheckResult check, bool expanded, Action fix ) : base( parent )\r\n\t{\r\n\t\t_check = check;\r\n\t\t_expanded = expanded;\r\n\t\tLayout = Layout.Row();\r\n\t\tLayout.Spacing = 8;\r\n\t\tLayout.Margin = expanded ? new Sandbox.UI.Margin( 10, 7, 7, 7 ) : new Sandbox.UI.Margin( 10, 1, 4, 1 );\r\n\t\tvar color = ColorOf( check.Severity );\r\n\t\tLayout.Add( new Glyph( this, GlyphOf( check.Severity ), color ) );\r\n\t\tvar text = Layout.AddColumn( 1 );\r\n\t\ttext.Spacing = 2;\r\n\t\tif ( expanded )\r\n\t\t{\r\n\t\t\ttext.Add( UiStyle.Colored( new Label( check.Name, this ), color, bold: true ) );\r\n\t\t\tif ( !string.IsNullOrEmpty( check.Message ) )\r\n\t\t\t\ttext.Add( new Label( check.Message, this ) { WordWrap = true } );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar line = text.AddRow();\r\n\t\t\tline.Spacing = 6;\r\n\t\t\tline.Add( new Label( check.Name, this ) );\r\n\t\t\tif ( !string.IsNullOrEmpty( check.Message ) )\r\n\t\t\t\tline.Add( UiStyle.Muted( new Label( check.Message, this ) { ToolTip = check.Message, WordWrap = true, MinimumWidth = 20 }, small: true ), 1 );\r\n\t\t\telse\r\n\t\t\t\tline.AddStretchCell();\r\n\t\t}\r\n\t\tif ( fix is not null \u0026\u0026 check.AutoFix is not null )\r\n\t\t{\r\n\t\t\tvar button = Layout.Add( new UiButton( this, check.AutoFixLabel, \u0022auto_fix_high\u0022, null, $\u0022Apply the safe fix for: {check.Name}\u0022 ) );\r\n\t\t\tbutton.Clicked = fix;\r\n\t\t}\r\n\t\tToolTip = string.IsNullOrEmpty( check.Message ) ? check.Name : $\u0022{check.Name}: {check.Message}\u0022;\r\n\t}\r\n\r\n\tpublic static Color ColorOf( CheckSeverity s ) =\u003E s switch\r\n\t{\r\n\t\tCheckSeverity.Error =\u003E Theme.Red,\r\n\t\tCheckSeverity.Warning =\u003E Theme.Yellow,\r\n\t\tCheckSeverity.Info =\u003E Theme.Blue,\r\n\t\t_ =\u003E Theme.Green,\r\n\t};\r\n\r\n\tpublic static string GlyphOf( CheckSeverity s ) =\u003E s switch\r\n\t{\r\n\t\tCheckSeverity.Error =\u003E \u0022\u00D7\u0022,\r\n\t\tCheckSeverity.Warning =\u003E \u0022!\u0022,\r\n\t\tCheckSeverity.Info =\u003E \u0022i\u0022,\r\n\t\t_ =\u003E \u0022\u2713\u0022,\r\n\t};\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tif ( !_expanded )\r\n\t\t\treturn;\r\n\t\tPaint.Antialiasing = true;\r\n\t\tvar color = ColorOf( _check.Severity );\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( color.WithAlpha( .07f ) );\r\n\t\tPaint.DrawRect( LocalRect, 4 );\r\n\t\tPaint.SetBrush( color );\r\n\t\tPaint.DrawRect( new Rect( 0, 0, 3, Height ), 1.5f );\r\n\t}\r\n\r\n\tprivate sealed class Glyph : Widget\r\n\t{\r\n\t\tprivate readonly string _glyph;\r\n\t\tprivate readonly Color _color;\r\n\r\n\t\tpublic Glyph( Widget parent, string glyph, Color color ) : base( parent )\r\n\t\t{\r\n\t\t\t_glyph = glyph;\r\n\t\t\t_color = color;\r\n\t\t\tFixedSize = 16;\r\n\t\t}\r\n\r\n\t\tprotected override void OnPaint()\r\n\t\t{\r\n\t\t\tPaint.SetPen( _color );\r\n\t\t\tPaint.SetDefaultFont( 9, 700 );\r\n\t\t\tPaint.DrawText( LocalRect, _glyph, TextFlag.Center );\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Tool/UI/Timeline/TimelineCard.cs","FileName":"TimelineCard.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"using Editor;\r\nusing Sandbox;\r\nusing WeaponImporter.Core.Analysis;\r\n\r\nnamespace WeaponImporter.Tool.UI;\r\n\r\n/// \u003Csummary\u003ETransport for the selected action (play, step, speed, loop, scrub) above its contact and event lanes.\u003C/summary\u003E\r\npublic sealed class TimelineCard : Card\r\n{\r\n\tprivate readonly ImporterController _c;\r\n\tprivate readonly IconButton _play;\r\n\tprivate readonly IconButton _loop;\r\n\tprivate readonly FloatSlider _scrub;\r\n\tprivate readonly Label _clock;\r\n\tprivate readonly Label _role;\r\n\tprivate readonly TimelineLanes _lanes;\r\n\tprivate bool _scrubbing;\r\n\r\n\tpublic TimelineCard( Widget parent, ImporterController controller ) : base( parent )\r\n\t{\r\n\t\t_c = controller;\r\n\t\tLayout.Margin = new Sandbox.UI.Margin( 10, 6, 10, 8 );\r\n\t\tLayout.Spacing = 4;\r\n\r\n\t\tvar transport = Layout.AddRow();\r\n\t\ttransport.Spacing = 6;\r\n\t\t_role = transport.Add( UiStyle.Bold( new Label( \u0022Idle\u0022, this ) { FixedWidth = TimelineLanes.LabelWidth - 6, FixedHeight = UiStyle.ControlHeight, ToolTip = \u0022The action being previewed; pick another in the preview header or in Animations\u0022 } ) );\r\n\t\t_play = transport.Add( UiStyle.Icon( this, \u0022pause\u0022, controller.TogglePlay, \u0022Play / pause (Space)\u0022 ) );\r\n\t\ttransport.Add( UiStyle.Icon( this, \u0022chevron_left\u0022, () =\u003E controller.StepFrame( -1 ), \u0022Previous frame (Left arrow)\u0022 ) );\r\n\t\ttransport.Add( UiStyle.Icon( this, \u0022chevron_right\u0022, () =\u003E controller.StepFrame( 1 ), \u0022Next frame (Right arrow)\u0022 ) );\r\n\t\t_scrub = transport.Add( new FloatSlider( this ) { Minimum = 0, Maximum = 1, Value = 0, ToolTip = \u0022Scrub through the action\u0022, FixedHeight = UiStyle.ControlHeight }, 1 );\r\n\t\t_scrub.OnValueEdited = () =\u003E\r\n\t\t{\r\n\t\t\t_scrubbing = true;\r\n\t\t\tcontroller.Seek( _scrub.Value );\r\n\t\t\t_scrubbing = false;\r\n\t\t};\r\n\t\t_clock = transport.Add( UiStyle.Muted( new Label( \u00220.00 s\u0022, this ) { FixedWidth = 92, FixedHeight = UiStyle.ControlHeight, Alignment = TextFlag.RightCenter, ToolTip = \u0022Time in the action / its length\u0022 } ) );\r\n\t\tvar speed = transport.Add( UiStyle.Framed( new ComboBox( this ) { FixedWidth = 70, ToolTip = \u0022Playback speed\u0022 } ) );\r\n\t\tforeach ( var s in new[] { 0.25f, 0.5f, 1f, 2f } )\r\n\t\t{\r\n\t\t\tvar v = s;\r\n\t\t\tspeed.AddItem( $\u0022{s:0.##}\u00D7\u0022, null, () =\u003E controller.Speed = v, selected: s == 1f );\r\n\t\t}\r\n\t\t_loop = transport.Add( UiStyle.Toggle( this, \u0022repeat\u0022, true, null, \u0022Loop the action\u0022 ) );\r\n\t\t_loop.OnToggled = on =\u003E controller.Loop = on;\r\n\r\n\t\t_lanes = Layout.Add( new TimelineLanes( this, controller ) );\r\n\r\n\t\tcontroller.RoleChanged \u002B= Refresh;\r\n\t\tcontroller.Changed \u002B= Refresh;\r\n\t}\r\n\r\n\tprivate void Refresh()\r\n\t{\r\n\t\t_role.Text = AnimationRoles.Label( _c.Role );\r\n\t\t_lanes.Update();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPer frame: playhead, clock and play icon.\u003C/summary\u003E\r\n\tpublic void Tick()\r\n\t{\r\n\t\tvar seconds = _c.RoleSeconds( _c.Role );\r\n\t\tif ( !_scrubbing )\r\n\t\t\t_scrub.Value = _c.Time;\r\n\t\t_clock.Text = $\u0022{_c.Time * seconds:0.00} / {seconds:0.00} s\u0022;\r\n\t\tvar icon = _c.Playing ? \u0022pause\u0022 : \u0022play_arrow\u0022;\r\n\t\tif ( _play.Icon != icon )\r\n\t\t{\r\n\t\t\t_play.Icon = icon;\r\n\t\t\t_play.Update();\r\n\t\t}\r\n\t\tif ( _role.Text != AnimationRoles.Label( _c.Role ) )\r\n\t\t\t_role.Text = AnimationRoles.Label( _c.Role );\r\n\t\t_lanes.SetPlayhead( _c.Time );\r\n\t}\r\n\r\n\tpublic override void OnDestroyed()\r\n\t{\r\n\t\t_c.RoleChanged -= Refresh;\r\n\t\t_c.Changed -= Refresh;\r\n\t\tbase.OnDestroyed();\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Tool/UI/Widgets/Card.cs","FileName":"Card.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"using Editor;\r\nusing Sandbox;\r\n\r\nnamespace WeaponImporter.Tool.UI;\r\n\r\n/// \u003Csummary\u003E\r\n/// A rounded dark-gray panel on the gray window. Its column layout holds an optional header\r\n/// row (icon, title, then controls) above the content.\r\n/// \u003C/summary\u003E\r\npublic class Card : Widget\r\n{\r\n\tpublic Card( Widget parent, bool row = false ) : base( parent )\r\n\t{\r\n\t\tLayout = row ? Layout.Row() : Layout.Column();\r\n\t\tLayout.Margin = 10;\r\n\t\tLayout.Spacing = 8;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EAdds the header row: an icon and a title, then whatever the caller adds to the returned row.\u003C/summary\u003E\r\n\tpublic Layout Header( string icon, string title, string tooltip = null )\r\n\t{\r\n\t\tvar row = Layout.AddRow();\r\n\t\trow.Spacing = 6;\r\n\t\trow.Add( new CardIcon( this, icon ) );\r\n\t\tvar label = row.Add( new Label( title, this ) { ToolTip = tooltip } );\r\n\t\tlabel.SetStyles( \u0022font-weight: 600;\u0022 );\r\n\t\treturn row;\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.SetPen( Theme.ControlBackground.Lighten( .25f ), 1 );\r\n\t\tPaint.SetBrush( Theme.ControlBackground );\r\n\t\tPaint.DrawRect( LocalRect.Shrink( 1 ), 6 );\r\n\t}\r\n\r\n\tprivate sealed class CardIcon : Widget\r\n\t{\r\n\t\tprivate readonly string _icon;\r\n\r\n\t\tpublic CardIcon( Widget parent, string icon ) : base( parent )\r\n\t\t{\r\n\t\t\t_icon = icon;\r\n\t\t\tFixedSize = 18;\r\n\t\t}\r\n\r\n\t\tprotected override void OnPaint()\r\n\t\t{\r\n\t\t\tPaint.SetPen( Theme.Green );\r\n\t\t\tPaint.DrawIcon( LocalRect, _icon, 16 );\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003EA small round light before the status text: amber while working, green when ready, red on errors.\u003C/summary\u003E\r\npublic sealed class StatusDot : Widget\r\n{\r\n\tprivate Color _color = Theme.TextLight;\r\n\r\n\tpublic StatusDot( Widget parent ) : base( parent )\r\n\t{\r\n\t\tFixedSize = 10;\r\n\t}\r\n\r\n\tpublic Color Color\r\n\t{\r\n\t\tget =\u003E _color;\r\n\t\tset\r\n\t\t{\r\n\t\t\tif ( _color == value )\r\n\t\t\t\treturn;\r\n\t\t\t_color = value;\r\n\t\t\tUpdate();\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( _color );\r\n\t\tPaint.DrawRect( LocalRect.Shrink( 1 ), 4 );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003ESmall uppercase caption with a hairline, separating groups inside a card.\u003C/summary\u003E\r\npublic sealed class SectionHeader : Widget\r\n{\r\n\tprivate readonly string _text;\r\n\r\n\tpublic SectionHeader( Widget parent, string text ) : base( parent )\r\n\t{\r\n\t\t_text = text.ToUpperInvariant();\r\n\t\tFixedHeight = 18;\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.SetDefaultFont( 7, 600 );\r\n\t\tPaint.SetPen( Theme.TextLight );\r\n\t\tvar size = Paint.MeasureText( _text );\r\n\t\tPaint.DrawText( new Rect( 0, 0, size.x \u002B 2, Height ), _text, TextFlag.LeftCenter );\r\n\t\tPaint.SetPen( Theme.ControlBackground.Lighten( .45f ), 1 );\r\n\t\tPaint.DrawLine( new Vector2( size.x \u002B 10, Height * .5f ), new Vector2( Width, Height * .5f ) );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003ESmall muted uppercase column caption (list headers).\u003C/summary\u003E\r\npublic sealed class SectionCaption : Widget\r\n{\r\n\tprivate readonly string _text;\r\n\tprivate readonly TextFlag _align;\r\n\r\n\tpublic SectionCaption( Widget parent, string text, float width, TextFlag align = TextFlag.LeftCenter ) : base( parent )\r\n\t{\r\n\t\t_text = text.ToUpperInvariant();\r\n\t\t_align = align;\r\n\t\tFixedHeight = 16;\r\n\t\tif ( width \u003E 0 )\r\n\t\t\tFixedWidth = width;\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.SetDefaultFont( 7, 600 );\r\n\t\tPaint.SetPen( Theme.TextLight );\r\n\t\tPaint.DrawText( LocalRect, _text, _align );\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Code/WeaponImporter/WeaponGripProfile.cs","FileName":"WeaponGripProfile.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":397047,"IsPrivate":false,"Code":"namespace WeaponImporter;\r\n\r\n/// \u003Csummary\u003E\r\n/// A reusable grip: how a character holds one weapon, made by the Weapon Importer. Transforms are\r\n/// \u0022px,py,pz,qx,qy,qz,qw\u0022 in the weapon model\u0027s space, finger poses \u0022bone=qx,qy,qz,qw;...\u0022 (the\r\n/// same formats as \u003Csee cref=\u0022WeaponHold\u0022/\u003E). Assign it to a Weapon Hold to use it; nothing here\r\n/// is computed at runtime beyond the small IK correction.\r\n/// \u003C/summary\u003E\r\n[AssetType( Name = \u0022Weapon Grip Profile\u0022, Extension = \u0022wgrip\u0022, Category = \u0022Weapons\u0022 )]\r\npublic sealed class WeaponGripProfile : GameResource\r\n{\r\n    [Header( \u0022Hands\u0022 )]\r\n    /// \u003Csummary\u003EThe hand that holds the weapon\u0027s main grip (\u0022R\u0022 or \u0022L\u0022).\u003C/summary\u003E\r\n    public string PrimaryHand { get; set; } = \u0022R\u0022;\r\n\r\n    /// \u003Csummary\u003EPrimary hand bone in weapon space.\u003C/summary\u003E\r\n    public string PrimaryGripTransform { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EPrimary hand finger rotations.\u003C/summary\u003E\r\n    public string PrimaryFingerPose { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003ESupport hand bone in weapon space (\u0022\u0022 = one-handed).\u003C/summary\u003E\r\n    public string SecondaryGripTransform { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003ESupport hand finger rotations.\u003C/summary\u003E\r\n    public string SecondaryFingerPose { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EWhere the index finger rests, weapon space (zero = no trigger).\u003C/summary\u003E\r\n    public Vector3 TriggerTarget { get; set; }\r\n\r\n    /// \u003Csummary\u003EWhere the elbows point, weapon space; the arm IK leans toward them (keeps elbows steady).\u003C/summary\u003E\r\n    public Vector3 PrimaryElbowHint { get; set; }\r\n    public Vector3 SecondaryElbowHint { get; set; }\r\n\r\n    public bool TwoHanded { get; set; } = true;\r\n\r\n    [Header( \u0022Weapon\u0022 )]\r\n    /// \u003Csummary\u003EWeapon model relative to the hold bone.\u003C/summary\u003E\r\n    public string WeaponOffset { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003ECharacter bone the weapon is held by.\u003C/summary\u003E\r\n    public string HoldBone { get; set; } = \u0022hold_R\u0022;\r\n\r\n    /// \u003Csummary\u003EDual weapons: the left-hand copy\u0027s root bone (\u0022\u0022 = a single weapon).\u003C/summary\u003E\r\n    public string SecondWeaponBone { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EDual weapons: the character bone the left-hand copy is held by.\u003C/summary\u003E\r\n    public string SecondHoldBone { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EDual weapons: the left-hand copy\u0027s bone relative to \u003Csee cref=\u0022SecondHoldBone\u0022/\u003E.\u003C/summary\u003E\r\n    public string SecondWeaponOffset { get; set; } = \u0022\u0022;\r\n\r\n    [Header( \u0022Contact\u0022 )]\r\n    /// \u003Csummary\u003EPer-action hand contact tracks (JSON, see \u003Csee cref=\u0022WeaponHold.Contacts\u0022/\u003E).\u003C/summary\u003E\r\n    public string Contacts { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EPer-action timing, weapon sequence and trigger parameter (JSON).\u003C/summary\u003E\r\n    public string Actions { get; set; } = \u0022\u0022;\r\n\r\n    [Range( 0, 1 )] public float Smoothing { get; set; } = 0.35f;\r\n    public float ReachSlack { get; set; } = 1.5f;\r\n    [Range( 0, 1 )] public float TransitionSeconds { get; set; } = 0.15f;\r\n\r\n    [Header( \u0022Third person\u0022 )]\r\n    /// \u003Csummary\u003ECitizen holdtype the weapon was fitted to (-1 = leave to the game).\u003C/summary\u003E\r\n    public int HoldType { get; set; } = -1;\r\n\r\n    /// \u003Csummary\u003ECharacter model the grip was fitted to.\u003C/summary\u003E\r\n    public string Character { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EReplacement character animations per action (JSON).\u003C/summary\u003E\r\n    public string CharacterActions { get; set; } = \u0022\u0022;\r\n\r\n    [Header( \u0022First person\u0022 )]\r\n    /// \u003Csummary\u003EWeapon sequence for each first-person action (JSON: role \u2192 sequence).\u003C/summary\u003E\r\n    public string FirstPersonSequences { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003ESuggested first-person field of view.\u003C/summary\u003E\r\n    public float FirstPersonFieldOfView { get; set; } = 70f;\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":397047,"IsPrivate":false,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Weapon Importer\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022chomnr_weapon_importer\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022notpointless\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022notpointless.chomnr_weapon_importer\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002229\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-09-26T07:52:10.4512751Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.120.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.120.0\u0022)]"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Core/Import/GltfWeaponReader.cs","FileName":"GltfWeaponReader.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Numerics;\r\nusing System.Text.Json;\r\nusing WeaponImporter.Core.Formats.Gltf;\r\nusing WeaponImporter.Core.Geometry;\r\nusing WeaponImporter.Core.Maths;\r\nusing WeaponImporter.Core.Rig;\r\nusing WeaponImporter.Core.Weapon;\r\n\r\nnamespace WeaponImporter.Core.Import;\r\n\r\nusing Vector2 = System.Numerics.Vector2;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003EReads a glTF/GLB weapon: skeleton, clips and bind-pose triangles in model space.\u003C/summary\u003E\r\npublic static class GltfWeaponReader\r\n{\r\n    public static WeaponAsset Read(byte[] data, string name, string sourcePath = \u0022\u0022, Func\u003Cstring, byte[]\u003E? externalBuffers = null, float sampleFps = 30f)\r\n    {\r\n        var imported = GltfImporter.Import(data, new GltfImportOptions { SampleFps = sampleFps, ExternalBufferResolver = externalBuffers }, out var document, out var boneByNode);\r\n        var conversion = SpaceConversion.For(imported);\r\n        var skeleton = conversion.Skeleton(imported.Skeleton);\r\n        var clips = conversion.Clips(imported.Clips, imported.Skeleton, skeleton);\r\n\r\n        var root = document.Root;\r\n        var nodes = document.Nodes;\r\n        var accessors = root.TryGetProperty(\u0022accessors\u0022, out var acc) ? acc : default;\r\n        var views = root.TryGetProperty(\u0022bufferViews\u0022, out var bv) ? bv : default;\r\n        if (!root.TryGetProperty(\u0022meshes\u0022, out var meshes) || meshes.ValueKind != JsonValueKind.Array)\r\n            throw new FormatException(\u0022The glTF contains no meshes.\u0022);\r\n        root.TryGetProperty(\u0022skins\u0022, out var skins);\r\n        root.TryGetProperty(\u0022nodes\u0022, out var nodeArray);\r\n\r\n        // World matrices (meters) of every node.\r\n        var world = new Matrix4x4[nodes.Count];\r\n        var done = new bool[nodes.Count];\r\n        Matrix4x4 World(int i)\r\n        {\r\n            if (done[i])\r\n                return world[i];\r\n            var n = nodes[i];\r\n            var local = Matrix4x4.CreateScale(n.Scale) * Matrix4x4.CreateFromQuaternion(n.Rotation) * Matrix4x4.CreateTranslation(n.Translation);\r\n            done[i] = true; // guards malformed cycles\r\n            world[i] = n.Parent \u003E= 0 ? local * World(n.Parent) : local;\r\n            return world[i];\r\n        }\r\n\r\n        int BoneOf(int node)\r\n        {\r\n            for (var n = node; n \u003E= 0; n = nodes[n].Parent)\r\n                if (boneByNode.TryGetValue(n, out var boneName))\r\n                    return skeleton.IndexOf(boneName);\r\n            return 0;\r\n        }\r\n\r\n        var positions = new List\u003CVector3\u003E();\r\n        var vertexBone = new List\u003Cint\u003E();\r\n        var indices = new List\u003Cint\u003E();\r\n        var triPart = new List\u003Cint\u003E();\r\n        var partNames = new List\u003Cstring\u003E();\r\n        var vertexNormals = new List\u003CVector3\u003E();\r\n        var vertexUVs = new List\u003CVector2\u003E();\r\n        var triMaterial = new List\u003Cint\u003E();\r\n        var anyNormals = false;\r\n        var anyUVs = false;\r\n        var materialReader = new GltfMaterialReader(document, sourcePath, externalBuffers);\r\n\r\n        for (var nodeIndex = 0; nodeIndex \u003C nodes.Count; nodeIndex\u002B\u002B)\r\n        {\r\n            var nodeJson = nodeArray[nodeIndex];\r\n            if (!nodeJson.TryGetProperty(\u0022mesh\u0022, out var meshIndexProp))\r\n                continue;\r\n            var meshIndex = meshIndexProp.GetInt32();\r\n            if (meshIndex \u003C 0 || meshIndex \u003E= meshes.GetArrayLength())\r\n                throw new FormatException($\u0022glTF node {nodeIndex} references missing mesh {meshIndex}.\u0022);\r\n            var meshJson = meshes[meshIndex];\r\n            int[]? skinJoints = null;\r\n            Matrix4x4[]? inverseBind = null;\r\n            if (nodeJson.TryGetProperty(\u0022skin\u0022, out var skinProp) \u0026\u0026 skins.ValueKind == JsonValueKind.Array)\r\n            {\r\n                var skin = skins[skinProp.GetInt32()];\r\n                skinJoints = skin.GetProperty(\u0022joints\u0022).EnumerateArray().Select(j =\u003E j.GetInt32()).ToArray();\r\n                if (skin.TryGetProperty(\u0022inverseBindMatrices\u0022, out var ibm))\r\n                {\r\n                    var floats = ReadFloats(accessors, views, document.Buffers, ibm.GetInt32(), 16);\r\n                    inverseBind = new Matrix4x4[skinJoints.Length];\r\n                    for (var j = 0; j \u003C skinJoints.Length \u0026\u0026 (j \u002B 1) * 16 \u003C= floats.Length; j\u002B\u002B)\r\n                    {\r\n                        var f = floats.AsSpan(j * 16, 16);\r\n                        // glTF matrices are column-major column-vector = row-major row-vector.\r\n                        inverseBind[j] = new Matrix4x4(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12], f[13], f[14], f[15]);\r\n                    }\r\n                }\r\n            }\r\n\r\n            var nodeBone = BoneOf(nodeIndex);\r\n            var partName = nodes[nodeIndex].Name ?? (meshJson.TryGetProperty(\u0022name\u0022, out var mn) ? mn.GetString() : null) ?? $\u0022mesh_{meshIndex}\u0022;\r\n            var part = partNames.Count;\r\n            partNames.Add(partName);\r\n\r\n            foreach (var prim in meshJson.GetProperty(\u0022primitives\u0022).EnumerateArray())\r\n            {\r\n                var mode = prim.TryGetProperty(\u0022mode\u0022, out var modeProp) ? modeProp.GetInt32() : 4;\r\n                if (mode != 4)\r\n                    continue; // points/lines/strips are not solid geometry\r\n                var attributes = prim.GetProperty(\u0022attributes\u0022);\r\n                if (!attributes.TryGetProperty(\u0022POSITION\u0022, out var posProp))\r\n                    continue;\r\n                var pos = ReadFloats(accessors, views, document.Buffers, posProp.GetInt32(), 3);\r\n                var count = pos.Length / 3;\r\n                float[]? joints = null, weights = null;\r\n                if (skinJoints is not null \u0026\u0026 attributes.TryGetProperty(\u0022JOINTS_0\u0022, out var jp) \u0026\u0026 attributes.TryGetProperty(\u0022WEIGHTS_0\u0022, out var wp))\r\n                {\r\n                    joints = ReadFloats(accessors, views, document.Buffers, jp.GetInt32(), 4, raw: true);\r\n                    weights = ReadFloats(accessors, views, document.Buffers, wp.GetInt32(), 4);\r\n                }\r\n\r\n                float[]? nrm = null, tex = null;\r\n                if (attributes.TryGetProperty(\u0022NORMAL\u0022, out var nrmProp))\r\n                {\r\n                    nrm = ReadFloats(accessors, views, document.Buffers, nrmProp.GetInt32(), 3);\r\n                    if (nrm.Length / 3 \u003C count)\r\n                        nrm = null;\r\n                }\r\n                if (attributes.TryGetProperty(\u0022TEXCOORD_0\u0022, out var texProp))\r\n                {\r\n                    tex = ReadFloats(accessors, views, document.Buffers, texProp.GetInt32(), 2);\r\n                    if (tex.Length / 2 \u003C count)\r\n                        tex = null;\r\n                }\r\n                anyNormals |= nrm is not null;\r\n                anyUVs |= tex is not null;\r\n                var material = materialReader.MaterialIndex(prim.TryGetProperty(\u0022material\u0022, out var matProp) ? matProp.GetInt32() : -1);\r\n\r\n                var baseVertex = positions.Count;\r\n                var nodeWorld = World(nodeIndex);\r\n                var anchorOfNode = Anchor(nodeIndex);\r\n                var invAnchor = Matrix4x4.Identity;\r\n                if (anchorOfNode \u003E= 0 \u0026\u0026 Matrix4x4.Invert(World(anchorOfNode), out var inv))\r\n                {\r\n                    // Rigid rest transforms carry no scale; keep the anchor\u0027s.\r\n                    Matrix4x4.Decompose(World(anchorOfNode), out var anchorScale, out _, out _);\r\n                    invAnchor = inv * Matrix4x4.CreateScale(anchorScale);\r\n                }\r\n                var nodeNormal = NormalMatrix(nodeWorld);\r\n                var anchoredNormal = NormalMatrix(nodeWorld * invAnchor);\r\n                for (var v = 0; v \u003C count; v\u002B\u002B)\r\n                {\r\n                    var p = new Vector3(pos[v * 3], pos[v * 3 \u002B 1], pos[v * 3 \u002B 2]);\r\n                    var n = nrm is not null ? new Vector3(nrm[v * 3], nrm[v * 3 \u002B 1], nrm[v * 3 \u002B 2]) : Vector3.Zero;\r\n                    int bone;\r\n                    Vector3 model;\r\n                    Vector3 normal;\r\n                    if (joints is not null \u0026\u0026 weights is not null)\r\n                    {\r\n                        var best = 0;\r\n                        for (var k = 1; k \u003C 4; k\u002B\u002B)\r\n                            if (weights[v * 4 \u002B k] \u003E weights[v * 4 \u002B best])\r\n                                best = k;\r\n                        var jointSlot = (int)joints[v * 4 \u002B best];\r\n                        var jointNode = jointSlot \u003E= 0 \u0026\u0026 jointSlot \u003C skinJoints!.Length ? skinJoints[jointSlot] : nodeIndex;\r\n                        bone = BoneOf(jointNode);\r\n                        // Mesh space -\u003E joint bind space -\u003E joint rest world. The full node matrix\r\n                        // is used (rigid rest transforms drop inherited scale, which would pull\r\n                        // parts on differently scaled joints apart).\r\n                        var ibm = inverseBind is not null \u0026\u0026 jointSlot \u003C inverseBind.Length ? inverseBind[jointSlot] : Matrix4x4.Identity;\r\n                        var bindWorld = Vector3.Transform(Vector3.Transform(p, ibm), World(jointNode)) * 100f;\r\n                        model = conversion.Point(bindWorld);\r\n                        normal = conversion.Direction(Vector3.TransformNormal(n, NormalMatrix(ibm * World(jointNode))));\r\n                    }\r\n                    else\r\n                    {\r\n                        bone = nodeBone;\r\n                        var anchorNode = anchorOfNode;\r\n                        var rel = Vector3.Transform(Vector3.Transform(p, nodeWorld), invAnchor) * 100f;\r\n                        var anchored = anchorNode \u003E= 0 \u0026\u0026 boneByNode.ContainsKey(anchorNode);\r\n                        model = anchored\r\n                            ? skeleton.RestWorld[bone].TransformPoint(conversion.Direction(rel) * conversion.Scale)\r\n                            : conversion.Point(Vector3.Transform(p, nodeWorld) * 100f);\r\n                        normal = anchored\r\n                            ? skeleton.RestWorld[bone].TransformVector(conversion.Direction(Vector3.TransformNormal(n, anchoredNormal)))\r\n                            : conversion.Direction(Vector3.TransformNormal(n, nodeNormal));\r\n                    }\r\n                    positions.Add(model);\r\n                    vertexBone.Add(bone);\r\n                    var len = normal.Length();\r\n                    vertexNormals.Add(nrm is not null \u0026\u0026 len \u003E 1e-12f \u0026\u0026 float.IsFinite(len) ? normal / len : new Vector3(float.NaN));\r\n                    // glTF UVs already have the top-left origin TriMesh uses.\r\n                    vertexUVs.Add(tex is not null ? new Vector2(tex[v * 2], tex[v * 2 \u002B 1]) : Vector2.Zero);\r\n                }\r\n\r\n                if (prim.TryGetProperty(\u0022indices\u0022, out var idxProp))\r\n                {\r\n                    var idx = ReadFloats(accessors, views, document.Buffers, idxProp.GetInt32(), 1, raw: true);\r\n                    for (var i = 0; i \u002B 2 \u003C idx.Length; i \u002B= 3)\r\n                    {\r\n                        var a = (int)idx[i]; var b = (int)idx[i \u002B 1]; var c = (int)idx[i \u002B 2];\r\n                        if ((uint)a \u003E= (uint)count || (uint)b \u003E= (uint)count || (uint)c \u003E= (uint)count)\r\n                            throw new FormatException($\u0022glTF mesh \u0027{partName}\u0027 has out-of-range indices.\u0022);\r\n                        indices.Add(baseVertex \u002B a); indices.Add(baseVertex \u002B b); indices.Add(baseVertex \u002B c);\r\n                        triPart.Add(part);\r\n                        triMaterial.Add(material);\r\n                    }\r\n                }\r\n                else\r\n                {\r\n                    for (var i = 0; i \u002B 2 \u003C count; i \u002B= 3)\r\n                    {\r\n                        indices.Add(baseVertex \u002B i); indices.Add(baseVertex \u002B i \u002B 1); indices.Add(baseVertex \u002B i \u002B 2);\r\n                        triPart.Add(part);\r\n                        triMaterial.Add(material);\r\n                    }\r\n                }\r\n                if (indices.Count / 3 \u003E FbxWeaponReader.MaxTriangles)\r\n                    throw new FormatException($\u0022The model has more than {FbxWeaponReader.MaxTriangles:N0} triangles; decimate it before importing.\u0022);\r\n            }\r\n        }\r\n\r\n        int Anchor(int node)\r\n        {\r\n            for (var n = node; n \u003E= 0; n = nodes[n].Parent)\r\n                if (boneByNode.ContainsKey(n))\r\n                    return n;\r\n            return -1;\r\n        }\r\n\r\n        if (indices.Count == 0)\r\n            throw new FormatException(\u0022The glTF contains no triangle geometry.\u0022);\r\n\r\n        var indexArray = indices.ToArray();\r\n        Vector3[]? cornerNormals = null;\r\n        if (anyNormals)\r\n        {\r\n            cornerNormals = indexArray.Select(i =\u003E vertexNormals[i]).ToArray();\r\n            if (cornerNormals.Any(n =\u003E float.IsNaN(n.X)))\r\n            {\r\n                var smooth = new TriMesh(positions.ToArray(), indexArray).EnsureNormals();\r\n                for (var c = 0; c \u003C cornerNormals.Length; c\u002B\u002B)\r\n                    if (float.IsNaN(cornerNormals[c].X))\r\n                        cornerNormals[c] = smooth[c];\r\n            }\r\n        }\r\n        var cornerUVs = anyUVs ? indexArray.Select(i =\u003E vertexUVs[i]).ToArray() : null;\r\n\r\n        var asset = new WeaponAsset\r\n        {\r\n            Name = name,\r\n            Kind = SourceKind.Gltf,\r\n            SourcePath = sourcePath,\r\n            Skeleton = skeleton,\r\n            Mesh = new TriMesh(positions.ToArray(), indexArray, vertexBone.ToArray(), triPart.ToArray(), partNames,\r\n                cornerNormals, cornerUVs, triMaterial.ToArray(), materialReader.Materials),\r\n            Clips = clips,\r\n        };\r\n        asset.Notes.AddRange(imported.Notes);\r\n        asset.Notes.AddRange(materialReader.Notes.Distinct().Take(20));\r\n        return asset;\r\n    }\r\n\r\n    /// \u003Csummary\u003ERow-vector normal matrix: inverse transpose of the linear part.\u003C/summary\u003E\r\n    private static Matrix4x4 NormalMatrix(Matrix4x4 m)\r\n    {\r\n        m.M41 = m.M42 = m.M43 = 0f;\r\n        return Matrix4x4.Invert(m, out var inv) ? Matrix4x4.Transpose(inv) : m;\r\n    }\r\n\r\n    /// \u003Csummary\u003EAccessor reader for any component type; \u003Cparamref name=\u0022raw\u0022/\u003E skips normalisation (indices, joints).\u003C/summary\u003E\r\n    private static float[] ReadFloats(JsonElement accessors, JsonElement views, List\u003Cbyte[]\u003E buffers, int index, int comps, bool raw = false)\r\n    {\r\n        if (accessors.ValueKind != JsonValueKind.Array || index \u003C 0 || index \u003E= accessors.GetArrayLength())\r\n            throw new FormatException($\u0022glTF accessor {index} does not exist.\u0022);\r\n        var accessor = accessors[index];\r\n        if (accessor.TryGetProperty(\u0022sparse\u0022, out _))\r\n            throw new FormatException(\u0022glTF sparse accessors are not supported.\u0022);\r\n        var type = accessor.GetProperty(\u0022type\u0022).GetString();\r\n        var actual = type switch { \u0022SCALAR\u0022 =\u003E 1, \u0022VEC2\u0022 =\u003E 2, \u0022VEC3\u0022 =\u003E 3, \u0022VEC4\u0022 =\u003E 4, \u0022MAT4\u0022 =\u003E 16, _ =\u003E throw new FormatException($\u0022glTF accessor type \u0027{type}\u0027 is not supported.\u0022) };\r\n        if (actual != comps)\r\n            throw new FormatException($\u0022glTF accessor {index} is {type}, expected {comps} components.\u0022);\r\n        var count = accessor.GetProperty(\u0022count\u0022).GetInt32();\r\n        var componentType = accessor.GetProperty(\u0022componentType\u0022).GetInt32();\r\n        var normalized = accessor.TryGetProperty(\u0022normalized\u0022, out var n) \u0026\u0026 n.GetBoolean();\r\n        var size = componentType switch { 5126 or 5125 =\u003E 4, 5122 or 5123 =\u003E 2, 5120 or 5121 =\u003E 1, _ =\u003E throw new FormatException($\u0022glTF component type {componentType} is not supported.\u0022) };\r\n        if (count \u003C 0 || count \u003E 50_000_000)\r\n            throw new FormatException($\u0022glTF accessor {index} has an invalid count.\u0022);\r\n        var result = new float[count * comps];\r\n        if (!accessor.TryGetProperty(\u0022bufferView\u0022, out var viewProp))\r\n            return result;\r\n        var view = views[viewProp.GetInt32()];\r\n        var buffer = buffers[view.GetProperty(\u0022buffer\u0022).GetInt32()];\r\n        var start = (long)(view.TryGetProperty(\u0022byteOffset\u0022, out var vo) ? vo.GetInt32() : 0) \u002B (accessor.TryGetProperty(\u0022byteOffset\u0022, out var ao) ? ao.GetInt32() : 0);\r\n        var stride = view.TryGetProperty(\u0022byteStride\u0022, out var st) ? st.GetInt32() : size * comps;\r\n        if (count \u003E 0 \u0026\u0026 start \u002B (long)(count - 1) * stride \u002B size * comps \u003E buffer.Length)\r\n            throw new FormatException($\u0022glTF accessor {index} reads past the end of its buffer (truncated file?).\u0022);\r\n        for (var e = 0; e \u003C count; e\u002B\u002B)\r\n        {\r\n            var offset = (int)(start \u002B (long)e * stride);\r\n            for (var c = 0; c \u003C comps; c\u002B\u002B)\r\n            {\r\n                var at = offset \u002B c * size;\r\n                float value = componentType switch\r\n                {\r\n                    5126 =\u003E BitConverter.ToSingle(buffer, at),\r\n                    5125 =\u003E BitConverter.ToUInt32(buffer, at),\r\n                    5123 =\u003E BitConverter.ToUInt16(buffer, at),\r\n                    5122 =\u003E BitConverter.ToInt16(buffer, at),\r\n                    5121 =\u003E buffer[at],\r\n                    _ =\u003E (sbyte)buffer[at],\r\n                };\r\n                if (!raw \u0026\u0026 normalized)\r\n                    value = componentType switch { 5121 =\u003E value / 255f, 5123 =\u003E value / 65535f, 5120 =\u003E MathF.Max(value / 127f, -1f), 5122 =\u003E MathF.Max(value / 32767f, -1f), _ =\u003E value };\r\n                result[e * comps \u002B c] = value;\r\n            }\r\n        }\r\n        return result;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Core/Import/SpaceConversion.cs","FileName":"SpaceConversion.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Numerics;\r\nusing WeaponImporter.Core.Maths;\r\nusing WeaponImporter.Core.Rig;\r\n\r\nnamespace WeaponImporter.Core.Import;\r\n\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003E\r\n/// Converts a source scene (centimetres, file axes) into s\u0026amp;box model space (inches, \u002BZ up).\r\n/// The file\u0027s up axis becomes \u002BZ, its coordinate (right) axis becomes \u002BX and its front axis\r\n/// becomes -Y, which matches how ModelDoc imports Y-up FBX files.\r\n/// \u003C/summary\u003E\r\npublic sealed class SpaceConversion\r\n{\r\n    public const float CmToInch = 1f / 2.54f;\r\n\r\n    /// \u003Csummary\u003ERow-vector basis change: v_sbox = v_file * Basis.\u003C/summary\u003E\r\n    public Matrix4x4 Basis { get; }\r\n\r\n    public float Scale { get; }\r\n\r\n    public SpaceConversion(int upAxis, int upSign, int frontAxis, int frontSign, int coordAxis, int coordSign, float scale = CmToInch)\r\n    {\r\n        var m = new Matrix4x4();\r\n        void Set(int row, int col, float v)\r\n        {\r\n            switch (row * 4 \u002B col)\r\n            {\r\n                case 0: m.M11 = v; break; case 1: m.M12 = v; break; case 2: m.M13 = v; break;\r\n                case 4: m.M21 = v; break; case 5: m.M22 = v; break; case 6: m.M23 = v; break;\r\n                case 8: m.M31 = v; break; case 9: m.M32 = v; break; case 10: m.M33 = v; break;\r\n            }\r\n        }\r\n        if (upAxis == frontAxis || upAxis == coordAxis || frontAxis == coordAxis)\r\n        {\r\n            // Malformed axis record: assume the FBX default (Y up, Z front, X right).\r\n            (upAxis, frontAxis, coordAxis) = (1, 2, 0);\r\n        }\r\n        Set(coordAxis, 0, coordSign \u003E= 0 ? 1f : -1f);\r\n        Set(frontAxis, 1, frontSign \u003E= 0 ? -1f : 1f);\r\n        Set(upAxis, 2, upSign \u003E= 0 ? 1f : -1f);\r\n        m.M44 = 1f;\r\n        Basis = m;\r\n        Scale = scale;\r\n    }\r\n\r\n    public static SpaceConversion For(SourceScene scene)\r\n        =\u003E new(scene.UpAxis, scene.UpAxisSign, scene.FrontAxis, scene.FrontAxisSign, scene.CoordAxis, scene.CoordAxisSign);\r\n\r\n    public static SpaceConversion Identity { get; } = new(2, 1, 1, -1, 0, 1, 1f);\r\n\r\n    public Vector3 Point(Vector3 cm) =\u003E Vector3.Transform(cm, Basis) * Scale;\r\n\r\n    public Vector3 Direction(Vector3 v) =\u003E Vector3.Transform(v, Basis);\r\n\r\n    public Quaternion Rotation(Quaternion q)\r\n    {\r\n        // Similarity transform keeps a proper rotation even when the file axes are mirrored.\r\n        var r = Matrix4x4.CreateFromQuaternion(q);\r\n        var t = Matrix4x4.Transpose(Basis);\r\n        var s = t * r * Basis;\r\n        s.M14 = s.M24 = s.M34 = s.M41 = s.M42 = s.M43 = 0f;\r\n        s.M44 = 1f;\r\n        return MathQ.Normalize(Quaternion.CreateFromRotationMatrix(s));\r\n    }\r\n\r\n    public XForm Transform(XForm x) =\u003E new(Point(x.Pos), Rotation(x.Rot));\r\n\r\n    public Skeleton Skeleton(Skeleton source)\r\n    {\r\n        var defs = new List\u003CBoneDefinition\u003E(source.Count);\r\n        for (var i = 0; i \u003C source.Count; i\u002B\u002B)\r\n        {\r\n            var b = source[i];\r\n            defs.Add(new BoneDefinition(b.Name, b.ParentIndex \u003C 0 ? null : source[b.ParentIndex].Name, Transform(b.RestLocal)));\r\n        }\r\n        return Rig.Skeleton.Create(defs);\r\n    }\r\n\r\n    /// \u003Csummary\u003EClips re-expressed on the converted skeleton (same bone order by name).\u003C/summary\u003E\r\n    public List\u003CClip\u003E Clips(IReadOnlyList\u003CClip\u003E clips, Skeleton source, Skeleton converted)\r\n    {\r\n        var map = new int[source.Count];\r\n        for (var i = 0; i \u003C source.Count; i\u002B\u002B)\r\n            map[i] = converted.IndexOf(source[i].Name);\r\n        var result = new List\u003CClip\u003E(clips.Count);\r\n        foreach (var clip in clips)\r\n        {\r\n            var frames = new List\u003CXForm[]\u003E(clip.FrameCount);\r\n            foreach (var frame in clip.Frames)\r\n            {\r\n                var locals = new XForm[converted.Count];\r\n                for (var i = 0; i \u003C source.Count \u0026\u0026 i \u003C frame.Length; i\u002B\u002B)\r\n                    if (map[i] \u003E= 0)\r\n                        locals[map[i]] = Transform(frame[i]);\r\n                frames.Add(locals);\r\n            }\r\n            result.Add(new Clip(clip.Name, clip.Fps, clip.Looping, frames, clip.NativeFps));\r\n        }\r\n        return result;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Core/Rig/Clip.cs","FileName":"Clip.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing WeaponImporter.Core.Maths;\r\n\r\nnamespace WeaponImporter.Core.Rig;\r\n\r\n/// \u003Csummary\u003E\r\n/// A sampled animation clip: a fixed-rate sequence of frames, each holding one\r\n/// parent-relative local transform per bone (skeleton bone order). Clips are always\r\n/// resampled at ingest \u2014 no key data is preserved.\r\n/// \u003C/summary\u003E\r\npublic sealed class Clip\r\n{\r\n    /// \u003Csummary\u003EClip (sequence) name.\u003C/summary\u003E\r\n    public string Name { get; }\r\n\r\n    /// \u003Csummary\u003ESample rate in frames per second.\u003C/summary\u003E\r\n    public float Fps { get; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Native frame rate of the take in the SOURCE file (FBX GlobalSettings\r\n    /// TimeMode/CustomFrameRate, BVH 1/FrameTime). External frame ranges \u2014 Unity\r\n    /// \u003Cc\u003E.fbx.meta\u003C/c\u003E \u003Cc\u003EclipAnimations\u003C/c\u003E definitions \u2014 are expressed in THIS rate, so\r\n    /// they must be rescaled by \u003Cc\u003EFps / NativeFps\u003C/c\u003E to index the resampled\r\n    /// \u003Csee cref=\u0022Frames\u0022/\u003E. Equals \u003Csee cref=\u0022Fps\u0022/\u003E when the importer records no native rate.\r\n    /// \u003C/summary\u003E\r\n    public float NativeFps { get; }\r\n\r\n    /// \u003Csummary\u003EWhether the clip is authored to loop.\u003C/summary\u003E\r\n    public bool Looping { get; }\r\n\r\n    /// \u003Csummary\u003EFrames in playback order; each entry is one local transform per bone.\u003C/summary\u003E\r\n    public List\u003CXForm[]\u003E Frames { get; }\r\n\r\n    /// \u003Csummary\u003ENumber of frames currently in the clip.\u003C/summary\u003E\r\n    public int FrameCount =\u003E Frames.Count;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Clip duration in seconds at \u003Csee cref=\u0022Fps\u0022/\u003E: the time span between the first and the\r\n    /// last sample, \u003Cc\u003E(FrameCount - 1) / Fps\u003C/c\u003E (frames are fence posts, intervals are the\r\n    /// spans between them \u2014 matching the DMX timeFrame this clip serializes to). Zero for\r\n    /// empty and single-frame clips.\r\n    /// \u003C/summary\u003E\r\n    public float Duration =\u003E FrameCount \u003C= 1 ? 0f : (FrameCount - 1) / Fps;\r\n\r\n    /// \u003Csummary\u003ECreates an empty clip.\u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentOutOfRangeException\u0022\u003EThrown when \u003Cparamref name=\u0022fps\u0022/\u003E is not positive.\u003C/exception\u003E\r\n    public Clip(string name, float fps, bool looping)\r\n        : this(name, fps, looping, new List\u003CXForm[]\u003E())\r\n    {\r\n    }\r\n\r\n    /// \u003Csummary\u003ECreates a clip wrapping an existing frame list (not copied).\u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022nativeFps\u0022\u003ESource-file native frame rate (\u003Csee cref=\u0022NativeFps\u0022/\u003E);\r\n    /// null = same as \u003Cparamref name=\u0022fps\u0022/\u003E.\u003C/param\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentOutOfRangeException\u0022\u003EThrown when \u003Cparamref name=\u0022fps\u0022/\u003E (or a\r\n    /// provided \u003Cparamref name=\u0022nativeFps\u0022/\u003E) is not positive.\u003C/exception\u003E\r\n    public Clip(string name, float fps, bool looping, List\u003CXForm[]\u003E frames, float? nativeFps = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(name);\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        if (!(fps \u003E 0f) || !float.IsFinite(fps))\r\n            throw new ArgumentOutOfRangeException(nameof(fps), fps, \u0022Fps must be a positive finite number.\u0022);\r\n        if (nativeFps is { } native \u0026\u0026 (!(native \u003E 0f) || !float.IsFinite(native)))\r\n            throw new ArgumentOutOfRangeException(nameof(nativeFps), native, \u0022NativeFps must be a positive finite number.\u0022);\r\n\r\n        Name = name;\r\n        Fps = fps;\r\n        NativeFps = nativeFps ?? fps;\r\n        Looping = looping;\r\n        Frames = frames;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Tool/UI/Preview/WeaponViewport.Picking.cs","FileName":"WeaponViewport.Picking.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"using Editor;\r\nusing Sandbox;\r\nusing WeaponImporter.Core.Hands;\r\n\r\nnamespace WeaponImporter.Tool.UI;\r\n\r\n/// \u003Csummary\u003EClicks in the preview: attachment markers, arms (per-action release) and the weapon surface.\u003C/summary\u003E\r\npublic sealed partial class WeaponViewport\r\n{\r\n\tprivate const float MarkerRadiusPx = 14f;\r\n\tprivate const float ArmRadiusPx = 12f;\r\n\r\n\tprivate readonly Dictionary\u003Cint, Side?\u003E _boneSide = new();\r\n\r\n\t/// \u003Csummary\u003ERight/left from the bone name suffix (hand_R, arm_lower_L, finger_index_0_R...).\u003C/summary\u003E\r\n\tprivate Side? SideOf( int bone )\r\n\t{\r\n\t\tif ( _boneSide.TryGetValue( bone, out var side ) )\r\n\t\t\treturn side;\r\n\t\tvar name = _body.IsValid() \u0026\u0026 _body.Model is { } m ? m.GetBoneName( bone ).ToLowerInvariant() : \u0022\u0022;\r\n\t\tside = name.EndsWith( \u0022_r\u0022 ) ? Side.Right : name.EndsWith( \u0022_l\u0022 ) ? Side.Left : null;\r\n\t\t_boneSide[bone] = side;\r\n\t\treturn side;\r\n\t}\r\n\r\n\tprivate bool ToScreen( Vector3 world, out Vector2 screen )\r\n\t{\r\n\t\tscreen = Camera.PointToScreenPixels( world, out var behind );\r\n\t\treturn !behind;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe muzzle or eject marker under a viewport pixel (None when neither).\u003C/summary\u003E\r\n\tpublic PickTarget HitMarker( Vector2 local )\r\n\t{\r\n\t\tvar setup = _c.Setup;\r\n\t\tif ( setup is null || !Camera.IsValid() || !_weaponObject.IsValid() || FirstPerson )\r\n\t\t\treturn PickTarget.None;\r\n\t\tvar weapon = WeaponWorld;\r\n\t\tvar best = PickTarget.None;\r\n\t\tvar bestDistance = MarkerRadiusPx;\r\n\t\tforeach ( var (target, point) in new[] { (PickTarget.Muzzle, setup.Muzzle), (PickTarget.Eject, setup.Eject) } )\r\n\t\t{\r\n\t\t\tif ( point is null || !ToScreen( weapon.PointToWorld( Core.Setup.V.Of( point.Canonical ).ToEngine() ), out var screen ) )\r\n\t\t\t\tcontinue;\r\n\t\t\tvar d = (screen - local).Length;\r\n\t\t\tif ( d \u003C bestDistance )\r\n\t\t\t{\r\n\t\t\t\tbestDistance = d;\r\n\t\t\t\tbest = target;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn best;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhich character arm (upper arm, forearm, hand or fingers) is under a viewport pixel.\u003C/summary\u003E\r\n\tpublic Side? HitArm( Vector2 local )\r\n\t{\r\n\t\tif ( !Camera.IsValid() || !_body.IsValid() || !_body.SceneModel.IsValid() || FirstPerson )\r\n\t\t\treturn null;\r\n\t\tSide? best = null;\r\n\t\tvar bestDistance = ArmRadiusPx;\r\n\t\tforeach ( var (bone, parent) in _armBones )\r\n\t\t{\r\n\t\t\tif ( SideOf( bone ) is not { } side )\r\n\t\t\t\tcontinue;\r\n\t\t\tif ( !ToScreen( _body.SceneModel.GetBoneWorldTransform( parent ).Position, out var a ) || !ToScreen( _body.SceneModel.GetBoneWorldTransform( bone ).Position, out var b ) )\r\n\t\t\t\tcontinue;\r\n\t\t\tvar d = DistanceToSegment( local, a, b );\r\n\t\t\tif ( d \u003C bestDistance )\r\n\t\t\t{\r\n\t\t\t\tbestDistance = d;\r\n\t\t\t\tbest = side;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn best;\r\n\t}\r\n\r\n\tprivate static float DistanceToSegment( Vector2 p, Vector2 a, Vector2 b )\r\n\t{\r\n\t\tvar ab = b - a;\r\n\t\tvar lengthSq = ab.x * ab.x \u002B ab.y * ab.y;\r\n\t\tvar t = lengthSq \u003C 1e-4f ? 0f : Math.Clamp( ((p.x - a.x) * ab.x \u002B (p.y - a.y) * ab.y) / lengthSq, 0f, 1f );\r\n\t\treturn (p - (a \u002B ab * t)).Length;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhere a marker is on screen (gate and tests), or null.\u003C/summary\u003E\r\n\tpublic Vector2? MarkerScreenPosition( PickTarget target )\r\n\t{\r\n\t\tvar point = target == PickTarget.Muzzle ? _c.Setup?.Muzzle : target == PickTarget.Eject ? _c.Setup?.Eject : null;\r\n\t\tif ( point is null || !Camera.IsValid() || !_weaponObject.IsValid() )\r\n\t\t\treturn null;\r\n\t\treturn ToScreen( WeaponWorld.PointToWorld( Core.Setup.V.Of( point.Canonical ).ToEngine() ), out var s ) ? s : null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EMiddle of a forearm on screen (gate and tests), or null.\u003C/summary\u003E\r\n\tpublic Vector2? ArmScreenPosition( Side side )\r\n\t{\r\n\t\tif ( !Camera.IsValid() || !_body.IsValid() || !_body.SceneModel.IsValid() )\r\n\t\t\treturn null;\r\n\t\tforeach ( var (bone, parent) in _armBones )\r\n\t\t{\r\n\t\t\tif ( SideOf( bone ) != side || !_body.Model.GetBoneName( bone ).StartsWith( \u0022hand\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\tcontinue;\r\n\t\t\tvar mid = (_body.SceneModel.GetBoneWorldTransform( parent ).Position \u002B _body.SceneModel.GetBoneWorldTransform( bone ).Position) * 0.5f;\r\n\t\t\treturn ToScreen( mid, out var s ) ? s : null;\r\n\t\t}\r\n\t\treturn null;\r\n\t}\r\n\r\n\tpublic int FirstPersonPieceCount =\u003E _fpPieces.Count;\r\n\r\n\t/// \u003Csummary\u003EThe hand whose grip contact marker is under a viewport pixel (drag to move the grip).\u003C/summary\u003E\r\n\tpublic Side? HitGripContact( Vector2 local )\r\n\t{\r\n\t\tvar setup = _c.Setup;\r\n\t\tif ( setup is null || !_c.ShowContacts || !Camera.IsValid() || !_weaponObject.IsValid() || OwnArmsView )\r\n\t\t\treturn null;\r\n\t\tvar weapon = WeaponWorld;\r\n\t\tSide? best = null;\r\n\t\tvar bestDistance = MarkerRadiusPx;\r\n\t\tforeach ( var (side, grip) in new[] { (Side.Right, setup.Primary), (Side.Left, setup.UseSupportHand ? setup.Support : null) } )\r\n\t\t{\r\n\t\t\tif ( grip is null || !ToScreen( weapon.PointToWorld( Core.Setup.V.Of( grip.Contact ).ToEngine() ), out var screen ) )\r\n\t\t\t\tcontinue;\r\n\t\t\tvar d = (screen - local).Length;\r\n\t\t\tif ( d \u003C bestDistance )\r\n\t\t\t{\r\n\t\t\t\tbestDistance = d;\r\n\t\t\t\tbest = side;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn best;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EHand grip being dragged across the weapon, or null.\u003C/summary\u003E\r\n\tprivate Side? _dragGrip;\r\n\r\n\t/// \u003Csummary\u003EStarts dragging a grip contact when the press lands on one.\u003C/summary\u003E\r\n\tprivate bool BeginGripDrag( Vector2 local )\r\n\t{\r\n\t\tif ( _c.Pick != PickTarget.None || HitGripContact( local ) is not { } side )\r\n\t\t\treturn false;\r\n\t\t_dragGrip = side;\r\n\t\tCursor = CursorShape.ClosedHand;\r\n\t\t_c.SetStatus( $\u0022Moving the {(side == Side.Right ? \u0022right\u0022 : \u0022left\u0022)} grip: drag across the weapon.\u0022, Theme.Blue );\r\n\t\treturn true;\r\n\t}\r\n\r\n\tprivate void DragGrip( Vector2 local )\r\n\t{\r\n\t\tif ( _dragGrip is not { } side )\r\n\t\t\treturn;\r\n\t\tif ( RaycastWeapon( local ) is { } hit )\r\n\t\t\t_c.DragGrip( side, hit.Point, hit.Normal );\r\n\t}\r\n\r\n\tprivate void EndGripDrag()\r\n\t{\r\n\t\tif ( _dragGrip is null )\r\n\t\t\treturn;\r\n\t\t_dragGrip = null;\r\n\t\tCursor = CursorShape.Arrow;\r\n\t\t_c.EndDragGrip();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA plain left click (no drag) in the preview.\u003C/summary\u003E\r\n\tprivate void Click( Vector2 local )\r\n\t{\r\n\t\tvar pick = _c.Pick;\r\n\t\tif ( pick is PickTarget.Muzzle or PickTarget.Eject )\r\n\t\t{\r\n\t\t\t// Another marker selects that one; the weapon surface moves the selected one; anything else deselects.\r\n\t\t\tvar marker = HitMarker( local );\r\n\t\t\tif ( marker != PickTarget.None \u0026\u0026 marker != pick )\r\n\t\t\t{\r\n\t\t\t\t_c.BeginPick( marker );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\tif ( RaycastWeapon( local ) is { } hit )\r\n\t\t\t\t_c.CompletePick( hit.Point, hit.Normal );\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_c.BeginPick( PickTarget.None );\r\n\t\t\t\t_c.SetStatus( \u0022Marker deselected.\u0022, Theme.TextLight );\r\n\t\t\t}\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tif ( pick != PickTarget.None )\r\n\t\t{\r\n\t\t\tif ( RaycastWeapon( local ) is { } hit )\r\n\t\t\t\t_c.CompletePick( hit.Point, hit.Normal );\r\n\t\t\telse\r\n\t\t\t\t_c.SetStatus( \u0022That missed the weapon. \u0022 \u002B ImporterController.PickHint( pick ), Theme.Yellow );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar clicked = HitMarker( local );\r\n\t\tif ( clicked != PickTarget.None )\r\n\t\t{\r\n\t\t\t_c.BeginPick( clicked );\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tif ( HitArm( local ) is { } side )\r\n\t\t\t_c.ToggleHand( _c.Role, side );\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Tool/UI/Steps/StepPanel.cs","FileName":"StepPanel.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"using Editor;\r\nusing Sandbox;\r\n\r\nnamespace WeaponImporter.Tool.UI;\r\n\r\n/// \u003Csummary\u003E\r\n/// One step of the workflow in the left column. Steps build their cards from the session and\r\n/// rebuild only when their structure changes (a new weapon, a different part list...); value\r\n/// changes just run the registered refreshers, so a slider being dragged is never destroyed.\r\n/// Rebuilds are deferred to the next frame so a widget is never deleted inside its own callback.\r\n/// \u003C/summary\u003E\r\npublic abstract class StepPanel : Widget\r\n{\r\n\tprotected readonly ImporterController C;\r\n\tprivate readonly List\u003CAction\u003E _refreshers = new();\r\n\tprivate string _builtKey;\r\n\tprivate bool _dirty = true;\r\n\r\n\tprotected StepPanel( Widget parent, ImporterController controller ) : base( parent )\r\n\t{\r\n\t\tC = controller;\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Spacing = 10;\r\n\t\tcontroller.Changed \u002B= MarkDirty;\r\n\t\tcontroller.SessionReplaced \u002B= ForceRebuild;\r\n\t}\r\n\r\n\tpublic abstract string Title { get; }\r\n\r\n\t/// \u003Csummary\u003EAnything that, when different, needs new widgets rather than new values.\u003C/summary\u003E\r\n\tprotected abstract string StructureKey();\r\n\r\n\tprotected abstract void Build();\r\n\r\n\tpublic void MarkDirty() =\u003E _dirty = true;\r\n\r\n\tpublic void ForceRebuild()\r\n\t{\r\n\t\t_builtKey = null;\r\n\t\t_dirty = true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECalled every frame by the window while this step is visible.\u003C/summary\u003E\r\n\tpublic void Flush()\r\n\t{\r\n\t\tif ( !_dirty )\r\n\t\t\treturn;\r\n\t\t_dirty = false;\r\n\t\tstring key;\r\n\t\ttry\r\n\t\t{\r\n\t\t\tkey = C.Session?.Setup is null ? \u0022\u0022 : StructureKey();\r\n\t\t}\r\n\t\tcatch ( Exception )\r\n\t\t{\r\n\t\t\tkey = Guid.NewGuid().ToString();\r\n\t\t}\r\n\t\tif ( key != _builtKey )\r\n\t\t{\r\n\t\t\t_builtKey = key;\r\n\t\t\tRebuild();\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tforeach ( var refresh in _refreshers.ToArray() )\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\trefresh();\r\n\t\t\t}\r\n\t\t\tcatch ( Exception e )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022[weapon importer] refresh failed: {e.Message}\u0022 );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void Rebuild()\r\n\t{\r\n\t\tLayout.Clear( true );\r\n\t\t_refreshers.Clear();\r\n\t\tif ( C.Session?.Setup is null )\r\n\t\t\treturn;\r\n\t\ttry\r\n\t\t{\r\n\t\t\tBuild();\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[weapon importer] {Title} panel failed: {e}\u0022 );\r\n\t\t\tLayout.Add( UiStyle.Colored( new Label( $\u0022This panel could not be shown: {e.Message}\u0022, this ) { WordWrap = true }, Theme.Red ) );\r\n\t\t}\r\n\t\tLayout.AddStretchCell();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERegisters a refresher and runs it once now.\u003C/summary\u003E\r\n\tprotected void Bind( Action refresh )\r\n\t{\r\n\t\t_refreshers.Add( refresh );\r\n\t\trefresh();\r\n\t}\r\n\r\n\tprotected Card AddCard( string icon, string title, out Layout header, string tooltip = null )\r\n\t{\r\n\t\tvar card = Layout.Add( new Card( this ) );\r\n\t\theader = card.Header( icon, title, tooltip );\r\n\t\treturn card;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA labelled slider with a live value readout.\u003C/summary\u003E\r\n\tprotected FloatSlider Slider( Widget owner, Layout parent, string caption, string tooltip, float min, float max, float value, Func\u003Cfloat, string\u003E format, Action\u003Cfloat\u003E edited, float step = 0f )\r\n\t{\r\n\t\tvar row = UiStyle.FieldRow( owner, parent, caption, tooltip );\r\n\t\tvar slider = row.Add( new FloatSlider( owner ) { Minimum = min, Maximum = max, Value = value, ToolTip = tooltip, FixedHeight = UiStyle.ControlHeight }, 1 );\r\n\t\tif ( step \u003E 0 )\r\n\t\t\tslider.Step = step;\r\n\t\tvar readout = row.Add( UiStyle.Muted( new Label( format( value ), owner ) { FixedWidth = UiStyle.PillColumn, FixedHeight = UiStyle.ControlHeight, Alignment = TextFlag.RightCenter } ) );\r\n\t\tslider.OnValueEdited = () =\u003E\r\n\t\t{\r\n\t\t\treadout.Text = format( slider.Value );\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tedited( slider.Value );\r\n\t\t\t}\r\n\t\t\tcatch ( Exception e )\r\n\t\t\t{\r\n\t\t\t\tC.SetStatus( e.Message, Theme.Red );\r\n\t\t\t}\r\n\t\t};\r\n\t\treturn slider;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA framed dropdown (framed like mocap inputs).\u003C/summary\u003E\r\n\tprotected ComboBox Combo( Widget owner, Layout row, string tooltip, float width = 0 )\r\n\t{\r\n\t\tvar combo = UiStyle.Framed( new ComboBox( owner ) { ToolTip = tooltip } );\r\n\t\tif ( width \u003E 0 )\r\n\t\t\tcombo.FixedWidth = width;\r\n\t\telse\r\n\t\t\tcombo.MinimumWidth = 60; // long bone/clip names must not widen the column\r\n\t\tcombo.MaxVisibleItems = 24;\r\n\t\trow.Add( combo, width \u003E 0 ? 0 : 1 );\r\n\t\treturn combo;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERuns a UI handler, turning exceptions into a red status line.\u003C/summary\u003E\r\n\tprotected void Safe( Action action )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\taction();\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[weapon importer] {e}\u0022 );\r\n\t\t\tC.SetStatus( e.Message, Theme.Red );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic override void OnDestroyed()\r\n\t{\r\n\t\tC.Changed -= MarkDirty;\r\n\t\tC.SessionReplaced -= ForceRebuild;\r\n\t\tbase.OnDestroyed();\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Tool/UI/Widgets/ProcessingIndicator.cs","FileName":"ProcessingIndicator.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"using Editor;\r\nusing Sandbox;\r\n\r\nnamespace WeaponImporter.Tool.UI;\r\n\r\n/// \u003Csummary\u003E\r\n/// Stands in the preview\u0027s place while the importer works: a green spinner arc with the step\r\n/// title and the latest progress message below it. Styled like the first-load drop box.\r\n/// \u003C/summary\u003E\r\npublic sealed class ProcessingIndicator : Widget\r\n{\r\n\tprivate string _title = \u0022Processing\u2026\u0022;\r\n\tprivate string _message = \u0022\u0022;\r\n\r\n\t/// \u003Csummary\u003EFalse once work stopped: the last message stays, without the spinner.\u003C/summary\u003E\r\n\tpublic bool Busy { get; set; } = true;\r\n\r\n\tpublic ProcessingIndicator( Widget parent ) : base( parent )\r\n\t{\r\n\t\tMinimumSize = new Vector2( 120, 120 );\r\n\t}\r\n\r\n\tpublic void SetTitle( string title )\r\n\t{\r\n\t\t_title = string.IsNullOrEmpty( title ) ? \u0022Processing\u2026\u0022 : title;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tpublic void SetMessage( string message )\r\n\t{\r\n\t\t_message = message ?? \u0022\u0022;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECalled every editor frame to turn the spinner.\u003C/summary\u003E\r\n\tpublic void Tick()\r\n\t{\r\n\t\tif ( Busy \u0026\u0026 Visible )\r\n\t\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnPaint() =\u003E DrawInto( LocalRect );\r\n\r\n\tpublic void DrawInto( Rect area )\r\n\t{\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.SetPen( Theme.ControlBackground.Lighten( .2f ), 1 );\r\n\t\tPaint.SetBrush( Theme.ControlBackground );\r\n\t\tPaint.DrawRect( area.Shrink( 1 ), 6 );\r\n\t\tvar center = area.Center;\r\n\t\tvar width = MathF.Min( 360f, area.Width - 40f );\r\n\t\tif ( Busy )\r\n\t\t{\r\n\t\t\tvar angle = (float)(RealTime.Now * 300 % 360);\r\n\t\t\tvar c = new Vector2( center.x, center.y - 30 );\r\n\t\t\tPaint.SetPen( Color.White.WithAlpha( .08f ), 4 );\r\n\t\t\tPaint.DrawArc( c, new Vector2( 18, 18 ), 0, 360 );\r\n\t\t\tPaint.SetPen( Theme.Green, 4 );\r\n\t\t\tPaint.DrawArc( c, new Vector2( 18, 18 ), angle, 100 );\r\n\t\t\tPaint.SetDefaultFont( 10, 600 );\r\n\t\t\tPaint.SetPen( Theme.Text );\r\n\t\t\tPaint.DrawText( new Rect( center.x - width / 2, center.y \u002B 2, width, 22 ), _title, TextFlag.Center );\r\n\t\t}\r\n\t\tPaint.SetDefaultFont();\r\n\t\tPaint.SetPen( Theme.TextLight );\r\n\t\tPaint.DrawText( new Rect( center.x - width / 2, center.y \u002B (Busy ? 26 : -20), width, 40 ), _message, TextFlag.Center | TextFlag.WordWrap );\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Tool/UI/Widgets/SearchPicker.cs","FileName":"SearchPicker.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"using Editor;\r\nusing Sandbox;\r\n\r\nnamespace WeaponImporter.Tool.UI;\r\n\r\n/// \u003Csummary\u003EOne choice in a \u003Csee cref=\u0022SearchPicker\u0022/\u003E.\u003C/summary\u003E\r\npublic sealed record PickerItem( string Value, string Label, string Detail = \u0022\u0022, bool Suggested = false, bool Current = false );\r\n\r\n/// \u003Csummary\u003E\r\n/// A popup list with a search box: type to filter (every word must match, anywhere in the\r\n/// name), Enter picks the first match. Suggested items come first. Used for choosing clips.\r\n/// \u003C/summary\u003E\r\npublic sealed class SearchPicker : PopupWidget\r\n{\r\n\tprivate readonly IReadOnlyList\u003CPickerItem\u003E _items;\r\n\tprivate readonly Action\u003Cstring\u003E _picked;\r\n\tprivate readonly LineEdit _search;\r\n\tprivate readonly Widget _list;\r\n\tprivate List\u003CPickerItem\u003E _shown = new();\r\n\r\n\tpublic SearchPicker( Widget parent, string title, IReadOnlyList\u003CPickerItem\u003E items, Action\u003Cstring\u003E picked ) : base( parent )\r\n\t{\r\n\t\t_items = items;\r\n\t\t_picked = picked;\r\n\t\tFixedWidth = 380;\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Margin = 8;\r\n\t\tLayout.Spacing = 6;\r\n\t\tLayout.Add( UiStyle.Bold( new Label( title, this ) ) );\r\n\t\t_search = Layout.Add( new LineEdit( this ) { PlaceholderText = \u0022Type to search\u2026\u0022, FixedHeight = UiStyle.ControlHeight } );\r\n\t\t_search.TextEdited \u002B= _ =\u003E Fill();\r\n\t\t_search.ReturnPressed \u002B= () =\u003E\r\n\t\t{\r\n\t\t\tif ( _shown.Count \u003E 0 )\r\n\t\t\t\tPick( _shown[0].Value );\r\n\t\t};\r\n\t\tvar scroll = Layout.Add( new ScrollArea( this ), 1 );\r\n\t\tscroll.HorizontalScrollbarMode = ScrollbarMode.Off;\r\n\t\tscroll.MaximumHeight = 420;\r\n\t\tscroll.MinimumHeight = Math.Min( 420, 30 * (items.Count \u002B 1) );\r\n\t\t_list = new Widget( scroll ) { Layout = Layout.Column() };\r\n\t\t_list.Layout.Spacing = 1;\r\n\t\tscroll.Canvas = _list;\r\n\t\tFill();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpens at the cursor with the search box focused.\u003C/summary\u003E\r\n\tpublic void Open()\r\n\t{\r\n\t\tOpenAtCursor();\r\n\t\t_search.Focus();\r\n\t}\r\n\r\n\tprivate void Fill()\r\n\t{\r\n\t\t_list.Layout.Clear( true );\r\n\t\t_shown = Filter( _items, _search.Text );\r\n\t\tforeach ( var item in _shown )\r\n\t\t{\r\n\t\t\tvar value = item.Value;\r\n\t\t\tvar row = _list.Layout.Add( new ClickRow( _list, () =\u003E Pick( value ), item.Detail ) );\r\n\t\t\trow.Selected = item.Current;\r\n\t\t\trow.Layout.Add( new IconButton( item.Current ? \u0022check\u0022 : item.Suggested ? \u0022auto_awesome\u0022 : \u0022movie\u0022 ) { Background = Color.Transparent, FixedSize = 22, TransparentForMouseEvents = true } );\r\n\t\t\trow.Layout.Add( new Label( UiStyle.Breakable( item.Label ), row ) { WordWrap = true, MinimumWidth = 20 }, 1 );\r\n\t\t\tif ( item.Detail.Length \u003E 0 )\r\n\t\t\t\trow.Layout.Add( UiStyle.Muted( new Label( item.Detail, row ), small: true ) );\r\n\t\t}\r\n\t\tif ( _shown.Count == 0 )\r\n\t\t\t_list.Layout.Add( UiStyle.Muted( new Label( \u0022Nothing matches.\u0022, _list ), small: true ) );\r\n\t\t_list.Layout.AddStretchCell();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EItems matching a query (every word, anywhere in the name or detail), suggested first.\u003C/summary\u003E\r\n\tpublic static List\u003CPickerItem\u003E Filter( IEnumerable\u003CPickerItem\u003E items, string query )\r\n\t{\r\n\t\tvar words = (query ?? \u0022\u0022).ToLowerInvariant().Split( new[] { \u0027 \u0027, \u0027_\u0027, \u0027-\u0027, \u0027|\u0027 }, StringSplitOptions.RemoveEmptyEntries );\r\n\t\treturn items\r\n\t\t\t.Where( i =\u003E words.All( w =\u003E i.Label.Contains( w, StringComparison.OrdinalIgnoreCase ) || i.Detail.Contains( w, StringComparison.OrdinalIgnoreCase ) ) )\r\n\t\t\t.OrderByDescending( i =\u003E i.Suggested )\r\n\t\t\t.ToList();\r\n\t}\r\n\r\n\tprivate void Pick( string value )\r\n\t{\r\n\t\t_picked( value );\r\n\t\tClose();\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Tool/UI/Widgets/NavList.cs","FileName":"NavList.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"using Editor;\r\nusing Sandbox;\r\n\r\nnamespace WeaponImporter.Tool.UI;\r\n\r\n/// \u003Csummary\u003E\r\n/// The editor\u0027s page list: one clickable row per page (icon, name, a status dot when something\r\n/// needs attention). Rows are the controls; there are no separate buttons.\r\n/// \u003C/summary\u003E\r\npublic sealed class NavList : Widget\r\n{\r\n\tpublic const float RowHeight = 34;\r\n\r\n\tprivate readonly List\u003C(string Icon, string Title, string Tip)\u003E _pages = new();\r\n\tprivate readonly Dictionary\u003Cint, Color?\u003E _status = new();\r\n\tprivate int _hover = -1;\r\n\r\n\tpublic int Selected { get; private set; }\r\n\tpublic Action\u003Cint\u003E OnSelected { get; set; }\r\n\r\n\tpublic NavList( Widget parent ) : base( parent )\r\n\t{\r\n\t\tMouseTracking = true;\r\n\t\tCursor = CursorShape.Finger;\r\n\t\tFixedWidth = 176;\r\n\t}\r\n\r\n\tpublic void Add( string icon, string title, string tooltip )\r\n\t{\r\n\t\t_pages.Add( (icon, title, tooltip) );\r\n\t\tMinimumHeight = _pages.Count * RowHeight \u002B 8;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDot beside a page: null hides it (all good), otherwise its colour (yellow, red).\u003C/summary\u003E\r\n\tpublic void SetStatus( int page, Color? color )\r\n\t{\r\n\t\tif ( _status.TryGetValue( page, out var old ) \u0026\u0026 old == color )\r\n\t\t\treturn;\r\n\t\t_status[page] = color;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe dot colour of a page (null = none), for tests.\u003C/summary\u003E\r\n\tpublic Color? StatusOf( int page ) =\u003E _status.TryGetValue( page, out var c ) ? c : null;\r\n\r\n\tpublic void Select( int page, bool notify = true )\r\n\t{\r\n\t\tpage = Math.Clamp( page, 0, Math.Max( 0, _pages.Count - 1 ) );\r\n\t\tSelected = page;\r\n\t\tUpdate();\r\n\t\tif ( notify )\r\n\t\t\tOnSelected?.Invoke( page );\r\n\t}\r\n\r\n\tprivate int RowAt( float y ) =\u003E y \u003C 4 ? -1 : (int)((y - 4) / RowHeight) is var i \u0026\u0026 i \u003C _pages.Count ? i : -1;\r\n\r\n\tprotected override void OnMouseMove( MouseEvent e )\r\n\t{\r\n\t\tvar row = RowAt( e.LocalPosition.y );\r\n\t\tif ( row != _hover )\r\n\t\t{\r\n\t\t\t_hover = row;\r\n\t\t\tToolTip = row \u003E= 0 ? _pages[row].Tip : null;\r\n\t\t\tUpdate();\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnMouseLeave()\r\n\t{\r\n\t\t_hover = -1;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnMousePress( MouseEvent e )\r\n\t{\r\n\t\tif ( !e.LeftMouseButton )\r\n\t\t\treturn;\r\n\t\tvar row = RowAt( e.LocalPosition.y );\r\n\t\tif ( row \u003E= 0 )\r\n\t\t\tSelect( row );\r\n\t\te.Accepted = true;\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.Antialiasing = true;\r\n\t\tfor ( var i = 0; i \u003C _pages.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar (icon, title, _) = _pages[i];\r\n\t\t\tvar rect = new Rect( 0, 4 \u002B i * RowHeight, Width, RowHeight - 2 );\r\n\t\t\tvar selected = i == Selected;\r\n\t\t\tif ( selected || i == _hover )\r\n\t\t\t{\r\n\t\t\t\tPaint.ClearPen();\r\n\t\t\t\tPaint.SetBrush( selected ? Theme.ControlBackground.Lighten( .35f ) : Theme.ControlBackground.Lighten( .15f ) );\r\n\t\t\t\tPaint.DrawRect( rect, 5 );\r\n\t\t\t}\r\n\t\t\tif ( selected )\r\n\t\t\t{\r\n\t\t\t\tPaint.ClearPen();\r\n\t\t\t\tPaint.SetBrush( Theme.Green );\r\n\t\t\t\tPaint.DrawRect( new Rect( rect.Left, rect.Top \u002B 7, 3, rect.Height - 14 ), 1.5f );\r\n\t\t\t}\r\n\t\t\tPaint.SetPen( selected ? Theme.Green : Theme.TextLight );\r\n\t\t\tPaint.DrawIcon( new Rect( rect.Left \u002B 12, rect.Top, 20, rect.Height ), icon, 16 );\r\n\t\t\tPaint.SetPen( selected ? Theme.Text : Theme.Text.WithAlpha( .8f ) );\r\n\t\t\tPaint.SetDefaultFont( 9, selected ? 600 : 400 );\r\n\t\t\tPaint.DrawText( new Rect( rect.Left \u002B 40, rect.Top, rect.Width - 60, rect.Height ), title, TextFlag.LeftCenter );\r\n\t\t\tif ( _status.TryGetValue( i, out var status ) \u0026\u0026 status is { } dot )\r\n\t\t\t{\r\n\t\t\t\tPaint.ClearPen();\r\n\t\t\t\tPaint.SetBrush( dot );\r\n\t\t\t\tPaint.DrawRect( new Rect( rect.Right - 18, rect.Center.y - 4, 8, 8 ), 4 );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Core/Analysis/AnimationClassifier.cs","FileName":"AnimationClassifier.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nnamespace WeaponImporter.Core.Analysis;\r\n\r\n/// \u003Csummary\u003EA guess at an animation\u0027s role, with how sure the classifier is.\u003C/summary\u003E\r\npublic sealed record AnimationGuess(string Animation, AnimationRole Role, float Confidence, string Reason)\r\n{\r\n    /// \u003Csummary\u003EFirst-person (view model) or third-person (world / body) clip, when the name says so.\u003C/summary\u003E\r\n    public AnimationPerspective Perspective { get; init; }\r\n}\r\n\r\npublic enum AnimationPerspective { Any, FirstPerson, ThirdPerson }\r\n\r\n/// \u003Csummary\u003E\r\n/// Maps animation names onto \u003Csee cref=\u0022AnimationRole\u0022/\u003Es using aliases common across game\r\n/// rigs and marketplace packs (\u003Cc\u003Eshoot\u003C/c\u003E, \u003Cc\u003Ereload_empty\u003C/c\u003E, \u003Cc\u003Edeploy\u003C/c\u003E, \u003Cc\u003Elookat\u003C/c\u003E,\r\n/// \u003Cc\u003Eads_fire\u003C/c\u003E, \u003Cc\u003Erechamber\u003C/c\u003E...). Scoring is token based so prefixes, numbering and\r\n/// casing do not matter.\r\n/// \u003C/summary\u003E\r\npublic static class AnimationClassifier\r\n{\r\n    private sealed record Rule(AnimationRole Role, string[] Any, string[]? Joined = null, string[]? Requires = null, string[]? Excludes = null, float Weight = 1f);\r\n\r\n    private static readonly string[] AimTokens = { \u0022ads\u0022, \u0022aim\u0022, \u0022aiming\u0022, \u0022iron\u0022, \u0022ironsight\u0022, \u0022ironsights\u0022, \u0022sight\u0022, \u0022sights\u0022, \u0022zoom\u0022, \u0022scope\u0022 };\r\n    private static readonly string[] AimLoopTokens = { \u0022idle\u0022, \u0022loop\u0022, \u0022hold\u0022, \u0022pose\u0022, \u0022static\u0022, \u0022aiming\u0022, \u0022breathing\u0022, \u0022breath\u0022 };\r\n    private static readonly string[] AimOutTokens = { \u0022out\u0022, \u0022exit\u0022, \u0022end\u0022, \u0022lower\u0022, \u0022leave\u0022, \u0022stop\u0022, \u0022release\u0022 };\r\n\r\n    // Order matters only for ties; more specific rules carry higher weights.\r\n    private static readonly Rule[] Rules =\r\n    {\r\n        new(AnimationRole.AdsFire, new[] { \u0022fire\u0022, \u0022shoot\u0022, \u0022shot\u0022, \u0022attack\u0022, \u0022firing\u0022 }, new[] { \u0022adsfire\u0022, \u0022aimfire\u0022, \u0022ironfire\u0022, \u0022firead\u0022, \u0022fireaim\u0022, \u0022fireiron\u0022, \u0022sightfire\u0022, \u0022scopefire\u0022 }, Requires: new[] { \u0022ads\u0022, \u0022aim\u0022, \u0022iron\u0022, \u0022ironsight\u0022, \u0022ironsights\u0022, \u0022sight\u0022, \u0022scope\u0022, \u0022zoom\u0022 }, Weight: 1.35f),\r\n        new(AnimationRole.FireEmpty, new[] { \u0022fire\u0022, \u0022shoot\u0022, \u0022shot\u0022, \u0022attack\u0022, \u0022dry\u0022, \u0022dryfire\u0022 }, new[] { \u0022fireempty\u0022, \u0022firelast\u0022, \u0022shootempty\u0022, \u0022shootlast\u0022, \u0022dryfire\u0022, \u0022lastshot\u0022, \u0022emptyfire\u0022 }, Requires: new[] { \u0022empty\u0022, \u0022last\u0022, \u0022dry\u0022, \u0022final\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.Fire, new[] { \u0022fire\u0022, \u0022shoot\u0022, \u0022shot\u0022, \u0022attack\u0022, \u0022firing\u0022, \u0022recoil\u0022, \u0022primary\u0022, \u0022burst\u0022, \u0022auto\u0022, \u0022semi\u0022 }, Excludes: new[] { \u0022reload\u0022, \u0022select\u0022, \u0022mode\u0022, \u0022switch\u0022 }),\r\n\r\n        new(AnimationRole.EmptyReload, new[] { \u0022reload\u0022, \u0022rld\u0022, \u0022reloading\u0022 }, new[] { \u0022reloadempty\u0022, \u0022emptyreload\u0022, \u0022reloadlong\u0022, \u0022reloaddry\u0022, \u0022reloadfull\u0022 }, Requires: new[] { \u0022empty\u0022, \u0022dry\u0022, \u0022long\u0022, \u0022full\u0022, \u0022outofammo\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.TacticalReload, new[] { \u0022reload\u0022, \u0022rld\u0022, \u0022reloading\u0022 }, new[] { \u0022reloadtac\u0022, \u0022tacreload\u0022, \u0022reloadtactical\u0022, \u0022tacticalreload\u0022, \u0022reloadshort\u0022, \u0022reloadpartial\u0022 }, Requires: new[] { \u0022tac\u0022, \u0022tactical\u0022, \u0022short\u0022, \u0022partial\u0022, \u0022fast\u0022, \u0022speed\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.Reload, new[] { \u0022reload\u0022, \u0022rld\u0022, \u0022reloading\u0022, \u0022magswap\u0022, \u0022magchange\u0022 }, Excludes: new[] { \u0022start\u0022, \u0022end\u0022, \u0022loop\u0022, \u0022insert\u0022, \u0022begin\u0022, \u0022enter\u0022, \u0022finish\u0022, \u0022exit\u0022, \u0022shell\u0022, \u0022single\u0022 }),\r\n        // Shell-by-shell reloads (shotguns, tube-fed rifles): start / one shell / end.\r\n        new(AnimationRole.ReloadStart, new[] { \u0022start\u0022, \u0022begin\u0022, \u0022enter\u0022, \u0022open\u0022, \u0022in\u0022, \u0022intro\u0022 }, new[] { \u0022reloadstart\u0022, \u0022startreload\u0022, \u0022reloadbegin\u0022, \u0022reloadenter\u0022, \u0022reloadopen\u0022, \u0022reloadintro\u0022, \u0022idletoreload\u0022, \u0022toreload\u0022 }, Requires: new[] { \u0022reload\u0022, \u0022rld\u0022, \u0022reloading\u0022, \u0022load\u0022, \u0022loading\u0022 }, Excludes: new[] { \u0022fire\u0022, \u0022shoot\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.ReloadInsert, new[] { \u0022insert\u0022, \u0022loop\u0022, \u0022shell\u0022, \u0022single\u0022, \u0022load\u0022, \u0022inserting\u0022, \u0022step\u0022 }, new[] { \u0022reloadinsert\u0022, \u0022reloadstep\u0022, \u0022insertshell\u0022, \u0022shellinsert\u0022, \u0022reloadloop\u0022, \u0022loadshell\u0022, \u0022shellload\u0022, \u0022reloadshell\u0022, \u0022reloadsingle\u0022, \u0022insertround\u0022, \u0022loadround\u0022 }, Requires: new[] { \u0022reload\u0022, \u0022rld\u0022, \u0022reloading\u0022, \u0022insert\u0022, \u0022shell\u0022, \u0022round\u0022, \u0022load\u0022 }, Excludes: new[] { \u0022start\u0022, \u0022begin\u0022, \u0022end\u0022, \u0022finish\u0022, \u0022exit\u0022, \u0022fire\u0022, \u0022shoot\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.ReloadEnd, new[] { \u0022end\u0022, \u0022finish\u0022, \u0022exit\u0022, \u0022close\u0022, \u0022out\u0022, \u0022outro\u0022, \u0022stop\u0022 }, new[] { \u0022reloadend\u0022, \u0022endreload\u0022, \u0022reloadfinish\u0022, \u0022reloadexit\u0022, \u0022reloadclose\u0022, \u0022reloadoutro\u0022, \u0022reloadtoidle\u0022, \u0022reloadto\u0022 }, Requires: new[] { \u0022reload\u0022, \u0022rld\u0022, \u0022reloading\u0022, \u0022load\u0022, \u0022loading\u0022 }, Excludes: new[] { \u0022fire\u0022, \u0022shoot\u0022 }, Weight: 1.3f),\r\n\r\n        new(AnimationRole.Unjam, new[] { \u0022unjam\u0022, \u0022clear\u0022, \u0022clearjam\u0022, \u0022fixjam\u0022, \u0022malfunctionclear\u0022, \u0022tapack\u0022, \u0022remedy\u0022 }, new[] { \u0022unjam\u0022, \u0022clearjam\u0022, \u0022fixjam\u0022, \u0022jamclear\u0022, \u0022jamfix\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.Jam, new[] { \u0022jam\u0022, \u0022jammed\u0022, \u0022malfunction\u0022, \u0022misfire\u0022, \u0022stovepipe\u0022 }, Excludes: new[] { \u0022clear\u0022, \u0022fix\u0022, \u0022un\u0022 }),\r\n        new(AnimationRole.Bolt, new[] { \u0022bolt\u0022, \u0022rechamber\u0022, \u0022chamber\u0022, \u0022pump\u0022, \u0022charge\u0022, \u0022charging\u0022, \u0022cock\u0022, \u0022cocking\u0022, \u0022rack\u0022, \u0022cycle\u0022, \u0022lever\u0022, \u0022slide\u0022 }, Excludes: new[] { \u0022reload\u0022, \u0022fire\u0022, \u0022shoot\u0022, \u0022idle\u0022 }),\r\n\r\n        new(AnimationRole.Draw, new[] { \u0022draw\u0022, \u0022deploy\u0022, \u0022equip\u0022, \u0022pullout\u0022, \u0022raise\u0022, \u0022unholster\u0022, \u0022takeout\u0022, \u0022ready\u0022, \u0022select\u0022, \u0022pickup\u0022 }, new[] { \u0022firstdraw\u0022, \u0022drawfirst\u0022, \u0022pullout\u0022, \u0022takeout\u0022 }, Excludes: new[] { \u0022reload\u0022 }),\r\n        new(AnimationRole.Holster, new[] { \u0022holster\u0022, \u0022putaway\u0022, \u0022unequip\u0022, \u0022lower\u0022, \u0022stow\u0022, \u0022putdown\u0022, \u0022deselect\u0022, \u0022hide\u0022 }, new[] { \u0022putaway\u0022, \u0022putdown\u0022 }, Excludes: new[] { \u0022unholster\u0022 }),\r\n        new(AnimationRole.Inspect, new[] { \u0022inspect\u0022, \u0022lookat\u0022, \u0022examine\u0022, \u0022check\u0022, \u0022fidget\u0022, \u0022admire\u0022, \u0022showoff\u0022, \u0022look\u0022 }, new[] { \u0022lookat\u0022, \u0022checkmag\u0022, \u0022magcheck\u0022, \u0022showoff\u0022 }),\r\n\r\n        new(AnimationRole.Sprint, new[] { \u0022sprint\u0022, \u0022run\u0022, \u0022running\u0022, \u0022dash\u0022 }),\r\n        new(AnimationRole.Walk, new[] { \u0022walk\u0022, \u0022walking\u0022, \u0022move\u0022, \u0022moving\u0022, \u0022jog\u0022, \u0022strafe\u0022, \u0022bob\u0022 }),\r\n        // Aiming, any naming style: raising the sights (\u0022Aim_In\u0022, \u0022ADS\u0022, \u0022IronIn\u0022, \u0022Zoom_Enter\u0022),\r\n        // the held loop (\u0022Aim_Idle\u0022, \u0022ADS_Loop\u0022), lowering them (\u0022Aim_Out\u0022, \u0022ADS_Exit\u0022, \u0022Unaim\u0022).\r\n        new(AnimationRole.Ads, AimTokens.Concat(new[] { \u0022adsin\u0022, \u0022aimin\u0022 }).ToArray(), new[] { \u0022aimin\u0022, \u0022adsin\u0022, \u0022ironin\u0022, \u0022zoomin\u0022, \u0022sightin\u0022, \u0022scopein\u0022, \u0022aimenter\u0022, \u0022adsenter\u0022, \u0022aimstart\u0022, \u0022adsstart\u0022 }, Excludes: AimOutTokens.Concat(AimLoopTokens).Concat(new[] { \u0022fire\u0022, \u0022shoot\u0022, \u0022unaim\u0022, \u0022unads\u0022 }).ToArray()),\r\n        new(AnimationRole.AdsIdle, AimLoopTokens, new[] { \u0022aimidle\u0022, \u0022adsidle\u0022, \u0022aimloop\u0022, \u0022adsloop\u0022, \u0022ironidle\u0022, \u0022zoomidle\u0022, \u0022aimhold\u0022, \u0022adshold\u0022, \u0022aimingidle\u0022 }, Requires: AimTokens, Excludes: AimOutTokens.Concat(new[] { \u0022fire\u0022, \u0022shoot\u0022 }).ToArray(), Weight: 1.3f),\r\n        new(AnimationRole.AdsOut, AimOutTokens.Concat(new[] { \u0022unaim\u0022, \u0022unads\u0022, \u0022adsout\u0022, \u0022aimout\u0022 }).ToArray(), new[] { \u0022aimout\u0022, \u0022adsout\u0022, \u0022ironout\u0022, \u0022zoomout\u0022, \u0022sightout\u0022, \u0022scopeout\u0022, \u0022unaim\u0022, \u0022unads\u0022, \u0022aimexit\u0022, \u0022adsexit\u0022, \u0022aimend\u0022, \u0022adsend\u0022 }, Requires: AimTokens.Concat(new[] { \u0022unaim\u0022, \u0022unads\u0022, \u0022adsout\u0022, \u0022aimout\u0022 }).ToArray(), Excludes: new[] { \u0022fire\u0022, \u0022shoot\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.Melee, new[] { \u0022melee\u0022, \u0022bash\u0022, \u0022stab\u0022, \u0022slash\u0022, \u0022swing\u0022, \u0022hit\u0022, \u0022knife\u0022, \u0022punch\u0022, \u0022butt\u0022, \u0022strike\u0022, \u0022attackmelee\u0022, \u0022jab\u0022, \u0022hook\u0022, \u0022uppercut\u0022, \u0022chop\u0022, \u0022thrust\u0022 }, new[] { \u0022meleeattack\u0022 }, Excludes: new[] { \u0022heavy\u0022, \u0022strong\u0022, \u0022power\u0022, \u0022charged\u0022, \u0022secondary\u0022, \u0022alt\u0022, \u0022get\u0022, \u0022react\u0022, \u0022reaction\u0022, \u0022damage\u0022, \u0022hurt\u0022, \u0022gethit\u0022, \u0022hitreact\u0022 }, Weight: 1.1f),\r\n        new(AnimationRole.Attack2, new[] { \u0022heavy\u0022, \u0022strong\u0022, \u0022power\u0022, \u0022charged\u0022, \u0022secondary\u0022, \u0022alt\u0022, \u0022attack2\u0022, \u0022special\u0022 }, new[] { \u0022heavyattack\u0022, \u0022attackheavy\u0022, \u0022secondaryattack\u0022, \u0022attacksecondary\u0022, \u0022attack2\u0022, \u0022altattack\u0022, \u0022attackalt\u0022, \u0022powerattack\u0022, \u0022chargedattack\u0022, \u0022strongattack\u0022, \u0022heavyslash\u0022, \u0022heavyswing\u0022 }, Requires: new[] { \u0022attack\u0022, \u0022slash\u0022, \u0022swing\u0022, \u0022stab\u0022, \u0022hit\u0022, \u0022strike\u0022, \u0022punch\u0022, \u0022melee\u0022, \u0022chop\u0022, \u0022attack2\u0022 }, Weight: 1.3f),\r\n        // Guarding: raise / hold / lower.\r\n        new(AnimationRole.BlockStart, new[] { \u0022start\u0022, \u0022begin\u0022, \u0022enter\u0022, \u0022in\u0022, \u0022raise\u0022, \u0022up\u0022 }, new[] { \u0022blockstart\u0022, \u0022startblock\u0022, \u0022blockin\u0022, \u0022blockbegin\u0022, \u0022parrystart\u0022, \u0022guardstart\u0022, \u0022guardup\u0022, \u0022blockenter\u0022 }, Requires: new[] { \u0022block\u0022, \u0022parry\u0022, \u0022guard\u0022, \u0022defend\u0022, \u0022defense\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.Block, new[] { \u0022block\u0022, \u0022parry\u0022, \u0022guard\u0022, \u0022defend\u0022, \u0022defense\u0022, \u0022blocking\u0022 }, new[] { \u0022blockloop\u0022, \u0022blockidle\u0022, \u0022blockhold\u0022, \u0022parryloop\u0022, \u0022guardloop\u0022, \u0022guardidle\u0022 }, Excludes: new[] { \u0022start\u0022, \u0022begin\u0022, \u0022enter\u0022, \u0022end\u0022, \u0022stop\u0022, \u0022exit\u0022, \u0022out\u0022 }),\r\n        new(AnimationRole.BlockEnd, new[] { \u0022end\u0022, \u0022stop\u0022, \u0022exit\u0022, \u0022out\u0022, \u0022lower\u0022, \u0022down\u0022, \u0022release\u0022 }, new[] { \u0022blockend\u0022, \u0022blockstop\u0022, \u0022endblock\u0022, \u0022blockout\u0022, \u0022blockexit\u0022, \u0022parryend\u0022, \u0022guardend\u0022, \u0022guarddown\u0022 }, Requires: new[] { \u0022block\u0022, \u0022parry\u0022, \u0022guard\u0022, \u0022defend\u0022, \u0022defense\u0022 }, Weight: 1.3f),\r\n        // Items: use once, or start / hold / finish a use.\r\n        new(AnimationRole.Use, new[] { \u0022use\u0022, \u0022drink\u0022, \u0022eat\u0022, \u0022inject\u0022, \u0022heal\u0022, \u0022consume\u0022, \u0022apply\u0022, \u0022bandage\u0022, \u0022smoke\u0022, \u0022injection\u0022, \u0022healing\u0022, \u0022sip\u0022, \u0022gulp\u0022, \u0022swallow\u0022, \u0022spray\u0022, \u0022activate\u0022, \u0022press\u0022, \u0022toggle\u0022 }, new[] { \u0022useitem\u0022, \u0022itemuse\u0022 }, Excludes: new[] { \u0022start\u0022, \u0022begin\u0022, \u0022enter\u0022, \u0022loop\u0022, \u0022hold\u0022, \u0022end\u0022, \u0022stop\u0022, \u0022exit\u0022, \u0022finish\u0022, \u0022takeout\u0022, \u0022out\u0022 }),\r\n        new(AnimationRole.UseStart, new[] { \u0022start\u0022, \u0022begin\u0022, \u0022enter\u0022, \u0022in\u0022, \u0022raise\u0022, \u0022up\u0022, \u0022open\u0022 }, new[] { \u0022usestart\u0022, \u0022startuse\u0022, \u0022drinkstart\u0022, \u0022eatstart\u0022, \u0022takestart\u0022, \u0022healstart\u0022, \u0022injectstart\u0022 }, Requires: new[] { \u0022use\u0022, \u0022drink\u0022, \u0022eat\u0022, \u0022inject\u0022, \u0022heal\u0022, \u0022consume\u0022, \u0022take\u0022, \u0022apply\u0022, \u0022bandage\u0022, \u0022smoke\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.UseLoop, new[] { \u0022loop\u0022, \u0022hold\u0022, \u0022idle\u0022, \u0022holding\u0022, \u0022continue\u0022 }, new[] { \u0022useloop\u0022, \u0022drinkloop\u0022, \u0022eatloop\u0022, \u0022takeloop\u0022, \u0022healloop\u0022, \u0022usehold\u0022 }, Requires: new[] { \u0022use\u0022, \u0022drink\u0022, \u0022eat\u0022, \u0022inject\u0022, \u0022heal\u0022, \u0022consume\u0022, \u0022take\u0022, \u0022apply\u0022, \u0022bandage\u0022, \u0022smoke\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.UseEnd, new[] { \u0022end\u0022, \u0022stop\u0022, \u0022exit\u0022, \u0022finish\u0022, \u0022out\u0022, \u0022lower\u0022, \u0022down\u0022, \u0022close\u0022 }, new[] { \u0022useend\u0022, \u0022usestop\u0022, \u0022drinkend\u0022, \u0022eatend\u0022, \u0022takestop\u0022, \u0022takeend\u0022, \u0022healend\u0022, \u0022injectend\u0022 }, Requires: new[] { \u0022use\u0022, \u0022drink\u0022, \u0022eat\u0022, \u0022inject\u0022, \u0022heal\u0022, \u0022consume\u0022, \u0022take\u0022, \u0022apply\u0022, \u0022bandage\u0022, \u0022smoke\u0022 }, Weight: 1.3f),\r\n        new(AnimationRole.Throw, new[] { \u0022throw\u0022, \u0022toss\u0022, \u0022lob\u0022, \u0022pitch\u0022, \u0022hurl\u0022 }, new[] { \u0022throwgrenade\u0022, \u0022grenadethrow\u0022 }, Weight: 1.2f),\r\n        new(AnimationRole.Idle, new[] { \u0022idle\u0022, \u0022rest\u0022, \u0022hold\u0022, \u0022static\u0022, \u0022pose\u0022, \u0022bind\u0022, \u0022stand\u0022, \u0022base\u0022, \u0022breathing\u0022, \u0022breath\u0022, \u0022breathe\u0022 }, new[] { \u0022breathing\u0022, \u0022idleloop\u0022 }, Excludes: new[] { \u0022to\u0022, \u0022fire\u0022, \u0022reload\u0022 }, Weight: 0.9f),\r\n    };\r\n\r\n    private static readonly string[] FirstPersonTokens = { \u0022fp\u0022, \u00221p\u0022, \u0022vm\u0022, \u0022v\u0022, \u0022view\u0022, \u0022viewmodel\u0022, \u0022arms\u0022, \u0022firstperson\u0022, \u0022fps\u0022 };\r\n    private static readonly string[] ThirdPersonTokens = { \u0022tp\u0022, \u00223p\u0022, \u0022wm\u0022, \u0022w\u0022, \u0022world\u0022, \u0022worldmodel\u0022, \u0022thirdperson\u0022, \u0022tps\u0022, \u0022body\u0022, \u0022player\u0022, \u0022citizen\u0022 };\r\n\r\n    /// \u003Csummary\u003EClassifies one animation name.\u003C/summary\u003E\r\n    public static AnimationGuess Classify(string name, AnimationMotionHint? motion = null)\r\n    {\r\n        var tokens = NameTokens.Split(name);\r\n        var joined = NameTokens.Joined(tokens);\r\n        var perspective = NameTokens.Has(tokens, ThirdPersonTokens) ? AnimationPerspective.ThirdPerson\r\n            : NameTokens.Has(tokens, FirstPersonTokens) ? AnimationPerspective.FirstPerson\r\n            : AnimationPerspective.Any;\r\n\r\n        AnimationRole best = AnimationRole.Unknown;\r\n        float bestScore = 0f;\r\n        string reason = \u0022no matching name\u0022;\r\n\r\n        foreach (var rule in Rules)\r\n        {\r\n            var score = 0f;\r\n            string why = \u0022\u0022;\r\n            if (rule.Joined is { } joinedAliases \u0026\u0026 joinedAliases.Any(j =\u003E joined.Contains(j, StringComparison.Ordinal)))\r\n            {\r\n                score = 1f;\r\n                why = $\u0022name contains \u0027{joinedAliases.First(j =\u003E joined.Contains(j, StringComparison.Ordinal))}\u0027\u0022;\r\n            }\r\n            else if (NameTokens.Has(tokens, rule.Any))\r\n            {\r\n                if (rule.Requires is { } req \u0026\u0026 !NameTokens.Has(tokens, req))\r\n                    continue;\r\n                score = rule.Requires is null ? 0.8f : 0.95f;\r\n                why = $\u0022\u0027{tokens.First(t =\u003E rule.Any.Any(a =\u003E t == a || (a.Length \u003E= 4 \u0026\u0026 t.StartsWith(a, StringComparison.Ordinal))))}\u0027 in name\u0022;\r\n            }\r\n            else\r\n            {\r\n                continue;\r\n            }\r\n\r\n            if (rule.Excludes is { } ex \u0026\u0026 NameTokens.Has(tokens, ex))\r\n                score *= 0.45f;\r\n            // The role\u0027s own word (\u0022Idle\u0022) beats a synonym (\u0022Hold\u0022) on a tie.\r\n            if (tokens.Contains(rule.Any[0]))\r\n                score \u002B= 0.02f;\r\n            score *= rule.Weight;\r\n\r\n            if (score \u003E bestScore)\r\n            {\r\n                bestScore = score;\r\n                best = rule.Role;\r\n                reason = why;\r\n            }\r\n        }\r\n\r\n        // Motion evidence breaks ties and rescues meaningless names (\u0022anim_03\u0022).\r\n        if (motion is { } m)\r\n        {\r\n            var (role, conf, why) = FromMotion(m);\r\n            if (best == AnimationRole.Unknown \u0026\u0026 role != AnimationRole.Unknown)\r\n            {\r\n                best = role;\r\n                bestScore = conf;\r\n                reason = why;\r\n            }\r\n            else if (role == best \u0026\u0026 role != AnimationRole.Unknown)\r\n            {\r\n                bestScore = MathF.Min(1f, bestScore \u002B 0.1f);\r\n            }\r\n        }\r\n\r\n        return new AnimationGuess(name, best, MathF.Min(1f, bestScore), reason) { Perspective = perspective };\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Classifies a set of animations and resolves duplicates: each role goes to the clip with\r\n    /// the highest confidence, preferring shorter base names (\u0022reload\u0022 over \u0022reload_02\u0022).\r\n    /// \u003C/summary\u003E\r\n    public static IReadOnlyDictionary\u003CAnimationRole, AnimationGuess\u003E Assign(IEnumerable\u003CAnimationGuess\u003E guesses, AnimationPerspective prefer = AnimationPerspective.Any)\r\n    {\r\n        var result = new Dictionary\u003CAnimationRole, AnimationGuess\u003E();\r\n        foreach (var group in guesses.Where(g =\u003E g.Role != AnimationRole.Unknown).GroupBy(g =\u003E g.Role))\r\n        {\r\n            var pick = group\r\n                .OrderByDescending(g =\u003E g.Confidence \u002B (prefer != AnimationPerspective.Any \u0026\u0026 g.Perspective == prefer ? 0.15f : 0f) - (prefer != AnimationPerspective.Any \u0026\u0026 g.Perspective != AnimationPerspective.Any \u0026\u0026 g.Perspective != prefer ? 0.3f : 0f))\r\n                .ThenBy(g =\u003E g.Animation.Count(char.IsDigit))\r\n                .ThenBy(g =\u003E g.Animation.Length)\r\n                .First();\r\n            result[group.Key] = pick;\r\n        }\r\n\r\n        // A lone \u0022reload\u0022 that is actually the empty one leaves Reload itself free: promote.\r\n        if (!result.ContainsKey(AnimationRole.Reload))\r\n        {\r\n            if (result.TryGetValue(AnimationRole.TacticalReload, out var tac))\r\n                result[AnimationRole.Reload] = tac with { Role = AnimationRole.Reload, Confidence = tac.Confidence * 0.9f, Reason = \u0022tactical reload used as reload\u0022 };\r\n            else if (result.TryGetValue(AnimationRole.EmptyReload, out var empty))\r\n                result[AnimationRole.Reload] = empty with { Role = AnimationRole.Reload, Confidence = empty.Confidence * 0.85f, Reason = \u0022empty reload used as reload\u0022 };\r\n        }\r\n        // \u0022IdleToReload\u0022 / \u0022Reload\u0022 / \u0022ReloadToIdle\u0022: between a start and an end, the plain\r\n        // reload is the one-shell part.\r\n        if (result.ContainsKey(AnimationRole.ReloadStart) \u0026\u0026 result.ContainsKey(AnimationRole.ReloadEnd)\r\n            \u0026\u0026 !result.ContainsKey(AnimationRole.ReloadInsert) \u0026\u0026 result.TryGetValue(AnimationRole.Reload, out var middle))\r\n        {\r\n            result[AnimationRole.ReloadInsert] = middle with { Role = AnimationRole.ReloadInsert, Reason = \u0022reload between a reload start and end\u0022 };\r\n            result.Remove(AnimationRole.Reload);\r\n        }\r\n        if (!result.ContainsKey(AnimationRole.Fire) \u0026\u0026 result.TryGetValue(AnimationRole.AdsFire, out var adsFire))\r\n            result[AnimationRole.Fire] = adsFire with { Role = AnimationRole.Fire, Confidence = adsFire.Confidence * 0.8f, Reason = \u0022ADS fire used as fire\u0022 };\r\n        return result;\r\n    }\r\n\r\n    private static (AnimationRole, float, string) FromMotion(AnimationMotionHint m)\r\n    {\r\n        if (m.MagazineTravel \u003E 2f \u0026\u0026 m.Duration \u003E 1f)\r\n            return (AnimationRole.Reload, 0.6f, \u0022magazine leaves the weapon\u0022);\r\n        if (m.BoltTravel \u003E 0.3f \u0026\u0026 m.Duration \u003C 0.6f)\r\n            return (AnimationRole.Fire, 0.55f, \u0022short clip with slide/bolt cycling\u0022);\r\n        if (m.BoltTravel \u003E 0.3f \u0026\u0026 m.Duration \u003E= 0.6f)\r\n            return (AnimationRole.Bolt, 0.5f, \u0022bolt cycles without magazine change\u0022);\r\n        if (m.RootTravel \u003C 0.05f \u0026\u0026 m.Duration \u003E 0.5f \u0026\u0026 m.MaxBoneMotion \u003C 0.1f)\r\n            return (AnimationRole.Idle, 0.5f, \u0022almost no motion\u0022);\r\n        return (AnimationRole.Unknown, 0f, \u0022motion inconclusive\u0022);\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003EMotion measurements used when a name alone is not enough.\u003C/summary\u003E\r\npublic readonly record struct AnimationMotionHint(float Duration, float MagazineTravel, float BoltTravel, float RootTravel, float MaxBoneMotion);\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Code/WeaponImporter/CharacterOverlay.cs","FileName":"CharacterOverlay.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":397047,"IsPrivate":false,"Code":"using System.Text.Json;\r\n\r\nnamespace WeaponImporter;\r\n\r\n/// \u003Csummary\u003EA replacement character animation for one action.\u003C/summary\u003E\r\npublic sealed class OverlayInfo\r\n{\r\n    public string Role;\r\n    /// \u003Csummary\u003EModel that owns the sequence (\u0022\u0022 = the character\u0027s own model). Must share the character\u0027s skeleton.\u003C/summary\u003E\r\n    public string Model;\r\n    public string Sequence;\r\n    /// \u003Csummary\u003EFade in/out time in seconds.\u003C/summary\u003E\r\n    public float Blend = 0.2f;\r\n    /// \u003Csummary\u003EMore sequences of the same model played in turn with \u003Csee cref=\u0022Sequence\u0022/\u003E (attack combos).\u003C/summary\u003E\r\n    public List\u003Cstring\u003E Variants = new();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Plays a replacement character sequence for an action on a hidden model with the same\r\n/// skeleton and blends it over the upper body (spine and everything above), so the weapon and\r\n/// the hands follow it while the animgraph keeps driving the legs.\r\n/// \u003C/summary\u003E\r\ninternal sealed class CharacterOverlay\r\n{\r\n    private SceneModel _model;\r\n    private Model _source;\r\n    private string _sequence;\r\n    private readonly Dictionary\u003Cstring, int\u003E _boneIndex = new( StringComparer.Ordinal );\r\n\r\n    public bool Active { get; private set; }\r\n    public float Weight { get; private set; }\r\n    public float Duration { get; private set; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Bone the overlay is attached at: its upper body is moved so this bone stays where the\r\n    /// character\u0027s own animation has it (a lunge or a crouch in the sequence bends the upper\r\n    /// body without pulling it off the hips the animgraph keeps).\r\n    /// \u003C/summary\u003E\r\n    public const string AnchorBone = \u0022spine_0\u0022;\r\n\r\n    private Vector3 _offset;\r\n\r\n    /// \u003Csummary\u003EStarts the overlay; returns false when the model or sequence is unusable.\u003C/summary\u003E\r\n    public bool Begin( SkinnedModelRenderer body, OverlayInfo info, out string error ) =\u003E Begin( body, info, info?.Sequence, out error );\r\n\r\n    /// \u003Csummary\u003EStarts the overlay with one of the info\u0027s sequences (its own or a variant).\u003C/summary\u003E\r\n    public bool Begin( SkinnedModelRenderer body, OverlayInfo info, string sequence, out string error )\r\n    {\r\n        error = null;\r\n        Active = false;\r\n        Weight = 0f;\r\n        if ( info == null || string.IsNullOrEmpty( sequence ) || body?.SceneModel == null )\r\n            return false;\r\n\r\n        var model = string.IsNullOrEmpty( info.Model ) ? body.Model : Sandbox.Model.Load( info.Model );\r\n        if ( model == null || model.IsError )\r\n        {\r\n            error = $\u0022model \u0027{info.Model}\u0027 could not be loaded\u0022;\r\n            return false;\r\n        }\r\n        if ( !model.AnimationNames.Contains( sequence ) )\r\n        {\r\n            error = $\u0022model \u0027{model.Name}\u0027 has no sequence \u0027{sequence}\u0027\u0022;\r\n            return false;\r\n        }\r\n\r\n        if ( _model == null || !_model.IsValid() || _source != model )\r\n        {\r\n            Dispose();\r\n            _model = new SceneModel( body.SceneModel.World, model, body.SceneModel.Transform )\r\n            {\r\n                UseAnimGraph = false,\r\n                RenderingEnabled = false,\r\n            };\r\n            _source = model;\r\n            _boneIndex.Clear();\r\n            for ( int i = 0; i \u003C model.BoneCount; i\u002B\u002B )\r\n                _boneIndex[model.GetBoneName( i )] = i;\r\n        }\r\n\r\n        _sequence = sequence;\r\n        _model.CurrentSequence.Name = _sequence;\r\n        _model.Update( 0f );\r\n        Duration = _model.CurrentSequence.Duration;\r\n        Active = true;\r\n        return true;\r\n    }\r\n\r\n    /// \u003Csummary\u003EPoses the overlay at \u003Cparamref name=\u0022seconds\u0022/\u003E into the action with the given blend weight.\u003C/summary\u003E\r\n    public void Update( SkinnedModelRenderer body, float seconds, float weight )\r\n    {\r\n        if ( !Active || _model == null || !_model.IsValid() )\r\n            return;\r\n        _model.Transform = body.SceneModel.Transform;\r\n        _model.CurrentSequence.Name = _sequence;\r\n        _model.CurrentSequence.Time = Math.Clamp( seconds, 0f, MathF.Max( 0f, Duration ) );\r\n        _model.Update( 0f );\r\n        Weight = Math.Clamp( weight, 0f, 1f );\r\n        _offset = Vector3.Zero;\r\n        var anchor = body.Model?.Bones.GetBone( AnchorBone );\r\n        if ( anchor != null \u0026\u0026 _boneIndex.TryGetValue( AnchorBone, out var index ) \u0026\u0026 body.TryGetBoneTransformAnimation( anchor, out var own ) )\r\n            _offset = own.Position - _model.GetBoneWorldTransform( index ).Position;\r\n    }\r\n\r\n    /// \u003Csummary\u003EWorld transform of a bone in the overlay pose.\u003C/summary\u003E\r\n    public bool TryGetWorld( string bone, out Transform world )\r\n    {\r\n        world = global::Transform.Zero;\r\n        if ( !Active || _model == null || !_boneIndex.TryGetValue( bone, out var index ) )\r\n            return false;\r\n        world = _model.GetBoneWorldTransform( index );\r\n        world.Position \u002B= _offset;\r\n        return true;\r\n    }\r\n\r\n    public void End()\r\n    {\r\n        Active = false;\r\n        Weight = 0f;\r\n    }\r\n\r\n    public void Dispose()\r\n    {\r\n        if ( _model != null \u0026\u0026 _model.IsValid() )\r\n            _model.Delete();\r\n        _model = null;\r\n        _source = null;\r\n        Active = false;\r\n    }\r\n\r\n    /// \u003Csummary\u003EParses \u003Cc\u003E{\u0022reload\u0022:{\u0022model\u0022:\u0022\u0022,\u0022sequence\u0022:\u0022reload_rifle\u0022,\u0022blend\u0022:0.2,\u0022variants\u0022:[\u0022reload_2\u0022]}}\u003C/c\u003E.\u003C/summary\u003E\r\n    public static bool TryParse( string json, Dictionary\u003Cstring, OverlayInfo\u003E into, out string error )\r\n    {\r\n        into.Clear();\r\n        error = null;\r\n        if ( string.IsNullOrWhiteSpace( json ) )\r\n            return true;\r\n        try\r\n        {\r\n            using var doc = JsonDocument.Parse( json );\r\n            if ( doc.RootElement.ValueKind != JsonValueKind.Object )\r\n            {\r\n                error = \u0022expected an object\u0022;\r\n                return false;\r\n            }\r\n            foreach ( var entry in doc.RootElement.EnumerateObject() )\r\n            {\r\n                if ( entry.Value.ValueKind != JsonValueKind.Object )\r\n                    continue;\r\n                var info = new OverlayInfo { Role = entry.Name.ToLowerInvariant() };\r\n                if ( entry.Value.TryGetProperty( \u0022model\u0022, out var m ) \u0026\u0026 m.ValueKind == JsonValueKind.String )\r\n                    info.Model = m.GetString();\r\n                if ( entry.Value.TryGetProperty( \u0022sequence\u0022, out var s ) \u0026\u0026 s.ValueKind == JsonValueKind.String )\r\n                    info.Sequence = s.GetString();\r\n                if ( entry.Value.TryGetProperty( \u0022blend\u0022, out var b ) \u0026\u0026 b.ValueKind == JsonValueKind.Number )\r\n                    info.Blend = MathF.Max( 0f, b.GetSingle() );\r\n                if ( entry.Value.TryGetProperty( \u0022variants\u0022, out var v ) \u0026\u0026 v.ValueKind == JsonValueKind.Array )\r\n                    foreach ( var item in v.EnumerateArray() )\r\n                        if ( item.ValueKind == JsonValueKind.String \u0026\u0026 !string.IsNullOrEmpty( item.GetString() ) )\r\n                            info.Variants.Add( item.GetString() );\r\n                if ( !string.IsNullOrEmpty( info.Sequence ) )\r\n                    into[info.Role] = info;\r\n            }\r\n            return true;\r\n        }\r\n        catch ( JsonException e )\r\n        {\r\n            error = e.Message;\r\n            return false;\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Core/Generation/ExportRig.cs","FileName":"ExportRig.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Numerics;\r\nusing WeaponImporter.Core.Analysis;\r\nusing WeaponImporter.Core.Maths;\r\nusing WeaponImporter.Core.Rig;\r\nusing WeaponImporter.Core.Weapon;\r\n\r\nnamespace WeaponImporter.Core.Generation;\r\n\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003E\r\n/// The rig a world model ships with: the weapon body becomes the fixed root, only bones the\r\n/// weapon uses are kept (arms, cameras and scene helpers are dropped) and parts parented\r\n/// elsewhere (a magazine hanging off a hand or the scene root) are re-parented under the body\r\n/// with their motion relative to it preserved. Root motion of the body is removed, so a\r\n/// first-person sway never moves the weapon in third person.\r\n/// \u003C/summary\u003E\r\npublic sealed class ExportRig\r\n{\r\n    public required WeaponAsset Asset { get; init; }\r\n    public required int[] Triangles { get; init; }\r\n    public required string RootName { get; init; }\r\n\r\n    /// \u003Csummary\u003EOld bone index -\u003E new bone index (-1 when dropped).\u003C/summary\u003E\r\n    public required int[] Map { get; init; }\r\n\r\n    public static ExportRig Build(WeaponAnalysis a, IEnumerable\u003Cstring\u003E? keepNames = null)\r\n    {\r\n        var source = a.Asset;\r\n        var skeleton = source.Skeleton;\r\n        var mesh = source.Mesh;\r\n\r\n        // Body: the bone most weapon triangles are skinned to.\r\n        var counts = new Dictionary\u003Cint, int\u003E();\r\n        foreach (var t in a.WeaponTriangles)\r\n        {\r\n            var b = mesh.TriangleBone(t);\r\n            if (b \u003E= 0)\r\n                counts[b] = counts.GetValueOrDefault(b) \u002B 1;\r\n        }\r\n        var body = counts.Count \u003E 0 ? counts.OrderByDescending(kv =\u003E kv.Value).First().Key : a.RootBone;\r\n\r\n        // Bones to keep: skinned weapon bones, named/moving parts, explicitly requested ones.\r\n        var keep = new HashSet\u003Cint\u003E { body };\r\n        foreach (var b in counts.Keys)\r\n            keep.Add(b);\r\n        foreach (var p in a.Parts)\r\n            if (p.Bone.Length \u003E 0 \u0026\u0026 skeleton.IndexOf(p.Bone) is var pi \u0026\u0026 pi \u003E= 0)\r\n                keep.Add(pi);\r\n        // The left-hand copy of a dual weapon: its bones keep their own hierarchy under its root.\r\n        var second = a.SecondWeaponBone is { } secondName ? skeleton.IndexOf(secondName) : -1;\r\n        if (second \u003E= 0)\r\n        {\r\n            keep.Add(second);\r\n            foreach (var t in a.SecondWeaponTriangles)\r\n                if (mesh.TriangleBone(t) is var sb \u0026\u0026 sb \u003E= 0)\r\n                    keep.Add(sb);\r\n        }\r\n        bool Under(int bone) =\u003E bone == body || IsDescendant(skeleton, bone, body) || (second \u003E= 0 \u0026\u0026 (bone == second || IsDescendant(skeleton, bone, second)));\r\n        if (keepNames is not null)\r\n            foreach (var n in keepNames)\r\n                if (!string.IsNullOrEmpty(n) \u0026\u0026 skeleton.IndexOf(n) is var ki \u0026\u0026 ki \u003E= 0 \u0026\u0026 !a.ArmBones.Contains(ki))\r\n                    keep.Add(ki);\r\n        // Close over ancestors that sit between the body and a kept bone (keeps hierarchies intact).\r\n        foreach (var b in keep.ToList())\r\n            for (var p = skeleton[b].ParentIndex; p \u003E= 0 \u0026\u0026 p != body \u0026\u0026 p != second; p = skeleton[p].ParentIndex)\r\n            {\r\n                if (!Under(p))\r\n                    break;\r\n                keep.Add(p);\r\n            }\r\n\r\n        // New parent of each kept bone: nearest kept ancestor under the body, else the body.\r\n        int NewParent(int b)\r\n        {\r\n            if (b == body)\r\n                return -1;\r\n            if (b == second)\r\n                return body;\r\n            for (var p = skeleton[b].ParentIndex; p \u003E= 0; p = skeleton[p].ParentIndex)\r\n                if (keep.Contains(p) \u0026\u0026 Under(p))\r\n                    return p;\r\n            return body;\r\n        }\r\n\r\n        // Order: body first, then topological by new parent (stable, source order).\r\n        var order = new List\u003Cint\u003E { body };\r\n        var placed = new HashSet\u003Cint\u003E { body };\r\n        var remaining = skeleton.Bones.Select(x =\u003E x.Index).Where(i =\u003E keep.Contains(i) \u0026\u0026 i != body).ToList();\r\n        while (remaining.Count \u003E 0)\r\n        {\r\n            var progress = false;\r\n            foreach (var b in remaining.ToList())\r\n                if (placed.Contains(NewParent(b)))\r\n                {\r\n                    order.Add(b);\r\n                    placed.Add(b);\r\n                    remaining.Remove(b);\r\n                    progress = true;\r\n                }\r\n            if (!progress)\r\n                break;\r\n        }\r\n\r\n        var rest = skeleton.RestWorld;\r\n        var names = new HashSet\u003Cstring\u003E(StringComparer.Ordinal);\r\n        var defs = new List\u003CBoneDefinition\u003E();\r\n        foreach (var b in order)\r\n        {\r\n            var parent = NewParent(b);\r\n            var local = parent \u003C 0 ? rest[b] : XForm.ToLocal(rest[parent], rest[b]);\r\n            defs.Add(new BoneDefinition(skeleton[b].Name, parent \u003C 0 ? null : skeleton[parent].Name, local));\r\n            names.Add(skeleton[b].Name);\r\n        }\r\n        var exportSkeleton = Skeleton.Create(defs);\r\n        var map = new int[skeleton.Count];\r\n        for (var i = 0; i \u003C map.Length; i\u002B\u002B)\r\n            map[i] = keep.Contains(i) ? exportSkeleton.IndexOf(skeleton[i].Name) : -1;\r\n\r\n        // Vertices skinned to dropped bones fall back to their nearest kept ancestor (or the body).\r\n        int MapBone(int b)\r\n        {\r\n            for (var x = b; x \u003E= 0; x = skeleton[x].ParentIndex)\r\n                if (map[x] \u003E= 0)\r\n                    return map[x];\r\n            return map[body];\r\n        }\r\n        var vertexBone = mesh.VertexBone.Select(b =\u003E b \u003E= 0 ? MapBone(b) : map[body]).ToArray();\r\n        var exportMesh = mesh.WithVertices(mesh.Positions, vertexBone, _ =\u003E Quaternion.Identity);\r\n\r\n        // Clips: re-express every kept bone relative to its new parent, frame by frame.\r\n        var clips = new List\u003CClip\u003E();\r\n        foreach (var clip in source.Clips)\r\n        {\r\n            var frames = new List\u003CXForm[]\u003E(clip.FrameCount);\r\n            foreach (var locals in clip.Frames)\r\n            {\r\n                var world = new XForm[skeleton.Count];\r\n                for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n                {\r\n                    var p = skeleton[i].ParentIndex;\r\n                    world[i] = p \u003C 0 ? locals[i] : XForm.Compose(world[p], locals[i]);\r\n                }\r\n                // Body stays at its rest placement; everything else keeps its motion relative to it.\r\n                var bodyWorld = world[body];\r\n                var toRest = XForm.Compose(rest[body], bodyWorld.Inverse());\r\n                var outLocals = new XForm[exportSkeleton.Count];\r\n                foreach (var b in order)\r\n                {\r\n                    var nb = map[b];\r\n                    var parent = NewParent(b);\r\n                    if (parent \u003C 0)\r\n                        outLocals[nb] = rest[body];\r\n                    else\r\n                        outLocals[nb] = XForm.ToLocal(XForm.Compose(toRest, world[parent]), XForm.Compose(toRest, world[b]));\r\n                }\r\n                frames.Add(outLocals);\r\n            }\r\n            clips.Add(new Clip(clip.Name, clip.Fps, clip.Looping, frames, clip.NativeFps));\r\n        }\r\n\r\n        var asset = new WeaponAsset\r\n        {\r\n            Name = source.Name,\r\n            Kind = source.Kind,\r\n            SourcePath = source.SourcePath,\r\n            Skeleton = exportSkeleton,\r\n            Mesh = exportMesh,\r\n            Clips = clips,\r\n            Attachments = source.Attachments,\r\n        };\r\n        return new ExportRig { Asset = asset, Triangles = a.WeaponTriangles.Concat(a.SecondWeaponTriangles).ToArray(), RootName = skeleton[body].Name, Map = map };\r\n    }\r\n\r\n    private static bool IsDescendant(Skeleton skeleton, int bone, int ancestor)\r\n    {\r\n        for (var p = skeleton[bone].ParentIndex; p \u003E= 0; p = skeleton[p].ParentIndex)\r\n            if (p == ancestor)\r\n                return true;\r\n        return false;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_weapon_importer","Path":"Editor/WeaponImporter/Core/Hands/HandRig.cs","FileName":"HandRig.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":397047,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Numerics;\r\nusing WeaponImporter.Core.Maths;\r\nusing WeaponImporter.Core.Rig;\r\n\r\nnamespace WeaponImporter.Core.Hands;\r\n\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\npublic enum Side { Right, Left }\r\n\r\npublic enum FingerKind { Thumb, Index, Middle, Ring, Pinky }\r\n\r\n/// \u003Csummary\u003EOne finger: its joint bones from the knuckle outward, with flex axes.\u003C/summary\u003E\r\npublic sealed class FingerRig\r\n{\r\n    public required FingerKind Kind { get; init; }\r\n\r\n    /// \u003Csummary\u003EJoint bones, proximal first (metacarpals are not included).\u003C/summary\u003E\r\n    public required int[] Joints { get; init; }\r\n\r\n    /// \u003Csummary\u003ESegment lengths, one per joint (the last one is estimated to the fingertip).\u003C/summary\u003E\r\n    public required float[] Lengths { get; init; }\r\n\r\n    /// \u003Csummary\u003EFlexion axis of each joint in that joint\u0027s own rest-local frame.\u003C/summary\u003E\r\n    public required Vector3[] FlexAxes { get; init; }\r\n\r\n    /// \u003Csummary\u003ESpread (abduction) axis of the base joint, local frame.\u003C/summary\u003E\r\n    public required Vector3 SpreadAxis { get; init; }\r\n\r\n    /// \u003Csummary\u003EFlexion limits per joint (radians).\u003C/summary\u003E\r\n    public required float[] MaxFlex { get; init; }\r\n\r\n    /// \u003Csummary\u003EFinger radius used for contact tests.\u003C/summary\u003E\r\n    public required float Radius { get; init; }\r\n\r\n    /// \u003Csummary\u003ELocal direction a joint points along (toward its child), rest frame.\u003C/summary\u003E\r\n    public required Vector3[] Along { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A character\u0027s hand and arm, read from bone names and the rest pose. Everything the grasp\r\n/// solver needs is derived here once: palm frame, finger chains, flex axes, sizes.\r\n/// \u003C/summary\u003E\r\npublic sealed class HandRig\r\n{\r\n    public required Side Side { get; init; }\r\n    public required Skeleton Skeleton { get; init; }\r\n    public required int Hand { get; init; }\r\n    public required int LowerArm { get; init; }\r\n    public required int UpperArm { get; init; }\r\n    public int Clavicle { get; init; } = -1;\r\n\r\n    /// \u003Csummary\u003ETwist/helper bones between shoulder and hand that just follow their parents.\u003C/summary\u003E\r\n    public IReadOnlyList\u003Cint\u003E Helpers { get; init; } = Array.Empty\u003Cint\u003E();\r\n\r\n    public required IReadOnlyList\u003CFingerRig\u003E Fingers { get; init; }\r\n\r\n    /// \u003Csummary\u003EMetacarpal bones (kept at rest).\u003C/summary\u003E\r\n    public IReadOnlyList\u003Cint\u003E Metacarpals { get; init; } = Array.Empty\u003Cint\u003E();\r\n\r\n    // ---- hand-local frame (relative to the hand bone\u0027s rest transform) ----\r\n    public required Vector3 PalmCenter { get; init; }\r\n    public required Vector3 PalmNormal { get; init; }   // out of the palm\r\n    public required Vector3 FingerDirection { get; init; } // wrist -\u003E knuckles\r\n    public required Vector3 KnuckleLine { get; init; }  // index -\u003E pinky side\r\n    public required Vector3 KnuckleCenter { get; init; }\r\n    public required float HandLength { get; init; }\r\n    public required float PalmWidth { get; init; }\r\n    public float PalmThickness =\u003E HandLength * 0.14f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The character\u0027s model copies the ring finger onto the pinky (a constraint, like the\r\n    /// human\u0027s \u0022CopyPinky\u0022), so the pinky can\u0027t be posed on its own: poses give it the ring\u0027s angles.\r\n    /// \u003C/summary\u003E\r\n    public bool PinkyFollowsRing { get; set; }\r\n\r\n    public FingerRig? Finger(FingerKind kind) =\u003E Fingers.FirstOrDefault(f =\u003E f.Kind == kind);\r\n\r\n    /// \u003Csummary\u003EAll bones this rig may rotate (arm chain and fingers).\u003C/summary\u003E\r\n    public IEnumerable\u003Cint\u003E Bones\r\n    {\r\n        get\r\n        {\r\n            if (Clavicle \u003E= 0) yield return Clavicle;\r\n            yield return UpperArm;\r\n            yield return LowerArm;\r\n            yield return Hand;\r\n            foreach (var h in Helpers) yield return h;\r\n            foreach (var f in Fingers)\r\n                foreach (var j in f.Joints)\r\n                    yield return j;\r\n        }\r\n    }\r\n\r\n    // ------------------------------------------------------------------ building\r\n\r\n    private static string SideSuffix(Side side) =\u003E side == Side.Right ? \u0022R\u0022 : \u0022L\u0022;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Finds a hand by the s\u0026amp;box naming scheme (\u003Cc\u003Ehand_R\u003C/c\u003E, \u003Cc\u003Earm_lower_R\u003C/c\u003E,\r\n    /// \u003Cc\u003Efinger_index_0_R\u003C/c\u003E...) with fallbacks for common rigs (Mixamo, UE, Rigify, Biped).\r\n    /// Returns null when the skeleton has no recognisable arm.\r\n    /// \u003C/summary\u003E\r\n    public static HandRig? Build(Skeleton skeleton, Side side)\r\n    {\r\n        var s = SideSuffix(side);\r\n        int Find(params string[] names)\r\n        {\r\n            foreach (var n in names)\r\n            {\r\n                var i = skeleton.IndexOf(n);\r\n                if (i \u003E= 0)\r\n                    return i;\r\n            }\r\n            // Case-insensitive fallback.\r\n            foreach (var n in names)\r\n                for (var b = 0; b \u003C skeleton.Count; b\u002B\u002B)\r\n                    if (string.Equals(skeleton[b].Name, n, StringComparison.OrdinalIgnoreCase))\r\n                        return b;\r\n            return -1;\r\n        }\r\n        var lower = s.ToLowerInvariant();\r\n        var full = side == Side.Right ? \u0022Right\u0022 : \u0022Left\u0022;\r\n        var hand = Find($\u0022hand_{s}\u0022, $\u0022Hand_{s}\u0022, $\u0022hand.{s}\u0022, $\u0022hand_{lower}\u0022, $\u0022{full}Hand\u0022, $\u0022mixamorig:{full}Hand\u0022, $\u0022hand_{lower}\u0022, $\u0022DEF-hand.{s}\u0022, $\u0022Bip01 {s} Hand\u0022);\r\n        if (hand \u003C 0)\r\n            hand = FindHandByTokens(skeleton, side);\r\n        if (hand \u003C 0)\r\n            return null;\r\n        var lowerArm = skeleton[hand].ParentIndex;\r\n        // Skip twist helpers between hand and forearm.\r\n        while (lowerArm \u003E= 0 \u0026\u0026 IsHelper(skeleton[lowerArm].Name))\r\n            lowerArm = skeleton[lowerArm].ParentIndex;\r\n        if (lowerArm \u003C 0)\r\n            return null;\r\n        var upperArm = skeleton[lowerArm].ParentIndex;\r\n        while (upperArm \u003E= 0 \u0026\u0026 IsHelper(skeleton[upperArm].Name))\r\n            upperArm = skeleton[upperArm].ParentIndex;\r\n        // Rigs whose hands hang off a detached IK forearm (parented to the root) keep the real\r\n        // arm chain elsewhere: the shoulder is then the side\u0027s named upper arm.\r\n        if (upperArm \u003C 0 || skeleton[upperArm].ParentIndex \u003C 0)\r\n        {\r\n            var named = Find($\u0022{full}Arm\u0022, $\u0022mixamorig:{full}Arm\u0022, $\u0022upperarm_{lower}\u0022, $\u0022UpperArm_{s}\u0022, $\u0022arm_upper_{s}\u0022, $\u0022upper_arm.{s}\u0022, $\u0022DEF-upper_arm.{s}\u0022, $\u0022Bip01 {s} UpperArm\u0022);\r\n            if (named \u003C 0)\r\n                for (var b = 0; b \u003C skeleton.Count \u0026\u0026 named \u003C 0; b\u002B\u002B)\r\n                {\r\n                    var bare = skeleton[b].Name;\r\n                    var colon = bare.LastIndexOf(\u0027:\u0027);\r\n                    if (colon \u003E= 0 \u0026\u0026 string.Equals(bare[(colon \u002B 1)..], $\u0022{full}Arm\u0022, StringComparison.OrdinalIgnoreCase))\r\n                        named = b;\r\n                }\r\n            if (named \u003E= 0)\r\n                upperArm = named;\r\n        }\r\n        if (upperArm \u003C 0)\r\n            return null;\r\n        var clavicle = skeleton[upperArm].ParentIndex;\r\n\r\n        var helpers = new List\u003Cint\u003E();\r\n        for (var b = 0; b \u003C skeleton.Count; b\u002B\u002B)\r\n        {\r\n            var p = skeleton[b].ParentIndex;\r\n            if ((p == upperArm || p == lowerArm) \u0026\u0026 IsHelper(skeleton[b].Name))\r\n                helpers.Add(b);\r\n        }\r\n\r\n        var fingers = new List\u003CFingerRig\u003E();\r\n        var metas = new List\u003Cint\u003E();\r\n        var restWorld = skeleton.RestWorld;\r\n        var handRest = restWorld[hand];\r\n\r\n        List\u003Cint\u003E Chain(string finger)\r\n        {\r\n            var chain = new List\u003Cint\u003E();\r\n            for (var k = 0; k \u003C 4; k\u002B\u002B)\r\n            {\r\n                // Deform bones before same-named controls (Rigify has both).\r\n                var b = Find($\u0022finger_{finger}_{k}_{s}\u0022, $\u0022{finger}_{k}_{s}\u0022, $\u0022{full}Hand{Cap(finger)}{k \u002B 1}\u0022, $\u0022mixamorig:{full}Hand{Cap(finger)}{k \u002B 1}\u0022, $\u0022{finger}_{(k \u002B 1):00}_{lower}\u0022,\r\n                    $\u0022DEF-f_{finger}.{(k \u002B 1):00}.{s}\u0022, $\u0022DEF-{finger}.{(k \u002B 1):00}.{s}\u0022, $\u0022{finger}.{(k \u002B 1):00}.{s}\u0022, $\u0022f_{finger}.{(k \u002B 1):00}.{s}\u0022);\r\n                if (b \u003C 0)\r\n                    break;\r\n                chain.Add(b);\r\n            }\r\n            return chain;\r\n        }\r\n\r\n        var jointsByKind = new Dictionary\u003CFingerKind, List\u003Cint\u003E\u003E();\r\n        foreach (var kind in Enum.GetValues\u003CFingerKind\u003E())\r\n        {\r\n            var name = kind.ToString().ToLowerInvariant();\r\n            var chain = Chain(name);\r\n            if (chain.Count == 0 \u0026\u0026 kind == FingerKind.Pinky)\r\n                chain = Chain(\u0022little\u0022);\r\n            // Keep only the bones that descend from the hand.\r\n            chain = chain.Where(b =\u003E Descends(skeleton, b, hand)).Take(3).ToList();\r\n            if (chain.Count \u003E 0)\r\n                jointsByKind[kind] = chain;\r\n            var meta = Find($\u0022finger_{name}_meta_{s}\u0022);\r\n            if (meta \u003E= 0)\r\n                metas.Add(meta);\r\n        }\r\n        if (!jointsByKind.ContainsKey(FingerKind.Index) || !jointsByKind.ContainsKey(FingerKind.Middle))\r\n            jointsByKind = FingersByShape(skeleton, hand) ?? jointsByKind;\r\n        if (!jointsByKind.ContainsKey(FingerKind.Index) || !jointsByKind.ContainsKey(FingerKind.Middle))\r\n            return null;\r\n\r\n        Vector3 Local(int bone) =\u003E handRest.Inverse().TransformPoint(restWorld[bone].Pos);\r\n\r\n        var indexBase = Local(jointsByKind[FingerKind.Index][0]);\r\n        var middleBase = Local(jointsByKind[FingerKind.Middle][0]);\r\n        var outer = jointsByKind.TryGetValue(FingerKind.Pinky, out var pinky) ? pinky : jointsByKind.GetValueOrDefault(FingerKind.Ring) ?? jointsByKind[FingerKind.Middle];\r\n        var outerBase = Local(outer[0]);\r\n        var knuckles = new[] { FingerKind.Index, FingerKind.Middle, FingerKind.Ring, FingerKind.Pinky }\r\n            .Where(jointsByKind.ContainsKey).Select(k =\u003E Local(jointsByKind[k][0])).ToList();\r\n        var knuckleCenter = knuckles.Aggregate(Vector3.Zero, (a, v) =\u003E a \u002B v) / knuckles.Count;\r\n        var wrist = Vector3.Zero;\r\n        var fingerDir = SafeNormal(knuckleCenter - wrist, Vector3.UnitX);\r\n        var knuckleLine = outerBase - indexBase;\r\n        knuckleLine = SafeNormal(knuckleLine - fingerDir * Vector3.Dot(knuckleLine, fingerDir), MathQ.Perpendicular(fingerDir));\r\n\r\n        // Palm normal: perpendicular to fingers and knuckles; the sign comes from anatomy.\r\n        var normal = Vector3.Normalize(Vector3.Cross(fingerDir, knuckleLine));\r\n        var vote = 0f;\r\n        // Thumb sits on the palm side of the knuckle plane.\r\n        if (jointsByKind.TryGetValue(FingerKind.Thumb, out var thumb))\r\n        {\r\n            var thumbTip = Local(thumb[^1]);\r\n            vote \u002B= Vector3.Dot(thumbTip - wrist, normal) * 2f;\r\n        }\r\n        // Rest curl bends fingertips toward the palm.\r\n        foreach (var kind in new[] { FingerKind.Index, FingerKind.Middle, FingerKind.Ring })\r\n            if (jointsByKind.TryGetValue(kind, out var chain) \u0026\u0026 chain.Count \u003E= 2)\r\n                vote \u002B= Vector3.Dot(Local(chain[^1]) - Local(chain[0]), normal) - Vector3.Dot(Local(chain[1]) - Local(chain[0]), normal) * 0.5f;\r\n        // Handedness fallback: for a right hand, fingers x knuckles(index-\u003Epinky) points out of\r\n        // the back of the hand in a right-handed frame; mirrored for the left.\r\n        if (MathF.Abs(vote) \u003C 1e-3f)\r\n            vote = side == Side.Right ? -1f : 1f;\r\n        if (vote \u003C 0f)\r\n            normal = -normal;\r\n\r\n        var middleChain = jointsByKind[FingerKind.Middle];\r\n        var middleTip = Local(middleChain[^1]);\r\n        var lastSeg = middleChain.Count \u003E= 2 ? Vector3.Distance(Local(middleChain[^1]), Local(middleChain[^2])) : 1f;\r\n        var handLength = Vector3.Distance(wrist, middleTip) \u002B lastSeg * 0.8f;\r\n        var palmWidth = MathF.Max(1e-3f, Vector3.Distance(indexBase, outerBase)) * 1.25f;\r\n        var palmCenter = Vector3.Lerp(wrist, knuckleCenter, 0.55f) \u002B normal * 0.0f;\r\n\r\n        foreach (var (kind, chain) in jointsByKind)\r\n        {\r\n            var lengths = new float[chain.Count];\r\n            var axes = new Vector3[chain.Count];\r\n            var along = new Vector3[chain.Count];\r\n            for (var j = 0; j \u003C chain.Count; j\u002B\u002B)\r\n            {\r\n                var head = Local(chain[j]);\r\n                var tailWorld = j \u002B 1 \u003C chain.Count ? Local(chain[j \u002B 1]) : head \u002B (head - Local(j \u003E 0 ? chain[j - 1] : hand)) * 0.8f;\r\n                var dir = SafeNormal(tailWorld - head, fingerDir);\r\n                lengths[j] = MathF.Max(0.05f, Vector3.Distance(head, tailWorld));\r\n                // Flexion bends the segment toward the palm: axis = dir x normal (right-hand rule\r\n                // turns dir toward -normal... pick the sign that moves the tip toward \u002Bnormal).\r\n                var axisHand = SafeNormal(Vector3.Cross(dir, normal), knuckleLine);\r\n                if (kind == FingerKind.Thumb)\r\n                {\r\n                    // Thumb flexes across the palm toward the little finger.\r\n                    axisHand = SafeNormal(Vector3.Cross(dir, SafeNormal(knuckleCenter \u002B knuckleLine * palmWidth * 0.5f - head, normal)), axisHand);\r\n                }\r\n                // Check sign: rotating dir about the axis must move it toward the palm (or across it).\r\n                var probeTarget = kind == FingerKind.Thumb ? SafeNormal(knuckleCenter \u002B knuckleLine * palmWidth * 0.5f - head, normal) : normal;\r\n                var rotated = Vector3.Transform(dir, Quaternion.CreateFromAxisAngle(axisHand, 0.3f));\r\n                if (Vector3.Dot(rotated - dir, probeTarget) \u003C 0f)\r\n                    axisHand = -axisHand;\r\n                // Into the joint\u0027s own rest frame.\r\n                var jointRestInHand = XForm.Compose(handRest.Inverse(), restWorld[chain[j]]);\r\n                axes[j] = Vector3.Normalize(Vector3.Transform(axisHand, Quaternion.Conjugate(jointRestInHand.Rot)));\r\n                along[j] = Vector3.Normalize(Vector3.Transform(dir, Quaternion.Conjugate(jointRestInHand.Rot)));\r\n            }\r\n            var baseRest = XForm.Compose(handRest.Inverse(), restWorld[chain[0]]);\r\n            var spreadHand = normal;\r\n            var spreadLocal = Vector3.Normalize(Vector3.Transform(spreadHand, Quaternion.Conjugate(baseRest.Rot)));\r\n            var maxFlex = kind == FingerKind.Thumb\r\n                ? new[] { 0.9f, 1.0f, 1.3f }\r\n                : new[] { 1.55f, 1.9f, 1.4f };\r\n            fingers.Add(new FingerRig\r\n            {\r\n                Kind = kind,\r\n                Joints = chain.ToArray(),\r\n                Lengths = lengths,\r\n                FlexAxes = axes,\r\n                SpreadAxis = spreadLocal,\r\n                MaxFlex = maxFlex.Take(chain.Count).ToArray(),\r\n                Radius = handLength * (kind == FingerKind.Thumb ? 0.058f : 0.05f),\r\n                Along = along,\r\n            });\r\n        }\r\n\r\n        return new HandRig\r\n        {\r\n            Side = side,\r\n            Skeleton = skeleton,\r\n            Hand = hand,\r\n            LowerArm = lowerArm,\r\n            UpperArm = upperArm,\r\n            Clavicle = clavicle,\r\n            Helpers = helpers,\r\n            Fingers = fingers.OrderBy(f =\u003E f.Kind).ToList(),\r\n            Metacarpals = metas,\r\n            PalmCenter = palmCenter,\r\n            PalmNormal = normal,\r\n            FingerDirection = fingerDir,\r\n            KnuckleLine = knuckleLine,\r\n            KnuckleCenter = knuckleCenter,\r\n            HandLength = handLength,\r\n            PalmWidth = palmWidth,\r\n        };\r\n    }\r\n\r\n    private static string Cap(string s) =\u003E char.ToUpperInvariant(s[0]) \u002B s[1..];\r\n\r\n    // ------------------------------------------------------------------ rigs without known names\r\n\r\n    /// \u003Csummary\u003EControl/mechanism bones that are never the deforming hand or finger.\u003C/summary\u003E\r\n    private static readonly HashSet\u003Cstring\u003E ControlTokens = new(StringComparer.OrdinalIgnoreCase)\r\n    {\r\n        \u0022ik\u0022, \u0022ctrl\u0022, \u0022cntrl\u0022, \u0022control\u0022, \u0022mch\u0022, \u0022org\u0022, \u0022drv\u0022, \u0022master\u0022, \u0022tweak\u0022, \u0022vis\u0022, \u0022pole\u0022, \u0022target\u0022, \u0022end\u0022, \u0022helper\u0022, \u0022twist\u0022, \u0022hold\u0022, \u0022attach\u0022, \u0022widget\u0022, \u0022parent\u0022, \u0022socket\u0022, \u0022nub\u0022,\r\n    };\r\n\r\n    /// \u003Csummary\u003ELowercase name tokens: \u0022IK_Hand_Cntrl_L_015\u0022 \u2192 ik, hand, cntrl, l, 015.\u003C/summary\u003E\r\n    public static List\u003Cstring\u003E Tokens(string name)\r\n    {\r\n        var tokens = new List\u003Cstring\u003E();\r\n        var current = new System.Text.StringBuilder();\r\n        void Flush()\r\n        {\r\n            if (current.Length \u003E 0)\r\n                tokens.Add(current.ToString().ToLowerInvariant());\r\n            current.Clear();\r\n        }\r\n        for (var i = 0; i \u003C name.Length; i\u002B\u002B)\r\n        {\r\n            var c = name[i];\r\n            if (!char.IsLetterOrDigit(c))\r\n            {\r\n                Flush();\r\n                continue;\r\n            }\r\n            // Camel case and letter/digit boundaries split words.\r\n            if (current.Length \u003E 0 \u0026\u0026 ((char.IsUpper(c) \u0026\u0026 char.IsLower(current[^1])) || char.IsDigit(c) != char.IsDigit(current[^1])))\r\n                Flush();\r\n            current.Append(c);\r\n        }\r\n        Flush();\r\n        return tokens;\r\n    }\r\n\r\n    private static bool IsSide(List\u003Cstring\u003E tokens, Side side)\r\n        =\u003E side == Side.Right ? tokens.Any(t =\u003E t is \u0022r\u0022 or \u0022right\u0022 or \u0022rt\u0022) : tokens.Any(t =\u003E t is \u0022l\u0022 or \u0022left\u0022 or \u0022lt\u0022);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The deforming hand of a side on rigs with other conventions: a bone named hand/wrist with a\r\n    /// side marker, not a control; deform (\u0022DEF\u0022) bones and bones with many descendants win.\r\n    /// \u003C/summary\u003E\r\n    private static int FindHandByTokens(Skeleton skeleton, Side side)\r\n    {\r\n        var best = -1;\r\n        var bestScore = 0;\r\n        for (var b = 0; b \u003C skeleton.Count; b\u002B\u002B)\r\n        {\r\n            var tokens = Tokens(skeleton[b].Name);\r\n            if (!tokens.Any(t =\u003E t is \u0022hand\u0022 or \u0022wrist\u0022) || !IsSide(tokens, side) || tokens.Any(ControlTokens.Contains))\r\n                continue;\r\n            var descendants = 0;\r\n            for (var c = 0; c \u003C skeleton.Count; c\u002B\u002B)\r\n                if (Descends(skeleton, c, b))\r\n                    descendants\u002B\u002B;\r\n            if (descendants \u003C 6)\r\n                continue;\r\n            var score = descendants \u002B (tokens.Contains(\u0022def\u0022) ? 1000 : 0);\r\n            if (score \u003E bestScore)\r\n            {\r\n                bestScore = score;\r\n                best = b;\r\n            }\r\n        }\r\n        return best;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Finger chains of a hand told apart by shape when their names say nothing (\u0022Bone_R.004\u0022):\r\n    /// each chain is followed from the hand (through palm bones) up to three joints; the thumb is\r\n    /// the chain whose direction differs most from the others, and the fingers are ordered by\r\n    /// their distance from the thumb (index nearest, pinky farthest).\r\n    /// \u003C/summary\u003E\r\n    private static Dictionary\u003CFingerKind, List\u003Cint\u003E\u003E? FingersByShape(Skeleton skeleton, int hand)\r\n    {\r\n        bool Usable(int b)\r\n        {\r\n            var tokens = Tokens(skeleton[b].Name);\r\n            return !tokens.Any(t =\u003E t is \u0022end\u0022 or \u0022nub\u0022 or \u0022tip\u0022 or \u0022mch\u0022 or \u0022org\u0022 or \u0022ik\u0022 or \u0022ctrl\u0022 or \u0022cntrl\u0022 or \u0022drv\u0022 or \u0022master\u0022 or \u0022tweak\u0022 or \u0022vis\u0022 or \u0022helper\u0022 or \u0022twist\u0022);\r\n        }\r\n        List\u003Cint\u003E Children(int b) =\u003E Enumerable.Range(0, skeleton.Count).Where(c =\u003E skeleton[c].ParentIndex == b \u0026\u0026 Usable(c)).ToList();\r\n\r\n        var roots = new List\u003Cint\u003E();\r\n        void Collect(int b, int depth)\r\n        {\r\n            foreach (var c in Children(b))\r\n            {\r\n                var kids = Children(c);\r\n                // A palm/metacarpal bone that fans out: its children are the fingers.\r\n                if (kids.Count \u003E= 2 \u0026\u0026 depth == 0)\r\n                    Collect(c, depth \u002B 1);\r\n                else if (kids.Count \u003E= 1)\r\n                    roots.Add(c);\r\n            }\r\n        }\r\n        Collect(hand, 0);\r\n        if (roots.Count \u003C 3)\r\n            return null;\r\n\r\n        var raw = roots.Select(r =\u003E\r\n        {\r\n            var chain = new List\u003Cint\u003E { r };\r\n            while (chain.Count \u003C 5 \u0026\u0026 Children(chain[^1]) is { Count: \u003E 0 } kids)\r\n                chain.Add(kids[0]);\r\n            return chain;\r\n        }).Where(c =\u003E c.Count \u003E= 2).ToList();\r\n        if (raw.Count \u003C 3)\r\n            return null;\r\n        // Four joints: the first is the metacarpal inside the palm, not a finger joint.\r\n        var chains = raw.Select(c =\u003E (c.Count \u003E= 4 ? c.Skip(1) : c).Take(3).ToList()).ToList();\r\n\r\n        var rest = skeleton.RestWorld;\r\n        Vector3 Dir(List\u003Cint\u003E c) =\u003E SafeNormal(rest[c[^1]].Pos - rest[c[0]].Pos, Vector3.UnitX);\r\n        // The thumb has one bone fewer than the fingers on most rigs; otherwise its base sits\r\n        // nearest the wrist and it points away from the others.\r\n        var counts = raw.Select(c =\u003E c.Count).ToList();\r\n        var shortest = counts.Min();\r\n        List\u003Cint\u003E thumb;\r\n        if (counts.Count(c =\u003E c == shortest) == 1 \u0026\u0026 counts.Count(c =\u003E c \u003E shortest) \u003E= 3)\r\n            thumb = chains[counts.IndexOf(shortest)];\r\n        else\r\n            thumb = chains.OrderBy(c =\u003E Vector3.Distance(rest[c[0]].Pos, rest[hand].Pos) - 0.3f * chains.Where(o =\u003E o != c).Sum(o =\u003E 1f - Vector3.Dot(Dir(c), Dir(o)))).First();\r\n        // Order along the knuckle line: from the knuckle nearest the thumb to the farthest one.\r\n        var others = chains.Where(c =\u003E c != thumb).ToList();\r\n        var near = others.OrderBy(c =\u003E Vector3.Distance(rest[c[0]].Pos, rest[thumb[0]].Pos)).First();\r\n        var far = others.OrderByDescending(c =\u003E Vector3.Distance(rest[c[0]].Pos, rest[thumb[0]].Pos)).First();\r\n        var line = rest[far[0]].Pos - rest[near[0]].Pos;\r\n        var fingers = others.OrderBy(c =\u003E Vector3.Dot(rest[c[0]].Pos - rest[near[0]].Pos, line)).ToList();\r\n        var result = new Dictionary\u003CFingerKind, List\u003Cint\u003E\u003E { [FingerKind.Thumb] = thumb };\r\n        var kinds = new[] { FingerKind.Index, FingerKind.Middle, FingerKind.Ring, FingerKind.Pinky };\r\n        for (var i = 0; i \u003C fingers.Count \u0026\u0026 i \u003C kinds.Length; i\u002B\u002B)\r\n            result[kinds[i]] = fingers[i];\r\n        return result;\r\n    }\r\n\r\n    private static bool IsHelper(string name)\r\n    {\r\n        var n = name.ToLowerInvariant();\r\n        return n.Contains(\u0022twist\u0022) || n.Contains(\u0022helper\u0022) || n.Contains(\u0022roll\u0022) || n.Contains(\u0022ikrule\u0022) || n.Contains(\u0022_ik\u0022) || n.Contains(\u0022ik_\u0022) || n.Contains(\u0022hold\u0022) || n.Contains(\u0022attach\u0022) || n.Contains(\u0022target\u0022);\r\n    }\r\n\r\n    private static bool Descends(Skeleton skeleton, int bone, int ancestor)\r\n    {\r\n        for (var b = skeleton[bone].ParentIndex; b \u003E= 0; b = skeleton[b].ParentIndex)\r\n            if (b == ancestor)\r\n                return true;\r\n        return false;\r\n    }\r\n\r\n    private static Vector3 SafeNormal(Vector3 v, Vector3 fallback) =\u003E v.LengthSquared() \u003E 1e-10f ? Vector3.Normalize(v) : fallback;\r\n\r\n    /// \u003Csummary\u003EPalm frame in hand-local space: X = fingers, Z = palm normal, Y completes it.\u003C/summary\u003E\r\n    public Quaternion PalmFrame =\u003E MathQ.FromAxes(FingerDirection, Vector3.Cross(PalmNormal, FingerDirection));\r\n}\r\n"}]}