Scene startup GameObjectSystem that optionally creates a network lobby when a host session initializes. It reads a console variable nz_autohost to decide whether to call Networking.CreateLobby with an empty LobbyConfig, logs status, and tracks if it auto-hosted.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>
/// HOST A LOBBY WHEN THE GAME STARTS, so the package page's **New Game** button produces something
/// a second player can actually join.
///
/// ⛔ THE MENU DOES NOT CREATE THE LOBBY, AND THAT IS THE WHOLE REASON THIS FILE EXISTS. The
/// create-game dialog on a package page collects a server name, a privacy setting and a player cap,
/// which reads exactly like a thing that is about to host — and it is not. Facepunch's own wording
/// on the three launch modes is *"None of these options will automatically create lobbies for
/// you."* So a published build clicked through **New Game** created no lobby at all: the host
/// landed in a solo session and the other account's **Join Game** showed `0 LOBBIES`.
///
/// Every multiplayer session this project has ever had was started from the EDITOR's network panel,
/// or later by `nz_host_start`. Neither exists for somebody who just installed the game.
///
/// ⚠️ `GameObjectSystem`, NOT A COMPONENT, SO NO SCENE EDIT IS NEEDED. The engine instantiates one
/// per scene on its own. A component would have to be placed in `scenes/nzombies.scene` — and this
/// project's rule is to never rewrite a `.scene` from a script, for good reasons recorded in
/// INSTRUCTIONS.md.
///
/// ⚠️ AN EMPTY `LobbyConfig`, WHICH IS NOT THE SAME AS NO OPINION BY ACCIDENT — IT IS CHECKED.
/// The parameterless `CreateLobby()` is obsolete (CS0618), so this passes a config; the engine's own
/// docs say what an unset one means, and it is exactly what we want: *Privacy* — "this will be
/// public by default"; *MaxPlayers* — "by default, this will be the Max Players set in the current
/// Game Package's project settings"; *Name* — "a default lobby name will be chosen instead".
///
/// So every field we leave alone defers to the package or the menu rather than to us. Setting any
/// of them here would override what the player chose in the create-game dialog with no way for them
/// to tell. `nz_host_start` still takes explicit settings, because there is no dialog behind a
/// console command.
///
/// ⛔ IT DOES NOT SOLVE §9.5 AND MUST NOT BE READ AS DOING SO. A lobby existing from the moment the
/// scene loads means somebody can join BEFORE a map is loaded — and `nz_map_load` disconnects every
/// client. The documented answer is `SceneNetworkSystem.LoadSceneBroadcast`, which we do not use.
/// Until that is done, the working order is still: load the map, THEN let people in. The log below
/// says so out loud rather than leaving it to be rediscovered.
/// </summary>
public sealed class NZStartup : GameObjectSystem<NZStartup>, ISceneStartup
{
public NZStartup( Scene scene ) : base( scene ) { }
/// <summary>
/// Host a lobby automatically on startup. `nz_autohost 0` never, `1` outside the editor
/// (default), `2` always.
///
/// ⛔ IT DEFAULTED TO ON EVERYWHERE AND THAT WAS A REAL REGRESSION, not just noise. The
/// paragraph this replaces already said *"the editor and a published build want opposite
/// things"* — and then defaulted to the published build's answer, which forced every editor
/// Play into the multiplayer code path.
///
/// The cost is not the lobby, it is `Networking.IsActive`. Solo in the editor that used to be
/// FALSE, and a great deal of this project branches on it — above all `NZPlayers.EnsureHostBody`,
/// whose first line is `if ( !Networking.IsActive || NZGame.IsClient ) return;`. With networking
/// off the player simply used the scene's own body. With it on, the body is CLONED, network-
/// spawned, and the scene original destroyed. Zombies find their target through
/// `PlayerSpawner.AllBodies()`, so anything that changes which bodies exist is a zombie-behaviour
/// change wearing a networking hat.
///
/// ⚠️ `2` EXISTS SO THE PUBLISHED PATH IS STILL TESTABLE FROM THE EDITOR. A switch that can
/// only be turned off would mean the thing that actually ships is the thing never exercised here.
/// </summary>
[ConVar( "nz_autohost" )]
public static int AutoHost { get; set; } = 1;
/// <summary>Should this machine host on startup, given where it is running.</summary>
public static bool ShouldAutoHost => AutoHost >= 2 || (AutoHost == 1 && !Game.IsEditor);
/// <summary>Did we host, this session. Reported by `nz_host`.</summary>
public static bool DidAutoHost { get; private set; }
void ISceneStartup.OnHostPreInitialize( SceneFile scene ) { }
void ISceneStartup.OnClientInitialize() { }
void ISceneStartup.OnHostInitialize()
{
if ( !ShouldAutoHost )
{
Log.Info( $"[nz-net] not auto-hosting (nz_autohost {AutoHost}"
+ (Game.IsEditor && AutoHost == 1 ? ", editor" : "")
+ ") — nobody can join until you host."
+ (Game.IsEditor ? " `nz_host_start` when you want a session; `nz_autohost 2` to"
+ " host on Play like a published build does." : "") );
return;
}
// ⚠️ CHECKED EVEN THOUGH THE DOCS' EXAMPLE DOES NOT. Their note says `CreateLobby` handles
// being called while already hosting; that is their word for their code, and a duplicate
// lobby is not a failure mode worth trusting a sentence about. The check costs nothing and
// the log line it produces is the answer to "why did nz_host_start say already networked".
if ( Networking.IsActive )
{
Log.Info( $"[nz-net] already networked at startup — "
+ $"{(Networking.IsHost ? "hosting" : "a client")}, not creating a second lobby." );
return;
}
Networking.CreateLobby( new Sandbox.Network.LobbyConfig() );
DidAutoHost = true;
Log.Info( "[nz-net] hosting — this is what makes the package page's Join Game find us." );
// ⛔ THE MAP WARNING IS THE POINT OF LOGGING AT ALL. A lobby now exists, so somebody can
// join RIGHT NOW — and `nz_map_load` disconnects every client, so a map loaded after they
// arrive throws them straight back out. See SBOX_MULTIPLAYER.md §9.5.
Log.Info( string.IsNullOrEmpty( NZMap.Current )
? "[nz-net] ⛔ NO MAP LOADED. Load one BEFORE anyone joins — nz_map_load kicks clients."
: $"[nz-net] map '{NZMap.Current}' is up — safe for people to join." );
}
}