OneMoreRoundWeapon.CycleAudio.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Timed bolt/pump/lever audio driven by the generic cycle presentation stream.
///
/// Cues are measured from CycleStarted, which already occurs after the cycle
/// profile's StartDelay and is generated peer-locally from the synchronized
/// ShotFired presentation event. The owner therefore hears the sound from the
/// viewmodel while remote peers hear it from the worldmodel.
/// </summary>
public partial class OneMoreRoundWeapon
{
[Property, Group( "Cycle Audio" )]
public List<OMRCycleSoundCue> CycleSoundCues { get; set; } = new();
[Property, Group( "Cycle Audio" )]
public bool LogCycleAudio { get; set; } = false;
private CancellationTokenSource _cycleAudioCts;
private bool _cycleAudioEventsBound;
private void BindCycleAudioPresentationEvents()
{
if ( _cycleAudioEventsBound )
return;
PresentationEvent += HandleCycleAudioPresentationEvent;
_cycleAudioEventsBound = true;
}
private void HandleCycleAudioPresentationEvent(
WeaponPresentationEvent presentationEvent
)
{
switch ( presentationEvent.Kind )
{
case WeaponPresentationEventKind.CycleStarted:
StartCycleAudio();
break;
case WeaponPresentationEventKind.CycleCompleted:
case WeaponPresentationEventKind.Holstered:
CancelCycleAudio();
break;
}
}
private void StartCycleAudio()
{
CancelCycleAudio();
if ( CycleSoundCues is null || CycleSoundCues.Count == 0 )
return;
var validCues = CycleSoundCues
.Where( cue => cue.Sound is not null )
.OrderBy( cue => cue.Time )
.ToList();
if ( validCues.Count == 0 )
return;
_cycleAudioCts = new CancellationTokenSource();
if ( LogCycleAudio )
{
Log.Info(
$"[OMR CYCLE AUDIO] Start | Weapon:{GameObject.Name} | " +
$"Cues:{validCues.Count} | Duration:{CycleProfile?.Duration ?? 0f:0.###}s"
);
}
_ = PlayCycleAudioAsync( validCues, _cycleAudioCts.Token );
}
private void CancelCycleAudio()
{
if ( _cycleAudioCts is null )
return;
if ( !_cycleAudioCts.IsCancellationRequested )
_cycleAudioCts.Cancel();
_cycleAudioCts.Dispose();
_cycleAudioCts = null;
if ( LogCycleAudio )
{
Log.Info( $"[OMR CYCLE AUDIO] Cancel/Finish | Weapon:{GameObject.Name}" );
}
}
private async Task PlayCycleAudioAsync(
List<OMRCycleSoundCue> cues,
CancellationToken cancellationToken
)
{
float elapsed = 0f;
try
{
foreach ( OMRCycleSoundCue cue in cues )
{
if ( cancellationToken.IsCancellationRequested )
return;
float cueTime = MathF.Max( cue.Time, 0f );
float delay = cueTime - elapsed;
if ( delay > 0f )
await Task.DelaySeconds( delay, cancellationToken );
if ( cancellationToken.IsCancellationRequested )
return;
GameObject emitter = GetCycleAudioEmitter();
if ( emitter is null || !emitter.IsValid )
return;
emitter.PlaySound( cue.Sound );
if ( LogCycleAudio )
{
Log.Info(
$"[OMR CYCLE AUDIO] Cue | Weapon:{GameObject.Name} | " +
$"Time:{cueTime:0.###}s | Sound:{cue.Sound.ResourceName}"
);
}
elapsed = cueTime;
}
}
catch ( TaskCanceledException )
{
// Expected when cycle presentation ends or the weapon is holstered.
}
catch ( OperationCanceledException )
{
// Expected when cycle presentation ends or the weapon is holstered.
}
}
private GameObject GetCycleAudioEmitter()
{
BaseWeaponModel model = WeaponModel;
if ( model is not null && model.GameObject is not null && model.GameObject.IsValid )
return model.GameObject;
return GameObject;
}
}