CitizenRiflePreview.cs
using Sandbox;
using Sandbox.Citizen;
namespace PaintballBaddies;
/// <summary>Opt-in native baseline for the built-in weapon animation graph.</summary>
public sealed class CitizenRiflePreview : Component
{
[Property] public string CaptureId { get; set; } = "";
[Property] public string ModelPath { get; set; } = "models/citizen/citizen.vmdl";
[Property] public float Speed { get; set; }
[Property] public float StopMovementAfter { get; set; }
[Property] public bool TranslatePreview { get; set; }
[Property] public float TurnSpeed { get; set; }
[Property] public bool InteractiveControls { get; set; }
[Property] public bool ExerciseInteractiveReload { get; set; }
private bool exercisedInteractiveReload;
[Property] public bool ExerciseInteractiveCover { get; set; }
[Property] public bool HighCoverExercise { get; set; }
[Property] public bool OppositeCoverEdge { get; set; }
[Property] public bool BlockCoverCorner { get; set; }
private GameObject cornerObstruction;
[Property] public bool ExerciseInteractiveVault { get; set; }
[Property] public bool UseVaultGraph { get; set; }
private float vaultBlend;
[Property] public string VaultSequence { get; set; } = "";
[Property] public string ReviewSequence { get; set; } = "";
[Property] public float ReviewSequencePhase { get; set; } = .5f;
[Property] public float OutSignalWeight { get; set; }
private bool citizenWasVaulting, vaultExerciseStarted;
private int vaultReportStage;
private int vaultMotionSamples, vaultSlabSamples;
private float vaultMinAnkleClearance = float.MaxValue, vaultMaxLocalFootStep, vaultStepTime, vaultStepDelta;
private Vector3 vaultPreviousLeft, vaultPreviousRight;
private bool vaultHasPreviousFeet;
private CoverController interactiveCover;
private GameObject coverFixture;
private int coverExerciseStage, coverReportStage;
private bool sheltered;
[Property] public bool ControllerMovement { get; set; }
[Property] public bool WeaponReloadClock { get; set; }
[Property] public float CancelReloadAtPhase { get; set; } = -1;
[Property] public bool CancelByEndRound { get; set; }
private int cancellationEvents;
private bool reloadCancelled;
private readonly CitizenReloadClock reloadClock = new();
private float cancelElapsed => reloadClock.CancellationElapsed;
private Transform cancelHand;
private Rotation cancelPodRotation;
private PaintballMarker reviewWeapon;
private bool weaponInputBefore, weaponReloadRequested;
private int ammoBeforeReload, reserveBeforeReload;
private bool shotAccepted;
private float shotMuzzleError=-1;
private int shotProjectiles;
private GameObject previousWeaponAnchor;
private PlayerController movementController;
private readonly List<ModelRenderer> hiddenRenderers = new();
[Property] public float DuckLevel { get; set; }
[Property] public bool ShowMarker { get; set; }
[Property] public Vector3 MarkerOffset { get; set; }
[Property] public Angles MarkerAngles { get; set; }
[Property] public bool SupportIk { get; set; }
[Property] public Vector3 SupportOffset { get; set; } = new( 4, 2, -2 );
[Property] public bool CloseGrip { get; set; }
[Property] public bool ClosePod { get; set; }
[Property] public float AimPitch { get; set; }
private bool aimReported;
private CitizenLocomotion locomotion;
[Property] public float ViewYaw { get; set; }
[Property] public CitizenAnimationHelper.HoldTypes WeaponHold { get; set; } = CitizenAnimationHelper.HoldTypes.Rifle;
[Property] public float HandPose { get; set; }
[Property] public float GripWeight { get; set; } = 1;
[Property] public bool PreviewReload { get; set; }
[Property] public bool RepeatReload { get; set; }
private int reloadCycle=1;
[Property] public bool PreviewHopperReload { get; set; }
private CitizenPodReload podReload;
private bool hopperReported;
private GameObject refillPod => podReload.refillPod;
private bool podHeld => podReload.podHeld;
private bool podReturned => podReload.podReturned;
private float pickupError => podReload.pickupError;
private float returnError => podReload.returnError;
private float minimumReturnGap => podReload.minimumReturnGap;
private float returnReachDistance => podReload.returnReachDistance;
private float returnArmLength => podReload.returnArmLength;
private float maximumPourError => podReload.maximumPourError;
private float totalPourError => podReload.totalPourError;
private int pourSamples => podReload.pourSamples;
private int pourAlignedSamples => podReload.pourAlignedSamples;
[Property] public bool SuppressReloadGesture { get; set; }
private bool reloadTriggered;
private bool reloadMotionReported;
private Vector3 previousHand;
private bool hasPreviousHand;
private float maxReleaseStep, maxReturnStep, maxActionStep;
private float maxReturnTime;
private int motionSamples;
private readonly Dictionary<string,float> baselineBoneHeights = new();
private readonly Dictionary<string,float> maximumBoneRise = new();
private float baselineHeadHeight;
private float maximumReloadHeadHeight = float.MinValue;
private float minimumReloadHeadHeight = float.MaxValue;
private CitizenMarkerGrip markerGrip;
private float currentGrip => markerGrip.Grip;
private float reloadPosture;
[Property] public bool ExportHandPose { get; set; }
[Property] public bool UseSampledGrip { get; set; }
public sealed class FingerSample
{
public string name { get; set; }
public string hand { get; set; }
public float[] position { get; set; }
public float[] rotation { get; set; }
}
public sealed class FingerTrack { public FingerSample[] bones { get; set; } }
private FingerTrack gripTrack;
private bool handPoseReported;
private bool gripReported;
[Property] public bool LibraryGloves { get; set; }
[Property] public bool SuitSecondaryMotion { get; set; }
private CitizenSuitFollowThrough suitMotion;
private bool suitMotionReported;
private bool suitSettledReported;
private float suitSettledPeak;
[Property] public string OutfitModel { get; set; } = "";
[Property] public string OutfitColorTexture { get; set; } = "";
[Property] public string BootsModel { get; set; } = "";
[Property] public string HelmetModel { get; set; } = "";
[Property] public string GloveModel { get; set; } = "models/citizen_clothes/gloves/rubber_gloves/models/rubber_gloves_black_m_human.vmdl";
private GameObject subject;
private GameObject marker;
private SkinnedModelRenderer body;
private SkinnedModelRenderer gloveRenderer;
private float reviewTime;
private float wallTime;
private float distanceTraveled;
private bool reported;
private bool reviewMenuClosed;
private GameObject supportTarget;
private Rotation supportRotation => markerGrip.SupportRotation;
private bool returnStarted => markerGrip.Returning;
private CitizenAnimationHelper animation;
protected override void OnStart()
{
if(InteractiveControls)
{
ControllerMovement=true;WeaponReloadClock=true;PreviewHopperReload=true;
ShowMarker=true;SupportIk=true;PreviewReload=false;RepeatReload=false;
}
var player = Scene.GetAllComponents<PlayerController>().First();
movementController=player;
interactiveCover=player.Components.Get<CoverController>();
if(WeaponReloadClock)
{
reviewWeapon=player.Components.Get<PaintballMarker>();
weaponInputBefore=reviewWeapon.AcceptInput;
reviewWeapon.AcceptInput=InteractiveControls;
if(InteractiveControls) reviewWeapon.ReloadStarted+=OnInteractiveReloadStarted;
reviewWeapon.ReloadCancelled+=OnWeaponReloadCancelled;
}
if(ControllerMovement)
{
if(!InteractiveControls)
{
player.WorldPosition=new Vector3(-950,-500,2);
player.EyeAngles=new Angles(0,90,0);
}
player.UseLookControls=InteractiveControls;
foreach(var renderer in Scene.GetAllComponents<ModelRenderer>().Where(x=>x.Enabled && (x==player.Renderer || x.GameObject.Name=="VX-9 training marker")))
{
renderer.Enabled=false;hiddenRenderers.Add(renderer);
}
}
player.UseCameraControls = InteractiveControls;
player.UseInputControls = InteractiveControls;
subject = new GameObject { Name = "Citizen rifle baseline" };
// Keep static garment reviews under the arena lighting, alongside motion reviews.
subject.WorldPosition = ControllerMovement ? player.WorldPosition : new Vector3(-950,-500,2);
subject.WorldRotation=Rotation.FromYaw(ControllerMovement ? player.EyeAngles.yaw : 90);
body = subject.Components.Create<SkinnedModelRenderer>();
body.Model = Model.Load( ModelPath );
body.UseAnimGraph = true;
if(UseSampledGrip) gripTrack=Json.Deserialize<FingerTrack>(FileSystem.Mounted.ReadAllText("animations/citizen_hand_grip_review.json"));
var equipment = subject.Components.Create<CharacterEquipment>();
equipment.Configure(body, OutfitModel, LibraryGloves ? GloveModel : "", BootsModel, HelmetModel);
if(SuitSecondaryMotion)
{
suitMotion=subject.Components.Create<CitizenSuitFollowThrough>();
suitMotion.Configure(body,equipment.Outfit);
}
gloveRenderer = equipment.Gloves;
if (equipment.Outfit.IsValid() && !string.IsNullOrWhiteSpace(OutfitColorTexture))
{
var material=Material.Load("models/citizen_clothes/shirt/jumpsuit/textures/blue_jumpsuit.vmat").CreateCopy();
var texture=Texture.Load(OutfitColorTexture);
var applied=material.Set("g_tColor",texture);
equipment.Outfit.MaterialOverride=material;
Log.Info($"CITIZEN_OUTFIT_COLOR applied={applied} valid={texture.IsValid} error={texture.IsError} size={texture.Width}x{texture.Height}");
}
locomotion = subject.Components.Create<CitizenLocomotion>();
locomotion.Configure(body, WeaponHold);
animation = locomotion.Animation;
markerGrip = subject.Components.Create<CitizenMarkerGrip>();
podReload = subject.Components.Create<CitizenPodReload>();
if ( ShowMarker )
{
marker = new GameObject( subject ) { Name = "VX-9 Citizen grip review" };
marker.Components.Create<ModelRenderer>().Model = Model.Load( "models/weapons/vx9_speed_feed/vx9_speed_feed.vmdl" );
if(WeaponReloadClock)
{
previousWeaponAnchor=reviewWeapon.PresentationAnchor;
reviewWeapon.PresentationAnchor=marker;
}
if (PreviewHopperReload)
{
podReload.CreateGear();
}
if ( SupportIk ) supportTarget = new GameObject( subject ) { Name = "Citizen support hand IK target" };
}
Log.Info( $"CITIZEN_RIFLE_PREVIEW valid={body.Model.IsValid()} graph={body.UseAnimGraph}" );
if (!string.IsNullOrWhiteSpace(ReviewSequence))
Log.Info("CITIZEN_SEQUENCE_REVIEW "+Json.Serialize(new {capture_id=CaptureId,sequence=ReviewSequence,available=body.Model.AnimationNames.Any(x=>string.Equals(x,ReviewSequence,System.StringComparison.OrdinalIgnoreCase)),phase=ReviewSequencePhase}));
}
protected override void OnUpdate()
{
if(!reviewMenuClosed && !string.IsNullOrWhiteSpace(ReviewSequence))
{
var controls=Scene.GetAllComponents<PaintballControls>().FirstOrDefault();
if(controls.IsValid()) { controls.StartPractice();reviewMenuClosed=true; }
}
// Also hide late-created production equipment during isolated sequence reviews.
if(ControllerMovement && !string.IsNullOrWhiteSpace(ReviewSequence))
foreach(var renderer in Scene.GetAllComponents<ModelRenderer>().Where(x=>x.Enabled && x.GameObject.Name=="VX-9 training marker"))
{ renderer.Enabled=false;if(!hiddenRenderers.Contains(renderer))hiddenRenderers.Add(renderer); }
if (!string.IsNullOrWhiteSpace(ReviewSequence))
{
body.UseAnimGraph=false;
if(body.Sequence.Name!=ReviewSequence) body.Sequence.Name=ReviewSequence;
body.Sequence.Looping=false;body.PlaybackRate=0;
body.Sequence.TimeNormalized=ReviewSequencePhase.Clamp(0,1);
if(marker.IsValid())markerGrip.Attach(body,marker,MarkerOffset,MarkerAngles);
var sequenceCamera=Components.Get<CameraComponent>();
var focus=subject.WorldPosition+Vector3.Up*35;
sequenceCamera.WorldPosition=focus+subject.WorldRotation*Rotation.FromYaw(ViewYaw)*new Vector3(145,-95,13);
sequenceCamera.WorldRotation=Rotation.LookAt(focus-sequenceCamera.WorldPosition);
sequenceCamera.FieldOfView=40;
return;
}
// The production marker may be created after this preview's OnStart.
if(ControllerMovement)
foreach(var renderer in Scene.GetAllComponents<ModelRenderer>().Where(x=>x.Enabled && x.GameObject.Name=="VX-9 training marker"))
{ renderer.Enabled=false;if(!hiddenRenderers.Contains(renderer))hiddenRenderers.Add(renderer); }
wallTime+=Time.Delta;
if(StopMovementAfter>0 && wallTime>=StopMovementAfter)Speed=0;
if(InteractiveControls && ExerciseInteractiveReload && !exercisedInteractiveReload && wallTime>.8f)
{
exercisedInteractiveReload=true;
shotAccepted=reviewWeapon.FireAt(marker.WorldPosition+marker.WorldRotation.Forward*1000);
shotMuzzleError=(reviewWeapon.Muzzle-marker.WorldTransform.PointToWorld(PaintballMarker.MuzzleOffset)).Length;
shotProjectiles=reviewWeapon.ActivePaintballs;
reviewWeapon.Reload();
}
if(RepeatReload && reviewTime>=5) ResetReloadCycle();
if(InteractiveControls)
{
if(ExerciseInteractiveCover) ExerciseCover();
sheltered=interactiveCover is { Attached:true, Peeking:false };
AimPitch=sheltered ? 0 : movementController.EyeAngles.pitch;
var nativeVault=movementController.Components.Get<VaultController>();
var vaulting=nativeVault?.IsVaulting==true;
if(UseVaultGraph)
{
locomotion.UpdateVault(vaulting, nativeVault?.Progress ?? 0, Time.Delta);
vaultBlend=locomotion.VaultWeight;
}
else if(!string.IsNullOrWhiteSpace(VaultSequence))
{
if(vaulting)
{
body.UseAnimGraph=false;
if(body.Sequence.Name!=VaultSequence) body.Sequence.Name=VaultSequence;
body.Sequence.Looping=false;body.PlaybackRate=0;
body.Sequence.TimeNormalized=nativeVault.Progress;
}
else if(citizenWasVaulting) { body.UseAnimGraph=true;body.PlaybackRate=1; }
}
else if(vaulting && !citizenWasVaulting) animation.TriggerJump();
citizenWasVaulting=vaulting;
DuckLevel=vaulting || movementController.IsDucking ? 1 : 0;
var facing=sheltered ? Rotation.LookAt(-interactiveCover.Normal) : Rotation.FromYaw(movementController.EyeAngles.yaw);
subject.WorldRotation=Rotation.Slerp(subject.WorldRotation,facing,1-System.MathF.Exp(-9*Time.Delta));
animation.IsWeaponLowered=sheltered;
}
if(WeaponReloadClock && PreviewReload)
{
if(!InteractiveControls && !weaponReloadRequested && reviewTime>=.8f)
{
shotAccepted=reviewWeapon.FireAt(marker.WorldPosition+marker.WorldRotation.Forward*1000);
shotMuzzleError=(reviewWeapon.Muzzle-marker.WorldTransform.PointToWorld(PaintballMarker.MuzzleOffset)).Length;
shotProjectiles=reviewWeapon.ActivePaintballs;
ammoBeforeReload=reviewWeapon.Ammo;
reserveBeforeReload=reviewWeapon.Reserve;
reviewWeapon.Reload();
weaponReloadRequested=true;
}
if(weaponReloadRequested && !reloadCancelled && CancelReloadAtPhase>=0
&& reviewWeapon.ReloadRemaining>0 && reviewWeapon.ReloadProgress>=CancelReloadAtPhase)
{
if(CancelByEndRound) reviewWeapon.EndRound();
else reviewWeapon.CancelReload();
}
reviewTime=reloadClock.Sample(reviewTime, weaponReloadRequested, reloadCancelled,
returnStarted, podHeld, reviewWeapon.ReloadRemaining, reviewWeapon.ReloadProgress, Time.Delta);
}
if(UseSampledGrip)body.ClearPhysicsBones();
if(UseSampledGrip && gloveRenderer.IsValid())gloveRenderer.ClearPhysicsBones();
if(ControllerMovement && !InteractiveControls)
{
movementController.UpdateDucking(DuckLevel>.5f);
movementController.WishVelocity=subject.WorldRotation.Forward*Speed;
}
bool releaseGrip = PreviewReload && reviewTime >= .8f && reviewTime < 3.2f;
var lookYaw=InteractiveControls && !sheltered ? Rotation.FromYaw(movementController.EyeAngles.yaw) : subject.WorldRotation;
var velocity = citizenWasVaulting ? (movementController.WorldPosition-subject.WorldPosition)/System.MathF.Max(Time.Delta,.001f)
: ControllerMovement ? movementController.Velocity : subject.WorldRotation.Forward*Speed;
locomotion.UpdateMotion(velocity,
InteractiveControls ? movementController.WishVelocity : subject.WorldRotation.Forward*Speed,
ControllerMovement ? movementController.IsOnGround && !citizenWasVaulting : true,
DuckLevel, TurnSpeed, lookYaw, releaseGrip ? 0 : AimPitch, sheltered, Time.Delta);
body.Set("holdtype_pose_hand", HandPose);
body.Set("pb_out_weight", OutSignalWeight);
var postureRecovery = ((reviewTime-3.2f)/.6f).Clamp(0,1);
postureRecovery = postureRecovery*postureRecovery*(3-2*postureRecovery);
var postureTarget = PreviewReload && reviewTime >= .8f ? DuckLevel.Clamp(0,1)*(1-postureRecovery) : 0;
reloadPosture += (postureTarget-reloadPosture) * (1-System.MathF.Exp(-24*Time.Delta));
body.Set("pb_reload_posture",reloadPosture);
markerGrip.UpdateGrip(body, GripWeight, releaseGrip, Time.Delta);
if (PreviewReload && !SuppressReloadGesture && !reloadCancelled && !reloadTriggered && reviewTime >= 1)
{
body.Set("b_reload", true);
reloadTriggered = true;
}
if ( markerGrip.Attach(body, marker, MarkerOffset, MarkerAngles) )
{
markerGrip.UpdateSupport(body, animation, marker, supportTarget, SupportOffset, releaseGrip,
PreviewReload && reviewTime >= 3.2f ? (reviewTime-3.2f)/.4f : -1);
podReload.Apply(body, animation, marker, supportTarget, supportRotation, currentGrip,
reviewTime, DuckLevel, releaseGrip, reloadCancelled, cancelElapsed, cancelHand, cancelPodRotation, Time.Delta);
if (PreviewHopperReload && releaseGrip && !hopperReported && reviewTime>2.2f
&& body.TryGetBoneTransform("hand_L",out var reloadHand))
{
hopperReported=true;
var handTarget=marker.WorldPosition+marker.WorldRotation*new Vector3(.27165f,1.4f,15.19449f);
var shoulderValid=body.TryGetBoneTransform("arm_upper_L",out var shoulder);
var elbowValid=body.TryGetBoneTransform("arm_lower_L",out var elbow);
var armLength=(shoulder.Position-elbow.Position).Length+(elbow.Position-reloadHand.Position).Length;
Log.Info("CITIZEN_HOPPER_FIT " + Json.Serialize(new { capture_id=CaptureId, time=reviewTime, bones_valid=shoulderValid && elbowValid, arm_length=armLength, shoulder_to_target=(shoulder.Position-handTarget).Length, target_error=(reloadHand.Position-handTarget).Length, hand_local=marker.WorldTransform.PointToLocal(reloadHand.Position), target_local=marker.WorldTransform.PointToLocal(handTarget) }));
}
reviewTime += Time.Delta;
if ( !reported && reviewTime > .5f && body.TryGetBoneTransform("hold_R", out var hold) )
{
reported = true;
Log.Info( $"CITIZEN_GRIP hold={hold.Position} rotation={hold.Rotation.Angles()}" );
foreach ( var name in new[] { "hand_R", "hand_L", "hold_L" } )
if ( body.TryGetBoneTransform( name, out var pose ) )
Log.Info( $"CITIZEN_GRIP {name} local={hold.PointToLocal(pose.Position)}" );
}
}
if(InteractiveControls)
{
var delta=movementController.WorldPosition-subject.WorldPosition;
subject.WorldPosition+=delta;
distanceTraveled+=delta.WithZ(0).Length;
return;
}
var camera = Components.Get<CameraComponent>();
var cameraFocus=subject.WorldPosition+new Vector3(0,0,35);
camera.WorldPosition = cameraFocus + subject.WorldRotation * Rotation.FromYaw(ViewYaw) * new Vector3(145,-95,13);
camera.WorldRotation = Rotation.LookAt( cameraFocus - camera.WorldPosition );
camera.FieldOfView = 40;
if ( CloseGrip && marker is not null )
{
var focus = ClosePod && refillPod is not null ? refillPod.WorldPosition+refillPod.WorldRotation.Up*4.2f : marker.WorldPosition;
camera.WorldPosition = focus + subject.WorldRotation * Rotation.FromYaw(ViewYaw) * new Vector3(32,-46,12);
camera.WorldRotation = Rotation.LookAt( focus - camera.WorldPosition );
camera.FieldOfView = 32;
}
if (TurnSpeed != 0)
{
var turn=Rotation.FromYaw(TurnSpeed*Time.Delta);
var offset=camera.WorldPosition-subject.WorldPosition;
subject.WorldRotation=turn*subject.WorldRotation;
camera.WorldPosition=subject.WorldPosition+turn*offset;
camera.WorldRotation=turn*camera.WorldRotation;
}
if (ControllerMovement)
{
var delta=movementController.WorldPosition-subject.WorldPosition;
subject.WorldPosition+=delta;
camera.WorldPosition+=delta;
distanceTraveled+=delta.WithZ(0).Length;
}
else if (TranslatePreview)
{
var delta=subject.WorldRotation.Forward*Speed*Time.Delta;
subject.WorldPosition+=delta;
distanceTraveled+=delta.Length;
camera.WorldPosition+=delta;
}
}
protected override void OnPreRender()
{
if(suitMotion.IsValid() && StopMovementAfter>0 && wallTime>StopMovementAfter+1)
{
suitSettledPeak=System.MathF.Max(suitSettledPeak,System.MathF.Abs(suitMotion.CurrentWeight));
if(!suitSettledReported && wallTime>StopMovementAfter+3)
{
suitSettledReported=true;
Log.Info("CITIZEN_SUIT_SETTLED "+Json.Serialize(new {capture_id=CaptureId,
ready=suitMotion.MorphsReady,peak=suitSettledPeak,speed=movementController.Velocity.Length}));
}
}
if(suitMotion.IsValid() && !suitMotionReported && wallTime>2)
{
suitMotionReported=true;
Log.Info("CITIZEN_SUIT_MOTION "+Json.Serialize(new {capture_id=CaptureId,
ready=suitMotion.MorphsReady,weight=suitMotion.CurrentWeight,peak=suitMotion.PeakWeight}));
}
if(ExerciseInteractiveVault && wallTime>=1.9f && wallTime<=3.4f &&
body.TryGetBoneTransform("ankle_L",out var motionLeft) && body.TryGetBoneTransform("ankle_R",out var motionRight))
{
var left=motionLeft.Position-subject.WorldPosition;
var right=motionRight.Position-subject.WorldPosition;
if(vaultHasPreviousFeet)
{
var step=System.MathF.Max((left-vaultPreviousLeft).Length,(right-vaultPreviousRight).Length);
if(step>vaultMaxLocalFootStep) { vaultMaxLocalFootStep=step;vaultStepTime=wallTime;vaultStepDelta=Time.Delta; }
}
vaultPreviousLeft=left;vaultPreviousRight=right;vaultHasPreviousFeet=true;vaultMotionSamples++;
// Ankle proxy only: the fixture spans x=-560..-540 and is 48 inches high.
// Include a six-inch margin; this does not prove boot mesh clearance.
foreach(var foot in new[]{motionLeft.Position,motionRight.Position})
if(foot.x>=-566 && foot.x<=-534 && foot.y>=-20 && foot.y<=160)
{ vaultSlabSamples++;vaultMinAnkleClearance=System.MathF.Min(vaultMinAnkleClearance,foot.z-48); }
}
if(ExerciseInteractiveVault && ((vaultReportStage==0 && wallTime>2.4f) || (vaultReportStage==1 && wallTime>3.4f)))
{
var nativeVault=movementController.Components.Get<VaultController>();
var leftValid=body.TryGetBoneTransform("ankle_L",out var leftFoot);
var rightValid=body.TryGetBoneTransform("ankle_R",out var rightFoot);
if(vaultReportStage==1) Log.Info("CITIZEN_VAULT_MOTION "+Json.Serialize(new { capture_id=CaptureId, samples=vaultMotionSamples, slab_samples=vaultSlabSamples, minimum_ankle_clearance=vaultSlabSamples>0 ? vaultMinAnkleClearance : (float?)null, maximum_local_foot_step=vaultMaxLocalFootStep, maximum_step_time=vaultStepTime, maximum_step_delta=vaultStepDelta }));
Log.Info("CITIZEN_VAULT_CHECK "+Json.Serialize(new { capture_id=CaptureId, phase=vaultReportStage++, selected_sequence=VaultSequence, sequence_duration=!body.UseAnimGraph ? body.Sequence.Duration : 0, sequence_time=!body.UseAnimGraph ? body.Sequence.Time : 0, vault_blend=vaultBlend, started=vaultExerciseStarted, vaulting=nativeVault.IsVaulting, outcome=nativeVault.Outcome, progress=nativeVault.Progress, graph=body.UseAnimGraph, left_valid=leftValid, right_valid=rightValid, left_z=leftFoot.Position.z, right_z=rightFoot.Position.z, body_z=subject.WorldPosition.z, input_restored=movementController.UseInputControls }));
}
if(ExerciseInteractiveCover && !ExerciseInteractiveVault && ((coverReportStage==0 && wallTime>1.6f) || (coverReportStage==1 && wallTime>3.2f) || (coverReportStage==2 && wallTime>(HighCoverExercise ? 5.3f : 4.3f)) || (HighCoverExercise && coverReportStage==3 && wallTime>6.3f)))
{
var phase=coverReportStage++;
var before=reviewWeapon.ActivePaintballs;
var fired=reviewWeapon.FireAt(marker.WorldPosition+marker.WorldRotation.Forward*1000);
Log.Info("CITIZEN_COVER_CHECK "+Json.Serialize(new { capture_id=CaptureId, phase, projectile_spawned=reviewWeapon.ActivePaintballs>before, blocked_corner=BlockCoverCorner, opposite_edge=OppositeCoverEdge, high_cover=HighCoverExercise, position_y=movementController.WorldPosition.y, corner_available=interactiveCover.CornerAvailable, attached=interactiveCover.Attached, peeking=interactiveCover.Peeking, duck=movementController.IsDucking, lowered=animation.IsWeaponLowered, shot_accepted=fired, input_restored=movementController.UseInputControls }));
}
if(!aimReported && wallTime>=1.8f && marker.IsValid())
{
aimReported=true;
var desired=(subject.WorldRotation*Rotation.FromPitch(AimPitch)).Forward;
var angle=System.MathF.Acos(Vector3.Dot(desired,marker.WorldRotation.Forward).Clamp(-1,1))*180/System.MathF.PI;
var hasHand=body.TryGetBoneTransform("hand_L",out var aimHand);
Log.Info("CITIZEN_AIM_FIT "+Json.Serialize(new { interactive=InteractiveControls, native_input=movementController.UseInputControls, native_look=movementController.UseLookControls, native_camera=movementController.UseCameraControls, weapon_input=reviewWeapon?.AcceptInput, capture_id=CaptureId, requested_pitch=AimPitch, applied_pitch=locomotion.AimPitch, barrel_angle_error=angle, marker_angles=marker.WorldRotation.Angles(), support_error=hasHand && supportTarget.IsValid() ? (aimHand.Position-supportTarget.WorldPosition).Length : -1 }));
}
if (PreviewReload)
{
foreach(var name in new[]{"pelvis","spine_0","spine_1","spine_2","neck_0"})
{
if(!body.TryGetBoneTransform(name,out var pose)) continue;
var height=pose.Position.z-subject.WorldPosition.z;
if(reviewTime<.75f) baselineBoneHeights[name]=height;
if(reviewTime>=1 && reviewTime<3.2f && baselineBoneHeights.TryGetValue(name,out var baseline))
maximumBoneRise[name]=System.MathF.Max(maximumBoneRise.GetValueOrDefault(name,float.MinValue),height-baseline);
}
}
if (PreviewReload && body.TryGetBoneTransform("head", out var headPose))
{
var height = headPose.Position.z - subject.WorldPosition.z;
if (reviewTime < .75f) baselineHeadHeight = height;
if (reviewTime >= 1 && reviewTime < 3.2f)
{
maximumReloadHeadHeight = System.MathF.Max(maximumReloadHeadHeight, height);
minimumReloadHeadHeight = System.MathF.Min(minimumReloadHeadHeight, height);
}
}
if (PreviewReload && body.TryGetBoneTransform("hand_L", out var motionHand))
{
if (hasPreviousHand && reviewTime > .5f && reviewTime < 4.5f)
{
var step = (motionHand.Position - previousHand).Length;
motionSamples++;
if (reviewTime < 1) maxReleaseStep = System.MathF.Max(maxReleaseStep, step);
else if (reviewTime >= 3.15f)
{
if (step > maxReturnStep) { maxReturnStep = step; maxReturnTime = reviewTime; }
}
else maxActionStep = System.MathF.Max(maxActionStep, step);
}
previousHand = motionHand.Position;
hasPreviousHand = true;
if (!reloadMotionReported && (reviewTime >= 4.5f || wallTime>=6))
{
reloadMotionReported = true;
Log.Info("CITIZEN_RELOAD_MOTION " + Json.Serialize(new { interactive=InteractiveControls, requested_pitch=AimPitch, final_pitch=locomotion.AimPitch, final_barrel_error=System.MathF.Acos(Vector3.Dot((subject.WorldRotation*Rotation.FromPitch(AimPitch)).Forward,marker.WorldRotation.Forward).Clamp(-1,1))*180/System.MathF.PI, shot_accepted=shotAccepted, shot_muzzle_error=shotMuzzleError, shot_projectiles=shotProjectiles, cancellation_events=cancellationEvents, end_round_cancel=CancelByEndRound, capture_id=CaptureId, presentation_time=reviewTime, pod_held=podHeld, cancelled=reloadCancelled, cancel_phase=CancelReloadAtPhase, weapon_clock=WeaponReloadClock, ammo_before=ammoBeforeReload, reserve_before=reserveBeforeReload, ammo_after=reviewWeapon?.Ammo, reserve_after=reviewWeapon?.Reserve, weapon_reload_remaining=reviewWeapon?.ReloadRemaining, controller_movement=ControllerMovement, cycle=reloadCycle, samples=motionSamples, translated=TranslatePreview, turn_speed=TurnSpeed, travel=distanceTraveled, displacement_x=subject.WorldPosition.x, release_step=maxReleaseStep, action_step=maxActionStep, return_step=maxReturnStep, return_time=maxReturnTime, grip=currentGrip, duck=DuckLevel, baseline_head=baselineHeadHeight, reload_head_min=minimumReloadHeadHeight, reload_head_max=maximumReloadHeadHeight, bone_rise=maximumBoneRise, posture_weight=reloadPosture, pod_picked=pickupError>=0, pod_returned=podReturned, pickup_error=pickupError, return_error=returnError, minimum_return_gap=minimumReturnGap, return_reach=returnReachDistance, return_arm_length=returnArmLength, pour_samples=pourSamples, pour_aligned_samples=pourAlignedSamples, pour_max_error=maximumPourError, pour_mean_error=pourSamples>0 ? totalPourError/pourSamples : 0 }));
}
}
if(gripTrack is not null)
{
float maximumError=0, maximumChange=0;
foreach(var sample in gripTrack.bones)
{
var handBone=body.Model.Bones.GetBone(sample.hand);
if(!body.TryGetBoneTransformAnimation(handBone,out var hand))continue;
var p=sample.position;var q=sample.rotation;
var local=new Transform(new Vector3(p[0],p[1],p[2]),new Rotation(q[0],q[1],q[2],q[3]));
var world=hand.ToWorld(local);
body.TryGetBoneTransform(sample.name,out var before);
body.SetBoneTransform(body.Model.Bones.GetBone(sample.name),body.WorldTransform.ToLocal(world));
if(gloveRenderer.IsValid())gloveRenderer.SetBoneTransform(gloveRenderer.Model.Bones.GetBone(sample.name),gloveRenderer.WorldTransform.ToLocal(world));
if(body.TryGetBoneTransform(sample.name,out var after))
{
maximumError=System.MathF.Max(maximumError,(after.Position-world.Position).Length);
maximumChange=System.MathF.Max(maximumChange,(after.Position-before.Position).Length);
}
}
if(!gripReported && reviewTime>1.5f) { Log.Info($"CITIZEN_GRIP_OVERRIDE bones={gripTrack.bones.Length} max_error={maximumError} max_change={maximumChange}");gripReported=true; }
}
if (!ExportHandPose || handPoseReported || reviewTime<1.5f) return;
var samples = new List<object>();
foreach(var bone in body.Model.Bones.AllBones.Where(x=>x.Name.StartsWith("finger_")))
{
var handName=bone.Name.EndsWith("_L") ? "hand_L" : "hand_R";
if (!body.TryGetBoneTransform(handName,out var hand) || !body.TryGetBoneTransform(bone.Name,out var pose)) continue;
var local=hand.ToLocal(pose);
samples.Add(new { name=bone.Name, hand=handName, position=new[]{local.Position.x,local.Position.y,local.Position.z}, rotation=new[]{local.Rotation.x,local.Rotation.y,local.Rotation.z,local.Rotation.w} });
}
Log.Info("CITIZEN_HAND_POSE " + Json.Serialize(new { source="Citizen HoldItem_HandPoses", phase=HandPose, bones=samples }));
handPoseReported=true;
}
private void ExerciseCover()
{
if(coverExerciseStage==0 && wallTime>.3f)
{
movementController.WorldPosition=new Vector3(-595,HighCoverExercise ? (OppositeCoverEdge ? 120 : 20) : 70,2);
movementController.EyeAngles=new Angles(0,0,0);
coverFixture=TrainingRange.Box(null,"Citizen cover review",new Vector3(-550,70,HighCoverExercise ? 45 : 24),new Vector3(20,180,HighCoverExercise ? 90 : 48),Color.Gray,true);
coverFixture.Components.Create<CoverSurface>();
if(BlockCoverCorner)
cornerObstruction=TrainingRange.Box(null,"Citizen blocked corner",new Vector3(-580,OppositeCoverEdge ? 175 : -35,35),new Vector3(28,25,70),Color.Gray,true);
interactiveCover.TestInput=true;coverExerciseStage=1;
}
if(coverExerciseStage==1 && wallTime>.7f) { interactiveCover.TryEnter();if(HighCoverExercise)interactiveCover.TestMovement=OppositeCoverEdge ? Vector3.Left : Vector3.Right;coverExerciseStage=2; }
if(coverExerciseStage==2 && wallTime>2)
{
interactiveCover.TestMovement=Vector3.Zero;
if(ExerciseInteractiveVault) vaultExerciseStarted=movementController.Components.Get<VaultController>().TryBegin();
else { interactiveCover.TestAim=true;reviewWeapon.ReviewAimOverride=true; }
coverExerciseStage=3;
}
if(coverExerciseStage==3 && wallTime>4)
{
interactiveCover.TestAim=false;reviewWeapon.ReviewAimOverride=false;
if(!HighCoverExercise) { interactiveCover.Leave();interactiveCover.TestInput=false;reviewWeapon.ReviewAimOverride=null; }
coverExerciseStage=4;
}
if(HighCoverExercise && coverExerciseStage==4 && wallTime>6)
{ interactiveCover.Leave();interactiveCover.TestInput=false;reviewWeapon.ReviewAimOverride=null;coverExerciseStage=5; }
}
private void ResetReloadCycle()
{
reviewTime=0;wallTime=0;weaponReloadRequested=false;reloadCancelled=false;reloadClock.Reset();cancellationEvents=0;
reloadCycle++;
reloadTriggered=false;reloadMotionReported=false;hopperReported=false;
markerGrip.ResetRecovery();podReload.ResetCycle();
maxReleaseStep=0;maxActionStep=0;maxReturnStep=0;maxReturnTime=0;motionSamples=0;
baselineBoneHeights.Clear();maximumBoneRise.Clear();
maximumReloadHeadHeight=float.MinValue;minimumReloadHeadHeight=float.MaxValue;
hasPreviousHand=false;
}
private void OnInteractiveReloadStarted()
{
ResetReloadCycle();
PreviewReload=true;weaponReloadRequested=true;reviewTime=.8f;
ammoBeforeReload=reviewWeapon.Ammo;reserveBeforeReload=reviewWeapon.Reserve;
}
private void OnWeaponReloadCancelled()
{
cancellationEvents++;
if(reloadCancelled || !subject.IsValid() || !body.IsValid()) return;
reloadCancelled=true;
if(body.TryGetBoneTransform("hand_L",out var interruptedHand))
cancelHand=subject.WorldTransform.ToLocal(interruptedHand);
if(refillPod.IsValid()) cancelPodRotation=subject.WorldRotation.Inverse*refillPod.WorldRotation;
}
protected override void OnDestroy()
{
if(reviewWeapon.IsValid())
{
reviewWeapon.PresentationAnchor=previousWeaponAnchor;
reviewWeapon.ReloadStarted-=OnInteractiveReloadStarted;
reviewWeapon.ReloadCancelled-=OnWeaponReloadCancelled;
reviewWeapon.CancelReload();
reviewWeapon.AcceptInput=weaponInputBefore;
}
if(ExerciseInteractiveCover && interactiveCover.IsValid())
{
interactiveCover.Leave();interactiveCover.TestInput=false;interactiveCover.TestAim=false;interactiveCover.TestMovement=Vector3.Zero;
if(reviewWeapon.IsValid()) reviewWeapon.ReviewAimOverride=null;
coverFixture?.Destroy();cornerObstruction?.Destroy();
}
subject?.Destroy();
foreach(var renderer in hiddenRenderers) if(renderer.IsValid()) renderer.Enabled=true;
if(ControllerMovement && movementController.IsValid())
{
movementController.WishVelocity=Vector3.Zero;
movementController.UpdateDucking(false);
movementController.UseInputControls=movementController.UseLookControls=movementController.UseCameraControls=true;
}
}
}