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.
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&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(),
};
}