92 lines
3.9 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|
|
}
|