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();}
}
}