OneMoreRoundWeapon.PresentationEvents.cs
using Sandbox;
using System;

/// <summary>
/// Bridges S&box BaseCombatWeapon lifecycle/presentation hooks into one small,
/// reusable event stream. The engine remains responsible for networking,
/// ballistics and stock BaseWeaponModel presentation.
/// </summary>
public partial class OneMoreRoundWeapon : IWeaponPresentationEventSource
{
	[Property, Group( "Presentation Events" )]
	public bool LogPresentationEvents { get; set; } = false;

	public event Action<WeaponPresentationEvent> PresentationEvent;

	public int PresentationEventCount { get; private set; }
	public WeaponPresentationEventKind? LastPresentationEvent { get; private set; }
	public float LastPresentationEventTime { get; private set; } = -1f;

	private int _presentationEventSequence;

	private void InitializePresentationEventPipeline()
	{
		_presentationEventSequence = 0;
		PresentationEventCount = 0;
		LastPresentationEvent = null;
		LastPresentationEventTime = -1f;

		BindReloadAudioPresentationEvents();
		BindCycleAudioPresentationEvents();
		BindCyclePresentationEvents();
		BindWeaponModelPresentationEvents();
	}

	private void RaisePresentationEvent(
		WeaponPresentationEventKind kind
	)
	{
		_presentationEventSequence++;
		PresentationEventCount++;
		LastPresentationEvent = kind;
		LastPresentationEventTime = RealTime.Now;

		var presentationEvent = new WeaponPresentationEvent
		{
			Kind = kind,
			Weapon = this,
			Sequence = _presentationEventSequence,
			Time = LastPresentationEventTime,
			Clip = Clip1,
			Reserve = Ammo1
		};

		if ( LogPresentationEvents )
		{
			Log.Info(
				$"[OMR WEAPON EVENT] {kind} | Weapon:{WeaponId} | " +
				$"Seq:{presentationEvent.Sequence} | Clip:{presentationEvent.Clip} | " +
				$"Reserve:{presentationEvent.Reserve}"
			);
		}

		PresentationEvent?.Invoke( presentationEvent );
	}

	protected override void OnEquipped()
	{
		// Give the delayed recovery path one fresh chance for this deployment.
		// Some peers receive the active-item change before the holder bone is
		// ready, so the normal base creation may legitimately need one retry.
		_worldModelRecoveryAttempted = false;
		_timeAlive = 0f;

		// BaseCombatWeapon creates both world- and view-models here, not in
		// OnStart. Inventory activation can call Equip immediately after the
		// item GameObject is enabled, so canonical definition data must be
		// available before base.OnEquipped() reads the prefab fields.
		ApplyWeaponDefinitionIfAssigned();

		base.OnEquipped();

		// BaseCombatWeapon treats any non-proxy owner as a local first-person
		// holder. Host-owned bots are also non-proxy, so the base equip path
		// legitimately creates a local viewmodel for them. A bot must only
		// expose its third-person world model; otherwise the host sees a second,
		// un-driven viewmodel floating in the composed camera. Tear that owner-
		// local presentation down immediately after the engine equip path.
		SuppressBotViewModel();

		EnsureEquippedPresentationModels();
		RaisePresentationEvent( WeaponPresentationEventKind.Equipped );
	}

	/// <summary>
	/// Host-owned bots share the host's network ownership state, so the stock
	/// weapon system can create an owner-only first-person viewmodel for them.
	/// Bots never render first-person presentation; their world model is the
	/// only weapon model the host/client should see.
	/// </summary>
	internal void SuppressBotViewModel()
	{
		if ( !IsBotControlled || ViewModel is null )
			return;

		DestroyViewModel();
	}

	/// <summary>
	/// Defensive model recovery for the equip lifecycle. BaseCombatWeapon's
	/// creation methods are safe/idempotent for the viewmodel, while the world
	/// model is only retried when no instance exists. This also covers peers
	/// where holder bones become available just after the active-item change.
	/// </summary>
	private void EnsureEquippedPresentationModels()
	{
		if ( !IsActive || !IsHeld )
			return;

		if ( WorldModel is null && WorldModelPrefab is not null && HolderRenderer is not null )
			CreateWorldModel();

		if (
			!IsProxy &&
			!IsBotControlled &&
			ViewModel is null &&
			ViewModelPrefab is not null
		)
		{
			CreateViewModel();
		}
	}

	/// <summary>
	/// OnShootEffects runs on every peer that should see the shot. For pellet
	/// weapons, S&box marks secondary pellet effects as NoEvents; emitting only
	/// for the lead effect keeps this at one ShotFired event per trigger pull.
	/// </summary>
	protected override void OnShootEffects( ShotEffect shot )
	{
		base.OnShootEffects( shot );

		if ( shot.NoEvents )
			return;

		RaisePresentationEvent( WeaponPresentationEventKind.ShotFired );
	}

	/// <summary>
	/// DryFire is owner-side input/presentation. Base retains responsibility for
	/// dry-fire sound, cooldown throttling and optional auto-reload.
	/// </summary>
	public override void DryFire()
	{
		base.DryFire();
		RaisePresentationEvent( WeaponPresentationEventKind.DryFire );
	}

	protected override void OnReloadStarted()
	{
		base.OnReloadStarted();
		RaisePresentationEvent( WeaponPresentationEventKind.ReloadStarted );
	}

	protected override void OnReloadInserted()
	{
		base.OnReloadInserted();
		RaisePresentationEvent( WeaponPresentationEventKind.ReloadInserted );
	}

	protected override void OnReloadCancelled()
	{
		base.OnReloadCancelled();
		RaisePresentationEvent( WeaponPresentationEventKind.ReloadCancelled );
	}

	/// <summary>
	/// BaseCombatWeapon calls this for both a successful reload and a cancelled
	/// reload. We deliberately expose the same neutral meaning here as
	/// ReloadFinished; consumers that care about interruption also receive the
	/// explicit ReloadCancelled event from the engine hook.
	/// </summary>
	protected override void OnReloadFinished()
	{
		base.OnReloadFinished();
		RaisePresentationEvent( WeaponPresentationEventKind.ReloadFinished );
	}

	internal void RaiseCycleStartedPresentation()
	{
		RaisePresentationEvent( WeaponPresentationEventKind.CycleStarted );
	}

	internal void RaiseCycleCompletedPresentation()
	{
		RaisePresentationEvent( WeaponPresentationEventKind.CycleCompleted );
	}

	// Compatibility wrappers for code written during Phase 4 before cycling was
	// generalized beyond bolt-action weapons. New consumers should use Cycle*.
	internal void RaiseBoltCycleStartedPresentation() =>
		RaiseCycleStartedPresentation();

	internal void RaiseBoltCycleCompletedPresentation() =>
		RaiseCycleCompletedPresentation();
}