Diagnostics/MaterialTint.cs

Console command helper that finds materials whose resource path matches a given fragment and dials their additive tint (g_vColorTint) for debugging; prints guidance for persisting the change into the original .vmt $color2. It walks active scene renderers and adjusts materials, with optional per-channel arguments.

File Access
using Sandbox;
using System.Linq;

namespace NZombies;

/// <summary>
/// Dial an additive material's brightness while looking at it. `nz_tint <match> <value>`.
///
/// ⛔ ON AN ADDITIVE SURFACE, BRIGHTNESS IS OPACITY. It adds to what is behind it, so there is no
/// separate transparency to turn down -- "more see-through" and "dimmer" are the same control, and
/// that control is $color2, which Source authored as a plain multiplier. Brya's Love states [5 5 5].
///
/// ⚠️ RUNTIME ONLY, and it edits the MATERIAL, which every object sharing it also sees. That is
/// exactly what makes it useful for dialling a value in and exactly why it must be written back to
/// the .vmt-generated .vmat afterwards rather than left as a session tweak.
/// </summary>
public static class MaterialTint
{
	/// <summary>
	/// `nz_tint nemesisstarglass 0.15` — set the colour tint on every loaded material whose path
	/// matches. One value tints grey, three keep the hue.
	///
	/// ⛔ ONE VALUE CHANGES THE COLOUR, NOT JUST THE BRIGHTNESS, and on a tinted sight that is not
	/// what anyone means by "more transparent". The Nemesis Star's glass states [0.3 0.3 0] -- zero
	/// blue is what makes it read as yellow-green -- so `nz_tint nemesisstarglass 0.15` would push
	/// blue up from 0 to 0.15 and wash the sight grey while appearing to do the right thing.
	///
	/// ⚠️ So r/g/b are separate, and g/b default to "same as r" only when they are not given at all.
	/// `nz_tint reticle 1` therefore still means what it did.
	/// </summary>
	[ConCmd( "nz_tint" )]
	public static void Set( string match = "", float value = -1f,
		float g = float.NaN, float b = float.NaN )
	{
		if ( string.IsNullOrWhiteSpace( match ) )
		{
			Log.Info( "[tint] nz_tint <path fragment> <r> [g] [b]" );
			Log.Info( "[tint]   nz_tint reticle 1              grey/uniform" );
			Log.Info( "[tint]   nz_tint nemesisstarglass .15 .15 0   keeps the hue" );
			return;
		}

		// ⚠️ NaN, not -1, as "not given" — 0 is a legitimate channel value and the Nemesis Star's
		// blue is exactly 0, so a sentinel inside the valid range would be unusable here.
		var gg = float.IsNaN( g ) ? value : g;
		var bb = float.IsNaN( b ) ? value : b;

		// ⚠️ Walk the RENDERERS in the scene rather than a material registry: what we want is the
		// material actually on screen, and a path fragment is how a human refers to it.
		var mats = Game.ActiveScene?.GetAllComponents<Renderer>()
			.SelectMany( r => MaterialsOf( r ) )
			.Where( m => m is not null && m.ResourcePath is not null
				&& m.ResourcePath.Contains( match, System.StringComparison.OrdinalIgnoreCase ) )
			.Distinct()
			.ToList();

		if ( mats is null || mats.Count == 0 )
		{
			Log.Info( $"[tint] nothing loaded matches '{match}'" );
			return;
		}

		foreach ( var m in mats )
		{
			if ( value >= 0 )
				m.Set( "g_vColorTint", new Vector4( value, gg, bb, 0f ) );
			Log.Info( $"[tint] {m.ResourcePath}"
				+ (value >= 0 ? $"  -> g_vColorTint [{value:0.###} {gg:0.###} {bb:0.###} 0]" : "") );
		}

		// ⛔ PRINT THE .vmt LINE, NOT JUST THE VALUE. A tint dialled in here dies with the session,
		// and the .vmat carries a "do not hand-edit" banner because the converter regenerates it --
		// so the number has to go back into the SOURCE .vmt as $color2 or the next sweep erases it.
		if ( value >= 0 )
			Log.Info( $"[tint] {mats.Count} material(s) set. To keep it, put this in the .vmt:"
				+ $"   $color2 \"[{value:0.###} {gg:0.###} {bb:0.###}]\"" );
	}

	static System.Collections.Generic.IEnumerable<Material> MaterialsOf( Renderer r )
	{
		if ( r is ModelRenderer mr )
		{
			if ( mr.MaterialOverride is not null )
				yield return mr.MaterialOverride;
			var model = mr.Model;
			if ( model is not null )
			{
				foreach ( var m in model.Materials )
					yield return m;
			}
		}
	}
}