Net/NZStartup.cs

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.

NetworkingFile Access
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." );
	}
}