Weapons/BaseCarryable/Carryable.cs

Carryable weapon base class and a TraceAttackInfo record. TraceAttackInfo packages trace hit data. Carryable extends BaseCombatWeapon, integrates with Player owner, handles per-frame updates, input control, equip/holster hooks, camera hooks, RPC TraceAttack to apply damage and force, and inventory switching.

NetworkingNative Interop
using Sandbox.Rendering;

/// <summary>
/// Info about a trace attack. It's a struct so we can add to it without updating params everywhere.
/// </summary>
/// <param name="Target"></param>
/// <param name="Damage"></param>
/// <param name="Tags"></param>
/// <param name="Position"></param>
/// <param name="Origin"></param>
public record struct TraceAttackInfo( GameObject Target, float Damage, TagSet Tags = null, Vector3 Position = default, Vector3 Origin = default )
{
	/// <summary>
	/// Constructs a <see cref="TraceAttackInfo"/> from a trace and input damage.
	/// </summary>
	public static TraceAttackInfo From( SceneTraceResult tr, float damage, TagSet tags = default, bool localise = true )
	{
		tags ??= new();

		if ( localise && tr.Hitbox?.Tags is not null )
		{
			tags.Add( tr.Hitbox?.Tags );
		}

		return new TraceAttackInfo( tr.GameObject, damage, tags, tr.HitPosition, tr.StartPosition );
	}
}

/// <summary>
/// A carryable is an item that can be carried in a player's inventory, such as a weapon or tool. This can be selected and equipped by the player.
/// The most notable difference between an item and carryable is that carryables have viewmodels and world models and can be selected in the weapon list.
/// </summary>
/// <remarks>
/// The inventory, slotting, view model, world model, magazine and reload plumbing all come from
/// <see cref="BaseCombatWeapon"/>. What lives here is the deathmatch-specific layer on top: our own
/// <see cref="Player"/> owner, the crosshair HUD, camera hooks and the <see cref="TraceAttack"/>
/// damage path.
/// </remarks>
public partial class Carryable : BaseCombatWeapon, IKillIcon
{
	/// <summary>
	/// The player that owns this. Deliberately hides <see cref="BaseCombatWeapon.Owner"/> (a
	/// <see cref="PlayerController"/>) - our game code wants the <see cref="Player"/> that sits
	/// alongside it, and reaches the controller through <see cref="Player.Controller"/>.
	/// </summary>
	public new Player Owner => GetComponentInParent<Player>( true );

	/// <summary>
	/// The engine draws a weapon's HUD for any holder that isn't a proxy, which on the host includes
	/// every bot it simulates - so their crosshairs pile up on the host's screen. Only the human
	/// playing on this client gets a HUD; everyone else still needs their body animated.
	/// </summary>
	protected override void OnUpdate()
	{
		var player = Owner;

		if ( player.IsValid() && !player.IsLocalPlayer )
		{
			if ( HolderRenderer is { } body && body.IsValid() )
				UpdateBodyAnimations( body );

			return;
		}

		base.OnUpdate();
	}

	/// <summary>
	/// Can we switch to this?
	/// </summary>
	public virtual bool CanSwitch()
	{
		return true;
	}

	protected override bool OnCanSwitchTo() => CanSwitch();

	/// <summary>
	/// The engine pumps this once per frame on the client that owns the inventory, for the deployed
	/// item only. Bots are owned by the host but drive themselves through
	/// <see cref="PlayerBotController"/>, not input - so keep them out of the input path entirely.
	/// </summary>
	protected override void OnControl()
	{
		var player = Owner;
		if ( !player.IsValid() || !player.IsLocalPlayer )
			return;

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

		OnPlayerUpdate( player );
	}

	/// <summary>
	/// Called every update for the owning client while this is deployed.
	/// </summary>
	public virtual void OnPlayerUpdate( Player player )
	{
		OnControl( player );
	}

	/// <summary>
	/// Called every update, scoped to the owning player
	/// </summary>
	/// <param name="player"></param>
	public virtual void OnControl( Player player )
	{
	}

	/// <summary>
	/// Called when this is pulled out
	/// </summary>
	public virtual void OnEquipped( Player player )
	{
	}

	/// <summary>
	/// Called when this is put away
	/// </summary>
	public virtual void OnHolstered( Player player )
	{
	}

	protected override void OnEquipped()
	{
		base.OnEquipped();

		// The engine leaves interpolation on, which makes a hand-attached world model swim.
		GameObject.Network.Interpolation = false;

		if ( Owner.IsValid() )
			OnEquipped( Owner );
	}

	protected override void OnHolstered()
	{
		base.OnHolstered();

		if ( Owner.IsValid() )
			OnHolstered( Owner );
	}

	/// <summary>
	/// Called when setting up the camera - use this to apply effects on the camera based on this carriable
	/// </summary>
	/// <param name="player"></param>
	/// <param name="camera"></param>
	public virtual void OnCameraSetup( Player player, Sandbox.CameraComponent camera )
	{
	}

	/// <summary>
	/// Can directly influence the player's eye angles here
	/// </summary>
	/// <param name="player"></param>
	/// <param name="angles"></param>
	public virtual void OnCameraMove( Player player, ref Angles angles )
	{
	}

	/// <summary>
	/// Run a trace related attack with some set information.
	/// This is targeted to the host who then does things.
	/// </summary>
	/// <param name="attack"></param>
	[Rpc.Host]
	public void TraceAttack( TraceAttackInfo attack )
	{
		if ( !attack.Target.IsValid() )
			return;

		if ( !Owner.IsValid() )
			return;

		var damagable = attack.Target.GetComponentInParent<IDamageable>();
		if ( damagable is not null )
		{
			var info = new DamageInfo( attack.Damage, Owner.GameObject, GameObject );
			info.Position = attack.Position;
			info.Origin = attack.Origin;
			info.Tags = attack.Tags;

			damagable.OnDamage( info );
		}

		if ( attack.Target.GetComponentInChildren<Rigidbody>() is var rb && rb.IsValid() )
		{
			rb.ApplyForce( (attack.Position - attack.Origin) * 1000f );
		}
	}

	/// <summary>
	/// Tell the player to switch away from us, to the next best weapon
	/// </summary>
	public void SwitchAway()
	{
		// ShouldAvoid keeps GetBestItem from handing us straight back.
		Inventory?.SwitchToBest();
	}

	/// <summary>
	/// Is this item currently being used? When true, prevents auto-switching away on item pickup etc.
	/// </summary>
	public virtual bool IsInUse()
	{
		return false;
	}

	/// <summary>
	/// Called when the owner dies
	/// </summary>
	public virtual void OnPlayerDeath( IPlayerEvent.DiedParams args )
	{
	}
}