Console command helpers for the game's easter-egg clue system. Provides commands to place a clue at the player, list configured clues, edit clue text, clear all clues, locate the nearest clue and inspect its panels, and search available clue images.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>
/// Console access to the easter-egg clues. Every setting in the tool panel has an
/// equivalent here, so one can be placed and written without clicking.
/// </summary>
public static class ClueCommands
{
static MapEditor Editor
=> Game.ActiveScene?.GetAllComponents<MapEditor>().FirstOrDefault();
static NZPlayer Player
=> NZPlayer.Local;
/// <summary>Drop one at your feet: `nz_clue [text...]`.
///
/// ⛔ `params string[]`, NOT `string`. A console argument is split on spaces, so
/// `nz_clue THE CODE IS 4 7 2` bound only "THE" and the clue was placed reading one
/// word — and because the board auto-sizes to its text, the RESULT looked exactly like
/// a sizing bug: a small board with clipped writing. The arithmetic was right and the
/// input was truncated.</summary>
[ConCmd( "nz_clue" )]
public static void Place( params string[] words )
{
var ed = Editor;
var p = Player;
if ( !ed.IsValid() || !p.IsValid() ) { Log.Warning( "[nz-clue] no editor/player" ); return; }
var text = words is { Length: > 0 } ? string.Join( " ", words ) : "";
if ( !string.IsNullOrEmpty( text ) ) ed.ClueText = text;
// ⚠️ At the PLAYER's feet with an UP normal — the command exists so this can be
// driven headlessly, and a trace needs somewhere to be aiming. A clue placed this
// way lies flat; use the tool for a wall.
ed.AddClueAt( p.WorldPosition, Vector3.Up );
}
/// <summary>What is placed: `nz_clue_list`.</summary>
[ConCmd( "nz_clue_list" )]
public static void List()
{
var list = ActiveConfig.Current.Clues;
if ( list.Count == 0 )
{
Log.Info( "[nz-clue] none placed — Q > Easter egg > Interactables > Clue, or nz_clue" );
return;
}
for ( int i = 0; i < list.Count; i++ )
{
var c = list[i];
// ⚠️ TEXT AND IMAGE BOTH PRINTED IN FULL. They are the two fields typed by hand,
// and a blank panel in the world is the same sight whether the text was left
// empty or the image path was wrong.
Log.Info( $"[nz-clue] [{i}] {c.Size.x:0}x{c.Size.y:0}u font {c.FontSize:0}"
+ $" text '{c.Text}'"
+ ( string.IsNullOrWhiteSpace( c.Image ) ? "" : $" image '{c.Image}'" ) );
}
var mgr = ClueManager.Instance;
Log.Info( $"[nz-clue] {list.Count} configured, "
+ $"{( mgr.IsValid() ? mgr.Built.ToString() : "?" )} standing" );
}
/// <summary>
/// `nz_clue_text <index> <text...>` — rewrite one clue in place.
///
/// ⚠️ REBUILDS. `CluePanel`'s BuildHash covers the text, so an edit redraws on its own
/// once the object exists — but the config row is a different object after a rebuild
/// elsewhere, and rebuilding here keeps "what the console says" and "what is on the
/// wall" the same thing.
/// </summary>
[ConCmd( "nz_clue_text" )]
public static void SetText( int index, params string[] words )
{
var text = words is { Length: > 0 } ? string.Join( " ", words ) : "";
var list = ActiveConfig.Current.Clues;
if ( index < 0 || index >= list.Count )
{
Log.Warning( $"[nz-clue] no clue [{index}] — there are {list.Count}" );
return;
}
list[index].Text = text;
ClueManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( $"[nz-clue] [{index}] now reads '{text}'" );
}
/// <summary>Remove them all: `nz_clue_clear`.</summary>
[ConCmd( "nz_clue_clear" )]
public static void Clear()
{
var n = ActiveConfig.Current.Clues.Count;
ActiveConfig.Current.Clues.Clear();
ClueManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( $"[nz-clue] removed {n}" );
}
/// <summary>
/// `nz_clue_where` — the nearest clue, what it holds, and whether a panel exists.
///
/// ⛔ IT REPORTS WHETHER THE `WorldPanel` AND `CluePanel` ARE ACTUALLY THERE. This is
/// the project's first world-space UI, so "the clue draws nothing" has one more
/// possible cause than usual: the panel components failing to attach. A blank wall
/// looks identical whether the text is empty, the image path is wrong, or no panel was
/// ever created — and only this separates them.
/// </summary>
[ConCmd( "nz_clue_where" )]
public static void Where()
{
var p = Player;
if ( !p.IsValid() ) { Log.Warning( "[nz-clue] no player" ); return; }
var clue = Clue.Near( p.WorldPosition );
if ( clue is null ) { Log.Info( "[nz-clue] none placed" ); return; }
var wp = clue.Components.Get<WorldPanel>( FindMode.EverythingInSelf );
var cp = clue.Components.Get<CluePanel>( FindMode.EverythingInSelf );
var s = clue.Spot;
Log.Info( $"[nz-clue] nearest is {clue.WorldPosition.Distance( p.WorldPosition ):0}u away" );
Log.Info( $"[nz-clue] text '{s?.Text}'"
+ ( string.IsNullOrWhiteSpace( s?.Image ) ? " (no image)" : $" image '{s.Image}'" ) );
Log.Info( $"[nz-clue] WorldPanel {( wp.IsValid() ? $"ok, {wp.PanelSize}" : "MISSING" )}"
+ $" · CluePanel {( cp.IsValid() ? "ok" : "MISSING" )}" );
if ( s is not null && string.IsNullOrWhiteSpace( s.Text ) && string.IsNullOrWhiteSpace( s.Image ) )
Log.Warning( "[nz-clue] this clue has NO text and NO image — it will draw nothing" );
}
/// <summary>
/// `nz_clue_images [filter]` — what the picker offers, and search the rest of the project.
///
/// ⛔ THE PICKER ONLY LISTS `materials/clues/`, and this is how anything else is found.
/// Scanning the whole project returns 6,515 PNGs — wall and weapon textures, almost none
/// of them a clue — so the tool panel offers a curated folder and the console offers the
/// search. With a filter this looks EVERYWHERE; without one it shows what the picker has.
///
/// ⚠️ Running it drops the cache, so art added while the game is running appears without
/// a restart.
/// </summary>
[ConCmd( "nz_clue_images" )]
public static void Images( string filter = "" )
{
ToolSettings.ForgetClueImages();
if ( string.IsNullOrWhiteSpace( filter ) )
{
var picker = ToolSettings.ClueImages().Where( p => !string.IsNullOrEmpty( p ) ).ToList();
Log.Info( $"[nz-clue] the picker offers {picker.Count} image(s) "
+ $"from {ToolSettings.ClueImageFolder}/" );
foreach ( var p in picker.Take( 40 ) )
Log.Info( $"[nz-clue] {p}" );
if ( picker.Count > 40 )
Log.Info( $"[nz-clue] … and {picker.Count - 40} more" );
if ( picker.Count == 0 )
Log.Info( $"[nz-clue] nothing there yet — drop PNGs in Assets/{ToolSettings.ClueImageFolder}/ "
+ "and run this again. To use art already in the project, pass a filter: "
+ "nz_clue_images skull" );
return;
}
// ⚠️ SEARCHES THE SOURCE PNGs, not the compiled textures. Every texture in this
// project is a `*_png_<hash>.generated.vtex` produced by material compilation, so a
// search over compiled names would return machine-generated strings a mapper has
// never seen and cannot recognise.
var hits = new List<string>();
try
{
foreach ( var f in FileSystem.Mounted.FindFile( "materials", "*.png", true ) )
{
if ( f.Contains( filter, System.StringComparison.OrdinalIgnoreCase ) )
hits.Add( $"materials/{f}" );
}
}
catch ( System.Exception e )
{
Log.Warning( $"[nz-clue] search failed: {e.Message}" );
return;
}
Log.Info( $"[nz-clue] {hits.Count} image(s) matching '{filter}'" );
foreach ( var h in hits.Take( 30 ) )
Log.Info( $"[nz-clue] {h}" );
if ( hits.Count > 30 )
Log.Info( $"[nz-clue] … and {hits.Count - 30} more — narrow the filter" );
if ( hits.Count > 0 )
Log.Info( "[nz-clue] set one with the Image row, or copy it into "
+ $"Assets/{ToolSettings.ClueImageFolder}/ to have it in the picker" );
}
}