Console command helpers for teleporter tooling. Exposes concommands to place teleporters, place one ahead of the player, list configured teleporters, clear them, use a teleporter, check the teleporter model asset, and diagnose why a pad is not usable.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>
/// Console access to the teleporters. Every button in the tool panel has an equivalent here, so a
/// pair can be placed, inspected and ridden without clicking.
/// </summary>
public static class TeleporterCommands
{
static MapEditor Editor
=> Game.ActiveScene?.GetAllComponents<MapEditor>().FirstOrDefault();
static NZPlayer Player
=> NZPlayer.Local;
/// <summary>
/// Place a whole teleporter in one call: `nz_teleporter <bx> <by> <bz> [price]`.
///
/// ⛔ THE PAD IS AT YOUR FEET AND B IS AN EXPLICIT COORDINATE. Nobody can click twice over
/// MCP, so a two-click tool has no console route at all unless one of the two points is given
/// outright — and a command that placed only half a teleporter would leave a pending corner
/// behind for the next click to consume.
/// </summary>
[ConCmd( "nz_teleporter" )]
public static void Place( float bx, float by, float bz, int price = -1 )
{
var ed = Editor;
var p = Player;
if ( !ed.IsValid() || !p.IsValid() ) { Log.Warning( "[nz-tp] no editor/player" ); return; }
if ( price >= 0 ) ed.TeleporterPrice = price;
ed.ResetCorners();
ed.AddTeleporterPointAt( p.WorldPosition );
ed.AddTeleporterPointAt( new Vector3( bx, by, bz ) );
}
/// <summary>
/// Place one that sends you a set distance ahead: `nz_teleporter_ahead [distance] [price]`.
///
/// A convenience over nz_teleporter for testing, where the exact destination does not matter
/// and typing three coordinates does.
/// </summary>
[ConCmd( "nz_teleporter_ahead" )]
public static void PlaceAhead( float distance = 400f, int price = -1 )
{
var p = Player;
if ( !p.IsValid() ) { Log.Warning( "[nz-tp] no player" ); return; }
var ahead = p.WorldPosition + p.WorldRotation.Forward.WithZ( 0 ).Normal * distance;
Place( ahead.x, ahead.y, ahead.z, price );
}
/// <summary>What is placed: `nz_teleporter_list`.</summary>
[ConCmd( "nz_teleporter_list" )]
public static void List()
{
var list = ActiveConfig.Current.Teleporters;
if ( list.Count == 0 )
{
Log.Info( "[nz-tp] none placed — Q > Placeables > Transportation > "
+ "Teleporter, or nz_teleporter_ahead" );
return;
}
for ( int i = 0; i < list.Count; i++ )
{
var t = list[i];
Log.Info( $"[nz-tp] [{i}] {t.A} -> {t.B} {t.A.Distance( t.B ):0,6}u"
+ (t.Price > 0 ? $" {t.Price} points" : " free")
+ $" {t.WarmupTime:0.#}+{t.TransitTime:0.#}s"
+ (t.Cooldown > 0f ? $" {t.Cooldown:0}s cd" : "")
+ (t.RequiresPower ? " ⚡power" : "")
+ $" pad {t.PadSize:0} flag {DoorLinks.Display( t.Link )}" );
}
var mgr = TeleporterManager.Instance;
// ⚠️ CONFIGURED vs STANDING, said separately — a pad whose mesh failed to build leaves a
// spot in the config and nothing in the world.
Log.Info( $"[nz-tp] {list.Count} configured, "
+ $"{(mgr.IsValid() ? mgr.Built.ToString() : "?")} standing" );
}
/// <summary>Remove them all: `nz_teleporter_clear`.</summary>
[ConCmd( "nz_teleporter_clear" )]
public static void Clear()
{
var n = ActiveConfig.Current.Teleporters.Count;
ActiveConfig.Current.Teleporters.Clear();
TeleporterManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( $"[nz-tp] removed {n}" );
}
/// <summary>
/// Ride the pad you are standing on: `nz_teleporter_use`.
///
/// ⚠️ Goes through the same Use the key does, so a refusal here is the refusal a player would
/// get — this is not a shortcut that skips the gates.
/// </summary>
[ConCmd( "nz_teleporter_use" )]
public static void Use()
{
var p = Player;
if ( !p.IsValid() ) { Log.Warning( "[nz-tp] no player" ); return; }
var tp = Teleporter.Near( p.WorldPosition );
if ( tp is null ) { Log.Info( "[nz-tp] not standing at a teleporter pad" ); return; }
Log.Info( $"[nz-tp] {tp.Use( p )}" );
}
/// <summary>
/// Is the pad MODEL usable? `nz_teleporter_model`.
///
/// ⚠️ Asks the asset system, not the config. A pad standing as a plain box has two causes —
/// the model deliberately blanked, or the vmdl missing — and only this tells them apart.
/// </summary>
[ConCmd( "nz_teleporter_model" )]
public static void ModelCheck()
{
var path = new TeleporterSpot().Model;
var m = Model.Load( path );
if ( m is null ) { Log.Warning( $"[nz-tp] {path} — NOT FOUND" ); return; }
if ( m.IsError ) { Log.Warning( $"[nz-tp] {path} — ERROR MODEL (did not compile)" ); return; }
Log.Info( $"[nz-tp] {path} ok — bounds {m.Bounds.Size}, meshes {m.MeshCount}" );
}
/// <summary>
/// Why is the pad under me not offering? `nz_teleporter_why`.
///
/// ⛔ EVERY GATE IN ONE LINE. "Pressing E does nothing" has four unrelated causes here — no
/// pad in range, the power off, the flag shut, or standing beside the pad rather than on it —
/// and from inside the game they all look identical.
/// </summary>
[ConCmd( "nz_teleporter_why" )]
public static void Why()
{
var p = Player;
if ( !p.IsValid() ) { Log.Warning( "[nz-tp] no player" ); return; }
var built = TeleporterManager.Instance;
Log.Info( $"[nz-tp] {ActiveConfig.Current.Teleporters.Count} configured, "
+ $"{(built.IsValid() ? built.Built.ToString() : "no manager")} standing, "
+ $"power {(Power.IsOn ? "ON" : "off")}" );
var tp = Teleporter.Near( p.WorldPosition );
if ( tp is null )
{
var nearest = Teleporter.All.Where( t => t.IsValid() && t.Spot is not null )
.OrderBy( t => p.WorldPosition.Distance( t.Spot.A ) ).FirstOrDefault();
Log.Info( nearest is null
? "[nz-tp] no pads standing at all"
: $"[nz-tp] none in range — nearest is #{nearest.Index} at "
+ $"{p.WorldPosition.Distance( nearest.Spot.A ):0}u, "
+ $"needs {nearest.UseRange:0}u" );
return;
}
var blocked = tp.Unavailable( p );
Log.Info( $"[nz-tp] #{tp.Index}: {(string.IsNullOrEmpty( blocked ) ? "USABLE" : blocked)}"
+ $" · on the pad: {tp.IsOn( p )}"
+ $" · riders {tp.Riders().Count}"
+ $" · price {tp.Price}, you have {p.Points}"
+ (tp.CoolingFor > 0f ? $" · recharging {tp.CoolingFor:0.0}s" : "") );
// ⚠️ THE SEQUENCE STATE IS ITS OWN LINE. "Nothing happened when I pressed E" now has a
// sixth cause — it DID happen and is mid-warmup — and that one looks identical to the
// others from inside the game.
Log.Info( $"[nz-tp] sequence: {(tp.Busy ? $"BUSY, {tp.PhaseLeft:0.0}s left, {tp.Aboard} aboard" : "idle")}"
+ $" · overlay {(TeleportOverlayState.Active ? $"frame {TeleportOverlayState.Frame}" : "off")}" );
}
}