using Unity.Entities; using Unity.NetCode; using UnityEngine; namespace ProjectM.Client { /// /// Why the last session ended, surfaced once on the main menu (client-local). Set by /// (host vanished / join never connected) or by the connect path /// (unparseable address); shows + clears it. Statics reset on play-enter /// (fast-enter-playmode safe, the VFXConfig precedent). /// public static class SessionEnd { public static string Reason; /// Force an immediate teardown-to-menu (e.g. a bad join address) without waiting for the /// watchdog's connect timeout. Set first. public static bool Abort; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { Reason = null; Abort = false; } } /// /// Hands-off co-op fail-safe: watches this client world's connection and returns the player to the main /// menu (reason on ) instead of leaving them in a dead world with a live HUD. /// Exits: (1) the accepted connection VANISHED on a pure join client — host quit or the network died (the /// host's own in-proc client is excluded: its server outliving user-driven teardown is /// -guarded); (2) a join was requested but never accepted within /// (wrong IP / host not hosting — covers the silent /// NetworkStreamRequestConnect failure modes); (3) was flagged. Managed /// observe-only client system — never writes sim state; world disposal runs on the WorldLauncher / /// SessionRunner frame-boundary path, never here. /// [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] public partial class ConnectionWatchdogSystem : SystemBase { const float JoinTimeoutSeconds = 12f; bool _sawConnection; float _unconnectedElapsed; protected override void OnUpdate() { if (WorldLauncher.Busy) return; // a start/teardown routine owns world lifecycle right now if (SessionEnd.Abort) { SessionEnd.Abort = false; WorldLauncher.TeardownToMenu(); return; } // An ACCEPTED connection carries NetworkId (at most one per client world). bool accepted = SystemAPI.HasSingleton(); if (accepted) { _sawConnection = true; _unconnectedElapsed = 0f; return; } var serverWorld = ClientServerBootstrap.ServerWorld; bool pureJoiner = serverWorld == null || !serverWorld.IsCreated; if (_sawConnection) { _sawConnection = false; if (pureJoiner) { SessionEnd.Reason = "CONNECTION LOST — the host ended the session."; Debug.Log("[ConnectionWatchdog] " + SessionEnd.Reason); // build-log trace for headless smokes WorldLauncher.TeardownToMenu(); } // Host/single: losing the loopback connection outside teardown is transient noise; re-arm. return; } // Never accepted yet — only a real remote join gets the timeout escape hatch. if (!pureJoiner) return; _unconnectedElapsed += SystemAPI.Time.DeltaTime; if (_unconnectedElapsed >= JoinTimeoutSeconds) { SessionEnd.Reason = "COULD NOT REACH HOST — check the IP and that the host is hosting."; Debug.Log("[ConnectionWatchdog] " + SessionEnd.Reason); // build-log trace for headless smokes WorldLauncher.TeardownToMenu(); } } } }