diff --git a/Assets/_Project/Scripts/Client/Connection/GoInGameClientSystem.cs b/Assets/_Project/Scripts/Client/Connection/GoInGameClientSystem.cs index 4e7345cf2..bf974d33f 100644 --- a/Assets/_Project/Scripts/Client/Connection/GoInGameClientSystem.cs +++ b/Assets/_Project/Scripts/Client/Connection/GoInGameClientSystem.cs @@ -29,6 +29,17 @@ namespace ProjectM.Client [BurstCompile] public void OnUpdate(ref SystemState state) { + // A FULL client must not go in-game until the gameplay subscene's ghost prefabs have streamed in. + // Otherwise the server's first ghost snapshot arrives before the client can resolve those prefabs + // ("ghost ... ENTITY_NOT_FOUND" -> the server disconnects the connection -> "nothing loads"). On + // loopback / fast LAN the connect+go-in-game handshake easily beats the ~0.5s entity-subscene stream. + // PlayerSpawner is a subscene-baked singleton that co-loads with the ghost prefabs, so its presence + // is a sound "subscene ready" gate. Thin clients never instantiate ghosts (and don't stream the + // subscene), so they skip the gate and connect immediately. + bool isThinClient = (state.WorldUnmanaged.Flags & WorldFlags.GameThinClient) == WorldFlags.GameThinClient; + if (!isThinClient && !SystemAPI.HasSingleton()) + return; + var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (_, connection) in diff --git a/Assets/_Project/Scripts/Client/UI/MainMenuController.cs b/Assets/_Project/Scripts/Client/UI/MainMenuController.cs index a7b1bb13d..759459b34 100644 --- a/Assets/_Project/Scripts/Client/UI/MainMenuController.cs +++ b/Assets/_Project/Scripts/Client/UI/MainMenuController.cs @@ -21,6 +21,7 @@ namespace ProjectM.Client VisualElement _howToPanel; TextField _ipField; Label _classLabel; + bool _autoStarted; void Awake() { @@ -32,10 +33,12 @@ namespace ProjectM.Client // The menu owns the cursor. UnityEngine.Cursor.lockState = CursorLockMode.None; UnityEngine.Cursor.visible = true; + _autoStarted = TryAutoStartFromCommandLine(); } void OnEnable() { + if (_autoStarted) return; // headless CLI co-op session; don't build the menu UI if (_doc == null) _doc = GetComponent(); var root = _doc.rootVisualElement; if (root == null) return; @@ -43,6 +46,28 @@ namespace ProjectM.Client BuildMain(root); } + /// + /// Dev/automation hook: a player launched with -mhost auto-hosts; -mjoin <ip> auto-joins + /// (ip optional, defaults to loopback). Lets two standalone builds form a co-op session headlessly for + /// testing — the same path the menu buttons use, no UI clicks. + /// No effect when neither arg is present. Returns true if a session was started. + /// + static bool TryAutoStartFromCommandLine() + { + var args = System.Environment.GetCommandLineArgs(); + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "-mhost") { WorldLauncher.StartSession(SessionMode.Host, "", false); return true; } + if (args[i] == "-mjoin") + { + string ip = (i + 1 < args.Length && !args[i + 1].StartsWith("-")) ? args[i + 1] : "127.0.0.1"; + WorldLauncher.StartSession(SessionMode.Join, ip, false); + return true; + } + } + return false; + } + static void EnsureMenuWorld() { var w = World.DefaultGameObjectInjectionWorld;