NetworkPawn.cs
using Sandbox;

using System;

namespace PaintballBaddies;



/// <summary>Owner movement/presentation state; host validates combat requests.</summary>

public sealed class NetworkPawn : Component

{
    [Sync(SyncFlags.FromHost)] public Vector3 JoinPosition {get;set;}
    [Sync(SyncFlags.FromHost)] public Angles JoinFacing {get;set;}
    public bool LocalWorldReady {get;private set;}
    private Vector3 heldSpawn;
    private Angles heldFacing;
    private bool pendingPlacement;

    private bool WaitForLocalWorld()
    {
        if(IsProxy || LocalWorldReady)return false;
        if(!player.IsValid() || !player.Body.IsValid())return true;
        if(!pendingPlacement){heldSpawn=JoinPosition;heldFacing=JoinFacing;pendingPlacement=true;}
        player.WorldPosition=heldSpawn;
        player.Body.MotionEnabled=false;player.Body.Velocity=Vector3.Zero;player.WishVelocity=Vector3.Zero;
        player.UseInputControls=player.UseLookControls=false;
        var session=MultiplayerSession.Find(Scene);
        if(session?.LocalArenaReady!=true || !session.ClearStart(heldSpawn))return true;
        // Placement is released only once. Later cover, falls and jumps retain
        // their normal controller ownership; this is not a teleport watchdog.
        LocalWorldReady=true;
        player.EyeAngles=heldFacing;
        player.Body.MotionEnabled=true;player.Body.Sleeping=false;
        player.UseInputControls=player.UseLookControls=player.UseCameraControls=true;
        Network.ClearInterpolation();
        return false;
    }

    protected override void OnFixedUpdate(){WaitForLocalWorld();}

    [Property,Sync(SyncFlags.FromHost)] public int Seat { get; set; }

    [Property,Sync(SyncFlags.FromHost)] public Guid ConnectionId { get; set; }

    [Sync] public bool Ducking { get; set; }

    [Sync] public bool Grounded { get; set; }

    [Sync] public bool Covered { get; set; }

    [Sync] public bool Low { get; set; }

    [Sync] public bool Peek { get; set; }

    [Sync] public bool Sliding { get; set; }

    [Sync] public Vector3 SlideDirection { get; set; }

    [Sync] public Vector3 CoverNormal { get; set; }

    [Sync] public string CoverName { get; set; }

    [Sync] public bool Vaulting { get; set; }

    [Sync] public float VaultProgress { get; set; }

    [Sync] public Rotation VaultFacing { get; set; }

    [Sync] public bool Aiming { get; set; }

    [Sync] public Vector3 Motion { get; set; }

    [Sync] public Angles Look { get; set; }

    [Sync(SyncFlags.FromHost)] public int AcceptedShots { get; private set; }

    [Sync(SyncFlags.FromHost)] public int RejectedShots { get; private set; }

    [Sync(SyncFlags.FromHost)] public string ContactState { get; private set; }="";

    private PlayerController player;

    private float sinceShot=10,stateTime,previousShotInterval;

    public int RecoveryRequests {get;private set;}

    public int RecoveryAccepted {get;private set;}

    private int lastSequence;

    private bool requestedPaint;

    protected override void OnStart()

    {

        player=Components.Get<PlayerController>();
        WaitForLocalWorld();

        if(IsProxy)player.UseInputControls=player.UseLookControls=player.UseCameraControls=false;

        if(MultiplayerSession.Authority)

        {

            var actor=Components.Get<PaintballCombatant>();actor.Team=Seat+10;actor.StayInMatch=true;

        }

        Components.Get<RosterSelection>()?.SelectNetwork(Seat);

    }

    protected override void OnUpdate()

    {

        sinceShot+=Time.Delta;

        if(!IsProxy && !requestedPaint && sinceShot>11){requestedPaint=true;RequestPaint();}

        if(!player.IsValid())return;
        if(WaitForLocalWorld())return;

        if(IsProxy)

        {

            player.UseInputControls=player.UseLookControls=player.UseCameraControls=false;

            player.EyeAngles=Look;player.UpdateDucking(Ducking);

            Components.Get<CoverController>()?.ApplyNetwork(this);

            Components.Get<VaultController>()?.ApplyNetwork(this);

        }

        else

        {

            var cover=Components.Get<CoverController>();var vault=Components.Get<VaultController>();

            // A destroyed component can remain referenced until cover's next

            // fixed update, while its GameObject is already null. Release the

            // reservation before publishing state so movement is restored too.

            var reserved=cover?.ReservedSurface;

            var coverObject=reserved.IsValid() ? reserved.GameObject : null;

            if(cover.IsValid() && (cover.Attached || cover.Sliding) && !coverObject.IsValid())cover.Leave();

            Covered=cover?.Attached==true;Low=cover?.LowCover==true;Peek=cover?.Peeking==true;

            Sliding=cover?.Sliding==true;SlideDirection=cover?.SlideDirection ?? Vector3.Zero;

            CoverNormal=cover?.Normal ?? Vector3.Zero;CoverName=coverObject.IsValid() ? coverObject.Name : "";

            Vaulting=vault?.IsVaulting==true;VaultProgress=vault?.Progress ?? 0;VaultFacing=vault?.Facing ?? Rotation.Identity;

            Ducking=player.IsDucking;Grounded=player.IsOnGround;

            Aiming=Components.Get<PaintballMarker>()?.Aiming==true;Motion=player.Velocity;Look=player.EyeAngles;

        }

        if(MultiplayerSession.Authority)

        {

            stateTime-=Time.Delta;

            if(stateTime<=0)PublishContactState();

        }

        else Components.Get<CloseCombat>()?.ApplyNetworkState(ContactState);

    }

    [Rpc.Host(NetFlags.OwnerOnly)]

    public async void RequestPaint()

    {

        var caller=Rpc.Caller?.Id;

        await GameTask.MainThread();

        if(!this.IsValid() || caller!=ConnectionId)return;

        var session=MultiplayerSession.Find(Scene);ReceivePaint(session.PaintSnapshot());

        if(session.Status=="TIME UP")EndRound();

    }

    [Rpc.Owner(NetFlags.HostOnly)]

    public async void ReceivePaint(string json)

    {

        await GameTask.MainThread();

        if(!this.IsValid())return;

        MultiplayerSession.Find(Scene)?.ReplayPaint(json);

    }

    internal void PublishContactState()

    {

        if(!MultiplayerSession.Authority)return;

        var contact=Components.Get<CloseCombat>();

        stateTime=contact?.Attacking==true ? .033f : .1f;

        ContactState=contact?.CaptureNetworkState() ?? "";

    }

    [Rpc.Owner(NetFlags.HostOnly)]

    public async void ContactFeedback(bool received,bool knocked)

    {

        await GameTask.MainThread();

        if(!this.IsValid())return;

        Components.Get<CloseCombat>()?.PlayContactFeedback(received,knocked);

    }

    [Rpc.Host(NetFlags.OwnerOnly)]

    public async void RequestShot(Vector3 origin,Vector3 aim,int sequence,bool exposedFromCover=false,GameObject assistedPlate=null)

    {

        // Delayed RPCs can resume on a worker thread. Preserve caller identity

        // before yielding, then validate and touch engine objects on its main thread.

        var caller=Rpc.Caller?.Id;

        await GameTask.MainThread();

        if(!this.IsValid())return;

        var session=MultiplayerSession.Find(Scene);var state=Components.Get<PaintballCombatant>();

        bool finite=float.IsFinite(origin.x)&&float.IsFinite(origin.y)&&float.IsFinite(origin.z)

            &&float.IsFinite(aim.x)&&float.IsFinite(aim.y)&&float.IsFinite(aim.z);

        if(caller!=ConnectionId || !finite || sequence<=lastSequence || sinceShot<previousShotInterval-.015f

            || !state.AcceptHits || (!session.InRound && session.Status!="LOBBY") || Components.Get<CloseCombat>()?.Incapacitated==true

            || Components.Get<CloseCombat>()?.Attacking==true || Covered&&!Peek&&!exposedFromCover || Vaulting || Sliding

            || (origin-WorldPosition).Length>115 || (aim-origin).Length<1)

        {RejectedShots++;return;}

        // Owner-controlled peek sync can arrive after this RPC, especially for

        // a single-click shot. Carry its at-shot state with the request, while

        // still enforcing the physical eye-to-muzzle obstruction below.

        lastSequence=sequence;sinceShot=0;previousShotInterval=OverdriveShotgun.IntervalFor(Scene,GameObject);AcceptedShots++;

        var obstruction=PaintballFlight.Trace(Scene,player.EyePosition,origin,GameObject,hitEnemies:false);

        if(session.IsOverdrive)
        {
            if(session.InRound && !obstruction.Hit)
                OverdriveShotgun.Fire(Scene,state,origin,ArcadePlateAssist.Reconcile(player,assistedPlate,origin,aim),sequence);
            return;
        }
        var tint=CharacterPaintColor.For(GameObject);

        NetworkEffects.Sound(CharacterMarkerSound.For(GameObject),origin,ConnectionId);

        ArenaOpponent.ReportMarkerSound(Scene,GameObject,origin);

        if(obstruction.Hit){PaintImpactSystem.Find(Scene)?.Spawn(obstruction,tint);return;}

        var target=origin+(aim-origin).Normal*MathF.Min((aim-origin).Length,PaintballFlight.Speed*PaintballFlight.Lifetime);

        NetworkEffects.SpawnBall(GameObject,state.Team,origin,PaintballFlight.LaunchVelocity(origin,target));

    }

    [Rpc.Host(NetFlags.OwnerOnly)]
    public async void RequestGrenade(Vector3 aim)
    {
        var caller=Rpc.Caller?.Id;
        await GameTask.MainThread();
        if(!this.IsValid() || caller!=ConnectionId || Vaulting || Sliding)return;
        Components.Get<PaintballCombatant>()?.TryThrowGrenade(aim);
    }

    [Rpc.Host(NetFlags.OwnerOnly)]
    public async void RequestWeapon(int weapon)
    {
        var caller=Rpc.Caller?.Id;
        await GameTask.MainThread();
        if(!this.IsValid() || caller!=ConnectionId)return;
        Components.Get<PaintballCombatant>()?.EquipWeapon(weapon);
        // Changing equipment never resets the host's shot cooldown.
    }

    [Rpc.Host(NetFlags.OwnerOnly)]

    public async void RequestElbow()

    {

        var caller=Rpc.Caller?.Id;

        await GameTask.MainThread();

        if(!this.IsValid())return;

        if(caller==ConnectionId && Components.Get<PaintballCombatant>().AcceptHits)Components.Get<CloseCombat>()?.TryAttackAuthoritative();

    }

    [Rpc.Host(NetFlags.OwnerOnly)]

    public async void RequestRecovery()

    {

        var caller=Rpc.Caller?.Id;

        await GameTask.MainThread();

        if(!this.IsValid())return;

        if(caller==ConnectionId){RecoveryRequests++;if(Components.Get<CloseCombat>()?.RegisterRecoveryAuthoritative()==true)RecoveryAccepted++;}

    }

    [Rpc.Owner(NetFlags.HostOnly)]

    public async void ConfirmHit()

    {

        await GameTask.MainThread();

        if(!this.IsValid())return;

        Components.Get<PaintballMarker>()?.NetworkHit();

    }

    [Rpc.Owner(NetFlags.HostOnly)]

    public async void ResetForRound(Vector3 position,int round,Angles facing)

    {

        await GameTask.MainThread();

        if(!this.IsValid())return;

        Components.Get<CoverController>()?.Leave();Components.Get<VaultController>()?.Cancel();Components.Get<CloseCombat>()?.ResetState();

        player ??= Components.Get<PlayerController>();
        heldSpawn=position;heldFacing=facing;pendingPlacement=true;
        if(!LocalWorldReady){WaitForLocalWorld();return;}

        player.WorldPosition=position;player.Body.MotionEnabled=true;player.Body.Sleeping=false;player.Body.Velocity=Vector3.Zero;player.WishVelocity=Vector3.Zero;

        player.EyeAngles=facing;

        player.UseInputControls=player.UseLookControls=player.UseCameraControls=true;

        Components.Get<PaintballMarker>()?.ResetTraining();Components.Get<PaintballMarker>().AcceptInput=true;

        Network.ClearInterpolation();

    }

    [Rpc.Owner(NetFlags.HostOnly)]

    public async void EndRound()

    {

        await GameTask.MainThread();

        if(!this.IsValid())return;

        Components.Get<CoverController>()?.Leave();Components.Get<VaultController>()?.Cancel();Components.Get<CloseCombat>()?.ResetState();

        Components.Get<PaintballMarker>()?.EndRound();

        player.UseInputControls=false;player.WishVelocity=Vector3.Zero;

        if(player.Body.IsValid())player.Body.Velocity=Vector3.Zero;

        var match=Components.Get<ArenaMatch>();

        var session=MultiplayerSession.Find(Scene);

        bool won=session?.IsOverdrive==true ? session.OverdriveWon : session?.IsPopRiot==true ? session.RiotWon : match?.Standings.FirstOrDefault()?.IsPlayer==true;

        Sound.Play(won ? "sounds/paintball/round_win.sound" : "sounds/paintball/round_loss.sound",WorldPosition);

    }

}