Game/Weapon/BaseSandboxWeapon/BaseSandboxWeapon.cs

A game weapon base class for s&box that extends the engine BaseCombatWeapon. It provides owner lookup, seated/contraption control, camera hooks, drop behavior, and some inventory/UI flags and overrides for weapon behavior.

NetworkingFile Access
using Sandbox.Rendering;

public partial class BaseSandboxWeapon : Sandbox.BaseCombatWeapon, IKillIcon, IPlayerControllable
{
	// DisplayName, DisplayIcon, Value, Slot, ViewModel and WorldModel are all inherited from the
	// engine BaseCombatWeapon / BaseInventoryItem. (DisplayIcon also satisfies IKillIcon.)

	// MuzzleGameObject is inherited from the engine BaseCombatWeapon.

	/// <summary>
	/// Used for overriding the display icon
	/// </summary>
	public virtual string InventoryIconOverride => null;

	/// <summary>
	/// If true the game should hide the hud when holding this weapon. Useful for cameras, or scopes.
	/// </summary>
	public virtual bool WantsHideHud => false;

	// WeaponModel resolution (view model when drawn, else world model, else own hierarchy) comes from
	// the engine BaseCombatWeapon.

	/// <summary>
	/// The owner of this carriable
	/// </summary>
	// Hides the engine BaseCombatWeapon.Owner (a PlayerController) with the game's Player-based owner.
	// Engine-internal code keeps using its own PlayerController owner; game code sees the Player.
	public new Player Owner
	{
		get
		{
			return GetComponentInParent<Player>( true );
		}
	}

	public bool HasOwner => Owner.IsValid();

	/// <summary>
	/// When true, seated aim uses the scene camera direction instead of the weapon's muzzle direction.
	/// Override in weapons that support player-directed aim (e.g. RPG tracked mode, Physgun aim mode).
	/// </summary>
	public virtual bool IsTargetedAim => false;

	/// <summary>
	/// Controlled from a seat with targeted aim fires where the camera looks; otherwise the engine's
	/// muzzle-based unheld aim applies.
	/// </summary>
	protected override Ray UnheldAimRay
	{
		get
		{
			var seated = ClientInput.Current;
			if ( seated.IsValid() && IsTargetedAim && Scene.Camera.IsValid() )
				return Scene.Camera.Transform.World.ForwardRay;

			return base.UnheldAimRay;
		}
	}

	/// <summary>
	/// The root GameObject to ignore when tracing from AimRay.
	/// </summary>
	public GameObject AimIgnoreRoot => HasOwner ? Owner.GameObject : GameObject;

	/// <summary>
	/// Who gets credit for this weapon's damage (the engine's <see cref="Sandbox.BaseCombatWeapon.Attacker"/>).
	/// The owning player if held, the seated player if controlled from a contraption seat, otherwise
	/// the nearest kill source or the weapon itself.
	/// </summary>
	protected override GameObject Attacker
	{
		get
		{
			if ( HasOwner ) return Owner.GameObject;

			var seatedPlayer = ClientInput.Current;
			if ( seatedPlayer.IsValid() ) return seatedPlayer.GameObject;

			var killSource = GetComponentInParent<IKillSource>( true );
			if ( killSource is Component c ) return c.GameObject;

			return GameObject;
		}
	}

	// The switch gate is the engine's - query with CanSwitchTo(), override OnCanSwitchTo() to gate.


	// DrawHud / DrawCrosshair and the per-frame crosshair positioning come from the engine BaseCombatWeapon.

	/// <summary>
	/// Runs on the host when added to an inventory. The engine base seeds the magazines and the
	/// shared reserve pool - only when they've never been filled, so a dropped gun keeps its rounds
	/// through a pickup.
	/// </summary>
	protected override void OnAdded( Sandbox.BaseInventoryComponent inventory )
	{
		// In an inventory - not a world pickup any more.
		IsDropped = false;

		base.OnAdded( inventory );
	}

	//
	// Seat / contraption control (IPlayerControllable) - a weapon mounted on a contraption fires from
	// the driver's inputs.
	//

	/// <summary>The input that fires the primary attack when this weapon is controlled via a seat.</summary>
	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput ShootInput { get; set; }

	/// <summary>The input that fires the secondary attack when this weapon is controlled via a seat.</summary>
	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput SecondaryInput { get; set; }

	/// <summary>Weapons wired into a contraption stay put - no pickup prompt, no Touch pickup.</summary>
	protected override bool OnCanPickup( Sandbox.BaseInventoryComponent inventory )
	{
		return !ShootInput.IsEnabled && !SecondaryInput.IsEnabled;
	}

	/// <summary>A seated player can only control this while not holding a weapon of their own.</summary>
	public virtual bool CanControl( Player player )
	{
		var inventory = player.GetComponent<PlayerInventory>();
		return inventory is null || !inventory.ActiveWeapon.IsValid();
	}

	public virtual void OnStartControl() { }

	public virtual void OnEndControl() { }

	// Explicit interface impl so it doesn't clash with the engine's held-item OnControl pump;
	// subclasses override OnSeatControl to change the seated behaviour.
	void IPlayerControllable.OnControl() => OnSeatControl();

	protected virtual void OnSeatControl()
	{
		if ( HasOwner ) return;
		// Seat fire is fully host-authoritative - the host reads the driver's synced ClientInput and
		// runs the shot for real, damage applying directly (no hit claims). The driving client doesn't
		// run the attack at all.
		if ( !Networking.IsHost ) return;

		if ( ShootInput.Down() )
			FirePrimary();

		if ( SecondaryInput.Down() )
			FireSecondary();
	}

	/// <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 )
	{
	}

	// The view the gun is placed at - captured before the camera bone offsets the view, so authored
	// camera animation moves the camera around the gun, not the gun with it.
	CameraView _viewModelView;

	protected override void ModifyCamera( CameraComponent camera, ref CameraView view )
	{
		base.ModifyCamera( camera, ref view );

		_viewModelView = view;

		ViewModel?.GetComponentInChildren<ViewModel>()?.UpdateCameraBone( ref view );
	}

	protected override void PlaceViewModel( CameraComponent camera, in CameraView view )
	{
		ViewModel?.GetComponentInChildren<ViewModel>()?.Place( _viewModelView );
	}

	/// <summary>
	/// Drops this weapon into the world as a pickup - the same live object, so it keeps its state
	/// (ammo, tool modes). Unparents it, drops network ownership, enables the dropped components and
	/// tags it cleanable. Host only.
	/// </summary>
	public void DropIntoWorld( Vector3 position, Vector3 velocity, Connection owner = null )
	{
		if ( !Networking.IsHost )
			return;

		GameObject.SetParent( null, true );
		GameObject.Enabled = true;
		WorldPosition = position;
		Slot = -1;

		Network.DropOwnership();

		Ownable.Set( GameObject, owner );
		GameObject.Tags.Add( "removable" );

		IsDropped = true;

		if ( GetComponent<Rigidbody>( true ) is { } body )
		{
			body.Velocity = velocity;
			body.AngularVelocity = Vector3.Random * 8.0f;
		}
	}

	/// <summary>
	/// The engine inventory is dropping this - throw the live object out in front of the dropper.
	/// </summary>
	protected override bool OnDrop()
	{
		var player = Owner;

		if ( player.IsValid() )
		{
			var eye = player.EyeTransform;
			var velocity = (player.Controller.IsValid() ? player.Controller.Velocity : Vector3.Zero)
				+ eye.Forward * 200f + Vector3.Up * 100f;

			DropIntoWorld( eye.Position + eye.Forward * 48f, velocity, player.Network.Owner );
		}
		else
		{
			DropIntoWorld( WorldPosition, Vector3.Up * 50f );
		}

		return true;
	}

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

	public virtual void OnPlayerDeath( PlayerDiedParams args )
	{
	}
}