Diagnostics/ScopeTuning.cs

Runtime console commands for tuning a weapon scope in-game. Finds the active SWB.Base.Weapon in the active scene and exposes ConCmds nz_scope (set/report magnification) and nz_scope_sway (toggle/report scope overlay sway), printing various live scope and camera state to logs.

File Access
using Sandbox;
using System.Linq;

namespace NZombies;

/// <summary>
/// TUNE A 2D SCOPE WHILE LOOKING THROUGH IT. `nz_scope`, `nz_scope_sway`.
///
/// ⛔ SCOPE FEEL IS NOT DERIVABLE, AND GUESSING AT IT COSTS A ROUND TRIP EACH TIME. TFA states a
/// magnification (the 1000 Yard Stare's RTScopeFOV works out to 2.5x) but it renders that into a
/// SMALL CIRCLE while the rest of the screen stays at normal FOV. SWB zooms the whole screen, so
/// the same 2.5 reads as far less magnification than the original. There is no arithmetic that
/// converts one into the other -- it has to be looked at.
///
/// ⚠️ RUNTIME ONLY. ScopeInfo lives on the prefab, which is read-only in game, so these change the
/// live weapon and print the value to be written back into the port. Say the number and it gets
/// baked; do not expect it to survive a respawn.
/// </summary>
public static class ScopeTuning
{
	static SWB.Base.Weapon Active =>
		Game.ActiveScene?.GetAllComponents<SWB.Base.Weapon>().FirstOrDefault( x => x.IsValid() );

	/// <summary>Set (or report) the scope magnification. `nz_scope 4`.</summary>
	[ConCmd( "nz_scope" )]
	public static void Magnification( float x = -1f )
	{
		var w = Active;
		if ( w is null ) { Log.Info( "[scope] no weapon equipped" ); return; }

		if ( !w.Scoping )
		{
			Log.Info( $"[scope] '{w.DisplayName}' is not a scoped weapon (Scoping = false)" );
			return;
		}

		if ( x > 0 )
		{
			w.ScopeInfo.Magnification = x;
			// ⚠️ Magnification only wins while it is ABOVE ZERO — ScopeInfo.FOV is the fallback
			// branch in PlayerCameraHandler, so a leftover FOV would take over the moment this
			// dropped to 0 and the scope would silently jump to a different zoom.
			Log.Info( $"[scope] {w.DisplayName}: magnification -> {x:0.##}x "
				+ $"(FOV {Preferences.FieldOfView / x:0.#} deg at your {Preferences.FieldOfView:0.#})" );
		}
		else
		{
			// ⛔ REPORT THE LIVE STATE, NOT THE PREFAB'S. "The zoom does not work" has four
			// possible causes and reasoning cannot separate them: the weapon may not be scoping,
			// the magnification may not have deserialised, the camera handler may be absent, or it
			// may be running and something downstream is overwriting the FOV. Each is one line here.
			var cam = Game.ActiveScene?.GetAllComponents<SWB.Base.PlayerCameraHandler>()
				.FirstOrDefault( x => x.IsValid() );

			Log.Info( $"[scope] {w.DisplayName}" );
			Log.Info( $"[scope]   Scoping={w.Scoping}  IsScoping={w.IsScoping}  IsAiming={w.IsAiming}" );
			Log.Info( $"[scope]   Magnification={w.ScopeInfo.Magnification:0.###}  "
				+ $"FOV={w.ScopeInfo.FOV:0.#}  Sway={w.ScopeInfo.Sway}" );
			Log.Info( $"[scope]   AimInfo.PlayerFOVZoom={w.AimInfo.PlayerFOVZoom:0.###}  "
				+ $"ShouldTuck={w.ShouldTuckVar}  IsReloading={w.IsReloading}" );
			Log.Info( $"[scope]   cameraHandler={(cam is null ? "MISSING" : (cam.Enabled ? "enabled" : "DISABLED"))}"
				+ $"  weaponOnHandler={(cam?.Weapon is null ? "null" : cam.Weapon.DisplayName)}" );
			Log.Info( $"[scope]   prefs FOV={Preferences.FieldOfView:0.#}  "
				+ $"expected scoped FOV={(w.ScopeInfo.Magnification > 0 ? Preferences.FieldOfView / w.ScopeInfo.Magnification : -1):0.#}" );
			Log.Info( $"[scope]   texture {w.ScopeInfo.ScopeTexture}" );
		}
	}

	/// <summary>Does the overlay drift with look and sway. `nz_scope_sway 0`.</summary>
	[ConCmd( "nz_scope_sway" )]
	public static void Sway( int on = -1 )
	{
		var w = Active;
		if ( w is null ) { Log.Info( "[scope] no weapon equipped" ); return; }

		if ( on >= 0 )
			w.ScopeInfo.Sway = on != 0;

		Log.Info( $"[scope] {w.DisplayName}: sway {w.ScopeInfo.Sway}" );
	}
}