Game/Weapon/BaseWeapon/BaseWeapon.Reloading.cs
using System.Threading;

public partial class BaseWeapon
{
	/// <summary>
	/// Should we consume 1 bullet per reload instead of filling the clip?
	/// </summary>
	[Property, Feature( "Ammo" )]
	public bool IncrementalReloading { get; set; } = false;

	/// <summary>
	/// Extra delay after the first shell reload before subsequent shells begin (e.g. longer carrier insertion animation).
	/// Only used with incremental reloading. If zero, no extra delay is added.
	/// </summary>
	[Property, Feature( "Ammo" ), ShowIf( nameof( IncrementalReloading ), true )]
	public float FirstShellReloadTime { get; set; } = 0f;

	/// <summary>
	/// Delay before the first shell is inserted during incremental reload.
	/// If zero, uses <see cref="ReloadTime"/>.
	/// </summary>
	[Property, Feature( "Ammo" ), ShowIf( nameof( IncrementalReloading ), true )]
	public float ReloadStartTime { get; set; } = 0f;

	/// <summary>
	/// Can we cancel reloads?
	/// </summary>
	[Property, Feature( "Ammo" )]
	public bool CanCancelReload { get; set; } = true;

	private CancellationTokenSource reloadToken;
	private bool isReloading;

	public bool CanReload()
	{
		if ( !UsesClips ) return false;
		if ( ClipContents >= ClipMaxSize ) return false;
		if ( isReloading ) return false;
		if ( !WeaponConVars.InfiniteReserves && ReserveAmmo <= 0 ) return false;

		return true;
	}

	public bool IsReloading() => isReloading;

	public virtual void CancelReload()
	{
		if ( reloadToken?.IsCancellationRequested == false )
		{
			reloadToken?.Cancel();
			isReloading = false;

			ViewModel?.RunEvent<ViewModel>( x => x.OnReloadCancel() );
		}
	}

	public virtual async void OnReloadStart()
	{
		if ( !CanReload() )
			return;

		CancelReload();

		var cts = new CancellationTokenSource();
		reloadToken = cts;
		isReloading = true;

		try
		{
			await ReloadAsync( cts.Token );
		}
		finally
		{
			// Only clean up our own reload
			if ( reloadToken == cts )
			{
				isReloading = false;
				reloadToken = null;
			}
			cts.Dispose();
		}
	}

	[Rpc.Broadcast]
	private void BroadcastReload()
	{
		if ( !HasOwner ) return;

		Assert.True( Owner.Controller.IsValid(), "BaseWeapon::BroadcastReload - Player Controller is invalid!" );
		Assert.True( Owner.Controller.Renderer.IsValid(), "BaseWeapon::BroadcastReload - Renderer is invalid!" );

		Owner.Controller.Renderer.Set( "b_reload", true );
	}

	protected virtual async Task ReloadAsync( CancellationToken ct )
	{
		// Capture so we can tell if a newer reload has replaced us by the time finally runs.
		var mySource = reloadToken;
		var isFirstShell = ClipContents == 0;

		try
		{
			ViewModel?.RunEvent<ViewModel>( x => x.OnReloadStart() );

			BroadcastReload();

			var firstIteration = true;

			while ( ClipContents < ClipMaxSize && !ct.IsCancellationRequested )
				{
					var delay = (firstIteration && IncrementalReloading && ReloadStartTime > 0f) ? ReloadStartTime : ReloadTime;
					firstIteration = false;
					await Task.DelaySeconds( delay, ct );

					var needed = IncrementalReloading ? 1 : (ClipMaxSize - ClipContents);

					if ( WeaponConVars.InfiniteReserves )
					{
						ViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );
						ClipContents += needed;
					}
					else
					{
						var available = Math.Min( needed, ReserveAmmo );

						if ( available <= 0 )
							break;

						ViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );

						ReserveAmmo -= available;
						ClipContents += available;
					}

					// After the first shell, wait longer before the next one starts
					if ( isFirstShell && FirstShellReloadTime > 0f )
					{
						await Task.DelaySeconds( FirstShellReloadTime, ct );
					}

					isFirstShell = false;
				}
		}
		finally
		{
			if ( reloadToken == mySource )
			{
				ViewModel?.RunEvent<ViewModel>( x => x.OnReloadFinish() );
			}
		}
	}
}