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();
}