Files
Project-M/Assets/_Project/Scripts/Client/Connection/ConnectionWatchdogSystem.cs
T
2026-07-04 16:57:40 -07:00

92 lines
3.9 KiB
C#

using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
namespace ProjectM.Client
{
/// <summary>
/// Why the last session ended, surfaced once on the main menu (client-local). Set by
/// <see cref="ConnectionWatchdogSystem"/> (host vanished / join never connected) or by the connect path
/// (unparseable address); <see cref="MainMenuController"/> shows + clears it. Statics reset on play-enter
/// (fast-enter-playmode safe, the VFXConfig precedent).
/// </summary>
public static class SessionEnd
{
public static string Reason;
/// <summary>Force an immediate teardown-to-menu (e.g. a bad join address) without waiting for the
/// watchdog's connect timeout. Set <see cref="Reason"/> first.</summary>
public static bool Abort;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void Reset() { Reason = null; Abort = false; }
}
/// <summary>
/// Hands-off co-op fail-safe: watches this client world's connection and returns the player to the main
/// menu (reason on <see cref="SessionEnd"/>) 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
/// <see cref="WorldLauncher.Busy"/>-guarded); (2) a join was requested but never accepted within
/// <see cref="JoinTimeoutSeconds"/> (wrong IP / host not hosting — covers the silent
/// NetworkStreamRequestConnect failure modes); (3) <see cref="SessionEnd.Abort"/> was flagged. Managed
/// observe-only client system — never writes sim state; world disposal runs on the WorldLauncher /
/// SessionRunner frame-boundary path, never here.
/// </summary>
[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<NetworkId>();
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();
}
}
}
}