PoseSnapshots.cs
using Sandbox;
using System;
namespace PaintballBaddies;

/// <summary>Editor-only visual check. During a Sausage Survival round it keeps
/// saving close-up pictures of the action to the game's data folder, so poses
/// and animations can be reviewed without a live view:
///   pose_front/side/back/three/left.png  an AI teammate with the pistols
///   act_*.png                            each sausage action when it happens
///                                        (carrying, throwing, headbutt, slip,
///                                        rage, dying, plates and eggs in flight)
/// Each picture is refreshed at most every few seconds. Never runs in a
/// published game.</summary>
public sealed class PoseSnapshots : Component
{
    [Property] public bool CaptureEnabled {get;set;}
    const float TeamEvery=10,ActionEvery=6;
    private float teamClock=4,scanClock;
    private int shot;
    private readonly Dictionary<string,float> lastAction=new();
    protected override void OnUpdate()
    {
        if(!Game.IsEditor || !CaptureEnabled)return;
        var s=MultiplayerSession.Find(Scene);
        if(s?.IsOverdrive!=true || !s.InRound)return;
        try
        {
            teamClock-=Time.Delta;
            if(teamClock<=0){teamClock=TeamEvery;Team();}
            scanClock-=Time.Delta;
            if(scanClock<=0){scanClock=.02f;Actions();}
        }
        catch(Exception e){Log.Warning($"POSE_SNAPSHOT {e.Message}");}
    }
    private void Team()
    {
        var mates=Scene.GetAllComponents<CitizenOpponentPresentation>().Where(p=>p.FootstepRenderer.IsValid()).ToArray();
        if(mates.Length==0)return;
        var body=mates[shot++%mates.Length].FootstepRenderer;
        var center=body.WorldPosition+Vector3.Up*50;var facing=Rotation.FromYaw(body.WorldRotation.Yaw());
        foreach(var (name,dir) in new[]{("front",facing.Forward),("side",facing.Right),("back",facing.Backward),("three",(facing.Forward+facing.Right).Normal),("left",facing.Left)})
            Render($"pose_{name}.png",center+dir*70+Vector3.Up*6,center);
        Log.Info($"POSE_SNAPSHOT saved to {FileSystem.Data.GetFullPath("/")}");
        // Grip check: how far each gun's grip is from its hand.
        var loadout=body.GameObject.Root.Components.GetInChildrenOrSelf<OverdriveLoadout>();
        if(loadout.IsValid() && body.TryGetBoneTransform("hand_R",out var hr) && body.TryGetBoneTransform("hand_L",out var hl))
        {
            string G(GameObject g,Transform h)=>g.IsValid() ? $"{(g.WorldTransform.PointToWorld(OverdriveLoadout.DualGrip)-h.Position).Length:0.0}(en={g.Enabled},parent={g.Parent?.Name})" : "none";
            DualPistols.Palm(body,"R",out var pr);
            Log.Info($"GRIP_CHECK right={G(loadout.Visual,hr)} left={G(loadout.LeftVisual,hl)} palmR-hand={(pr-hr.Position).Length:0.0} renderers={body.GameObject.Root.Components.GetAll<SkinnedModelRenderer>(FindMode.EverythingInSelfAndDescendants).Count()}");
        }
        // Stray check: loose egg or ball models that are not in flight or carried.
        foreach(var r in Scene.GetAllComponents<ModelRenderer>())
        {
            var path=r.Model?.ResourcePath ?? "";
            if(!(path.Contains("egg") || path.Contains("sphere")))continue;
            var root=r.GameObject.Root;
            if(root.Components.GetInChildrenOrSelf<SausageProjectile>().IsValid() || root.Components.GetInChildrenOrSelf<OverdriveRobotRig>().IsValid() || root.Components.GetInChildrenOrSelf<RagePuff>().IsValid())continue;
            Log.Info($"STRAY_MODEL {path} go={r.GameObject.Name} root={root.Name} at={r.WorldPosition} enabled={r.Enabled}");
        }
    }
    private bool Due(string action)
    {
        float now=Time.Now;
        if(lastAction.TryGetValue(action,out var t) && now-t<ActionEvery)return false;
        lastAction[action]=now;return true;
    }
    private void Actions()
    {
        foreach(var e in Scene.GetAllComponents<OverdriveEnemy>())
        {
            string kind=e.ThrowPlate ? "plate" : e.ThrowEgg ? "egg" : "knife";
            string action=
                e.Dead ? "dying" :
                e.SlipRemaining>0 ? "slip" :
                e.RangedWindup && e.Windup>.66f && e.Windup<.74f ? $"throw_{kind}_0_grab" :
                e.RangedWindup && e.Windup>.23f && e.Windup<.34f ? $"throw_{kind}_1_cocked" :
                e.RangedWindup && e.Windup>0 && e.Windup<.07f ? $"throw_{kind}_2_release" :
                !e.RangedWindup && e.ThrowRecovery>.3f && e.ThrowRecovery<.45f ? "throw_3_follow" :
                e.Windup>0 && !e.RangedWindup ? (e.Windup>.3f ? "headbutt_1_lean_back" : e.Windup<.08f ? "headbutt_2_impact" : null) :
                e.Enraged ? "rage" :
                e.PlatesCarried>0 ? "carry_plates" :
                e.EggsCarried>0 ? "carry_eggs" : null;
            if(action is null || !Due(action))continue;
            var c=e.WorldPosition+Vector3.Up*40;var f=Rotation.FromYaw(e.WorldRotation.Yaw());
            // Front-left three-quarter view (the carrying hand is the left).
            Render($"act_{action}.png",c+(f.Forward+f.Left*.8f).Normal*85+Vector3.Up*12,c);
        }
        foreach(var p in Scene.GetAllComponents<SausageProjectile>())
        {
            if(p.Resolved)continue;
            string action=p.Plate ? "flight_plate" : p.Egg ? "flight_egg" : "flight_knife";
            if(!Due(action))continue;
            var at=p.WorldPosition;
            Render($"act_{action}.png",at+Vector3.Left*45+Vector3.Up*18,at);
        }
    }
    private void Render(string file,Vector3 from,Vector3 at)
    {
        var go=new GameObject(true,"Pose snapshot camera"){NetworkMode=NetworkMode.Never,WorldPosition=from,WorldRotation=Rotation.LookAt(at-from)};
        try
        {
            var camera=go.Components.Create<CameraComponent>();camera.IsMainCamera=false;camera.FieldOfView=50;camera.ZNear=2;
            using var bitmap=new Bitmap(640,640);camera.RenderToBitmap(bitmap,false);
            var png=bitmap.ToPng();
            using var stream=FileSystem.Data.OpenWrite(file);stream.Write(png,0,png.Length);
        }
        finally{go.Destroy();}
    }
}