Zombies/FlameDump.cs

Developer console command that dumps runtime state of a burning-zombie ParticleEffect. Finds the ParticleEffect by GameObject name, logs its config and then prints up to N live particles with selected named Particle fields formatted for compact output.

File Access
using Sandbox;
using System.Linq;

namespace NZombies;

/// <summary>
/// Dump the LIVE particle state of the burning-zombie flame, so it can be looked at
/// without a screenshot.
///
/// ⛔ THIS EXISTS BECAUSE SCREENSHOTS COULD NOT ANSWER THE QUESTION. `LookAtCamera`
/// sprites orient to the PRIMARY camera, so a screenshot taken from any other camera
/// photographs the light and misses the flame entirely — several shots were spent
/// diagnosing a particle that was rendering correctly the whole time. And a one-shot
/// emitter made "visible" depend on WHEN the shot was taken rather than on what was
/// set. Numbers do not have either problem.
///
/// ⚠️ AND IT DOCUMENTS `Particle`, WHICH NOTHING ELSE DOES. Only five of its members
/// appear in Sandbox.Engine.xml. Reflection would have printed the shape for free but
/// s&amp;box's whitelist forbids `System.Reflection` outright (`SB1000`), so these names
/// were recovered by scanning the assembly's string table and then confirmed by the
/// compiler accepting them: `Position`, `Velocity`, `Radius`, `Color`, `Alpha`, `Angles`,
/// `BornTime`, `DeathTime`, `Sequence`, `Frame`.
/// </summary>
public static class FlameDump
{
	/// <summary>The GameObject name StatusEffects gives its flame.</summary>
	const string FlameObject = "nz_status_flame";

	/// <summary>
	/// `nz_flame_dump [count]` — print the effect's config and the live particles.
	///
	/// ⚠️ One line per particle, in a fixed `key=value` shape, so the output can be
	/// parsed straight out of the console rather than read by eye.
	/// </summary>
	[ConCmd( "nz_flame_dump" )]
	public static void DumpCmd( int count = 10 )
	{
		var eff = Game.ActiveScene?
			.GetAllComponents<ParticleEffect>()
			.FirstOrDefault( e => e.IsValid() && e.GameObject.IsValid()
				&& e.GameObject.Name == FlameObject );

		if ( !eff.IsValid() )
		{
			Log.Info( "[nz-flame] no live flame — nz_spawn 1 then nz_status burn 900" );
			return;
		}

		var ren = eff.Components.Get<ParticleSpriteRenderer>( FindMode.EverythingInSelf );
		var emi = eff.Components.Get<ParticleConeEmitter>( FindMode.EverythingInSelf );

		Log.Info( $"[nz-flame] cfg  max={eff.MaxParticles}  live={eff.Particles.Count}"
			+ $"  scale={eff.Scale}  lifetime={eff.Lifetime}"
			+ $"  renderScale={(ren.IsValid() ? ren.Scale : -1f)}"
			+ $"  additive={(ren.IsValid() ? ren.Additive : false)}"
			+ $"  rate={(emi.IsValid() ? emi.Rate : default)}"
			+ $"  duration={(emi.IsValid() ? emi.Duration : -1f)}" );

		Log.Info( $"[nz-flame] origin {eff.WorldPosition}" );

		// ⛔ NAMED FIELDS, NOT REFLECTION. `System.Reflection` is blocked by s&box's
		// whitelist (`SB1000: FieldInfo.GetValue is not allowed`), so the shape has to be
		// written out. These names came from scanning Sandbox.Engine.dll's string table,
		// because only five of `Particle`'s members are documented in the xml.
		var shown = 0;
		foreach ( var p in eff.Particles )
		{
			if ( shown >= count ) break;
			Log.Info( $"[nz-flame] p{shown}"
				+ $"  pos={Fmt( p.Position )}"
				+ $"  local={Fmt( p.Position - eff.WorldPosition )}"
				+ $"  vel={Fmt( p.Velocity )}"
				+ $"  radius={Fmt( p.Radius )}"
				+ $"  col={Fmt( p.Color )}"
				+ $"  alpha={Fmt( p.Alpha )}"
				+ $"  angles={Fmt( p.Angles )}"
				+ $"  born={Fmt( p.BornTime )}"
				+ $"  death={Fmt( p.DeathTime )}"
				+ $"  seq={p.Sequence}"
				+ $"  frame={Fmt( p.Frame )}" );
			shown++;
		}

		if ( shown == 0 )
			Log.Info( "[nz-flame] zero live particles — the emitter is not producing any" );
	}

	/// <summary>
	/// ⚠️ Vectors and colours print with far too many digits by default, and a dump of
	/// ten particles times fifteen fields becomes unreadable. Trim to what can be judged
	/// by eye; anything needing more precision is a different question.
	/// </summary>
	static string Fmt( object v ) => v switch
	{
		null => "null",
		float f => f.ToString( "0.###" ),
		Vector3 v3 => $"({v3.x:0.#},{v3.y:0.#},{v3.z:0.#})",
		Color c => $"({c.r:0.##},{c.g:0.##},{c.b:0.##},{c.a:0.##})",
		Angles a => $"({a.pitch:0.#},{a.yaw:0.#},{a.roll:0.#})",
		Rotation r => $"rot({r.Yaw():0.#})",
		GameObject g => g.IsValid() ? g.Name : "<invalid>",
		_ => v.ToString(),
	};
}