Editor/CollaborationSession.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Editor;
using Sandbox;
using Sandbox.Helpers;
using Sandbox.Internal;
using Sandbox.Services;
using Sandbox.Utility;

namespace TeamCreate;

public enum SessionState
{
    Offline,
    Ready,
    Hosting,
    Joining,
    Connected,
    Degraded,
    Recovering
}

public sealed class CollaborationSession : EditorEvent.ISceneView, EditorEvent.IEventListener
{
    private sealed class Tracked
    {
        public SceneEditorSession Session;

        public Dictionary<string, ObjectState> Known;

        public Packet Pending;

        public bool Ready;

        public Dictionary<string, Change> Group = new Dictionary<string, Change>();

        public Stack<List<Change>> Undo = new Stack<List<Change>>();

        public bool UndoPending;

        public JsonObject Settings;

        public Dictionary<string, ObjectState> LastOrder;

        public DateTime DiskStamp;

        public DateTime StampMismatchSince;
    }

    private sealed class StartPlan
    {
        public bool Host;

        public string Address;

        public int Port;

        public string Name;

        public string Token;

        public string SteamId;

        public string SteamToken;

        public bool AllowUnverifiedPeers;

        public bool ZeroConfig;

        public byte[] SessionKey;

        public byte[] Secret;

        public List<EndpointResolver.EndpointCandidate> Candidates;
    }

    private enum HotloadArchiveOutcome
    {
        NotApplicable,
        Archived,
        Failed
    }

    private sealed class EditScope(CollaborationSession owner) : IDisposable
    {
        private bool disposed;

        public void Dispose()
        {
            if (!disposed)
            {
                disposed = true;
                owner.continuousEdits--;
            }
        }
    }

    private readonly Dictionary<string, Tracked> scenes = new Dictionary<string, Tracked>();

    private Wire wire;

    private SessionServer server;

    private Task<Wire> connecting;

    private byte[] activeSessionKey;

    private string activeAddress;

    private int activePort;

    private Task<string> reconnectTokenTask;

    private bool reconnectQueued;

    private string reconnectAddress;

    private int reconnectPort;

    private bool reconnectRecovery;

    private bool reconnectRetrySpent;

    private bool endedByLeave;

    private LanDiscovery discovery;

    private StunClient stun;

    private RelayHost relayHost;

    private bool soloHarness;

    private readonly HeldPacketBuffer heldPackets = new HeldPacketBuffer();

    private bool inviteCarriesRelay;

    /// <summary>The relay this session uses: the project's own if it names one, none if it says "off", otherwise the one built into the library.</summary>
    private string EffectiveRelayUrl()
    {
        return RelayDefaults.Resolve(sessionConfig?.RelayUrl, RelaySetupRules.Fallback(RelaySetup.LocalRelay, RelayDefaults.DefaultUrl));
    }

    /// <summary>What the project's own settings say about the relay (an address, "off", or nothing), for the Relay card.</summary>
    public string ConfiguredRelayUrl => sessionConfig?.RelayUrl;

    private byte[] activeSecret;

    private List<EndpointResolver.EndpointCandidate> activeCandidates;

    private UpnpPortMapper upnp;

    private string inviteCodeValue;

    private string projectIdValue;

    private bool projectIdResolved;

    private byte[] hostSecret;

    private int hostPort;

    private bool stunEndpointFolded;

    private CancellationTokenSource hostVerificationLifetime = new CancellationTokenSource();

    private readonly ConnectionTeardownCoordinator lifecycle = new ConnectionTeardownCoordinator();

    private string baseline;

    private string project;

    private string userName;

    private string sessionToken;

    private string sourceBaseline;

    private string hostSession;

    private string steamIdValue;

    private string steamTokenValue;

    private DateTime nextScan;

    private DateTime nextPresence;

    private DateTime lastPresenceSent;

    private DateTime lastPingSent;

    private string lastPresenceFingerprint;

    private DateTime lastReceive;

    private CollaborationConfig sessionConfig;

    private OrgDirectory orgDirectory;

    private string orgIdentValue;

    private string libraryVersionValue;

    private string projectManifestValue;

    private Task<Package> packageFetch;

    private bool? packageReachable;

    private DateTime nextReadiness;

    private DateTime nextPackageFetch;

    private int readinessOrgRevision = -1;

    private const int PackageRefreshSeconds = 30;

    private const int AutoSaveCheckSeconds = 1;

    private bool restoring;

    private int continuousEdits;

    private bool saving;

    private string focusedPeer;

    private DateTime focusedUntil;

    private DateTime focusNoticeExpires;

    private DateTime saveNoticeExpires;

    private DateTime lastAutoSave;

    private DateTime lastLocalEdit;

    private DateTime nextAutoSaveCheck;

    private bool autoRecover;

    private DateTime undoNoticeExpires;

    private DateTime actionNoticeExpires;

    private SessionFailureClass errorClass = SessionFailureClass.Terminal;

    private int recoverAttempts;

    private DateTime recoverNextAttempt;

    private bool recoverPromptShown;

    private bool recoverArmed;

    private bool recoverDeclined;

    private bool recoverArming;

    private string startAddress;

    private int startPort;

    private readonly HashSet<string> seen = new HashSet<string>();

    private const float PromptToastSeconds = 15f;

    private const float OutcomeToastSeconds = 6f;

    public const string NativeUndoEntryName = "Collaboration Undo";

    private const int ToolTextLimit = 40;

    private const int StateTextLimit = 32;

    private const int SceneTextLimit = 96;

    private const int ObjectNameTextLimit = 64;

    public static CollaborationSession Current { get; } = new CollaborationSession();

    public string Status { get; private set; } = "Offline";

    public string Error { get; private set; }

    public bool Connected { get; private set; }

    public SessionState State
    {
        get
        {
            string terminalTeardownError = TerminalTeardownError;
            if (Connected)
            {
                if (Error != null || terminalTeardownError != null)
                {
                    return SessionState.Degraded;
                }
                return IsHost ? SessionState.Hosting : SessionState.Connected;
            }
            if (terminalTeardownError != null)
            {
                return SessionState.Degraded;
            }
            if (Status.StartsWith("Reconnecting", StringComparison.Ordinal))
            {
                return SessionState.Recovering;
            }
            if (Busy)
            {
                return IsHost ? SessionState.Hosting : SessionState.Joining;
            }
            return ReadinessRules.CanStart(Readiness) ? SessionState.Ready : SessionState.Offline;
        }
    }

    public bool IsHost => server != null;

    public CollaborationConfig Config => sessionConfig;

    public OrgDirectory OrganizationDirectory => orgDirectory;

    public string InviteCode => inviteCodeValue;

    public IReadOnlyList<DiscoveredHost> DiscoveredHosts => discovery?.Hosts;

    public string PublicEndpointStatus { get; private set; }

    /// <summary>How this editor is connected: the route that actually carried the join, for the dock to show.</summary>
    public string RouteDescription { get; private set; }

    /// <summary>
    /// TEST-ONLY SEAM. Nothing in this library sets it; only the workspace's editor probe does. When set, and ONLY inside a
    /// disposable test copy (a folder under <c>.collaboration/test-copies/</c> that carries the marker file the solo-test
    /// launcher writes), the host lets it vouch for a guest hello instead of asking Steam's service, and treats that guest as
    /// a friend and organization member. It exists so one person with one Steam account can run a full two-editor session.
    /// In any other folder it is ignored, so a real project cannot be weakened by it.
    /// </summary>
    public static Func<Packet, bool> SoloGuestVoucher { get; set; }

    /// <summary>
    /// The facts a guest must present in its hello, read back so an out-of-process test collaborator can present the same ones.
    /// They are not secrets: the project id, baseline hash and manifest hash travel in every hello, and the organization and library
    /// version are printed in the dock.
    /// </summary>
    public (string ProjectId, string Baseline, string Manifest, string OrgIdent, string LibraryVersion) HandshakeFacts => (project, baseline, projectManifestValue, orgIdentValue, libraryVersionValue);

    /// <summary>Scene paths this session has subscribed and received a snapshot for. Empty until the host's scene is live.</summary>
    public IReadOnlyList<string> ReadyScenes => scenes.Where((KeyValuePair<string, Tracked> x) => x.Value.Ready).Select((KeyValuePair<string, Tracked> x) => x.Key).ToList();

    /// <summary>True in a disposable test copy with the voucher installed. Shown to the user, so it is never a silent state.</summary>
    public bool SoloHarness => soloHarness;

    /// <summary>The visible banner for a session running with the test seam active. Null otherwise.</summary>
    public string SoloHarnessNotice => soloHarness ? "Solo test copy: one reserved test identity is accepted here in place of a second Steam account. This never applies to a real project." : null;

    private static bool ComputeSoloHarness()
    {
        if (SoloGuestVoucher == null)
        {
            return false;
        }
        string root = SceneAdapter.Root;
        bool marker = false;
        try
        {
            marker = !string.IsNullOrEmpty(root) && File.Exists(Path.Combine(root, ".collaboration", TestHarnessRules.MarkerFile));
        }
        catch (Exception)
        {
            marker = false;
        }
        return TestHarnessRules.IsDisposableCopy(root, marker);
    }

    private bool SoloMayVouch(Packet hello)
    {
        try
        {
            return soloHarness && hello != null && SoloGuestVoucher != null && SoloGuestVoucher(hello);
        }
        catch (Exception)
        {
            return false;
        }
    }

    /// <summary>The host's relay registration, worded for the dock. Null when no relay is configured.</summary>
    public string RelayStatus
    {
        get
        {
            RelayHost host = relayHost;
            if (host == null || host.State == RelayHostState.Stopped || host.State == RelayHostState.Idle)
            {
                return null;
            }
            switch (host.State)
            {
                case RelayHostState.Ready:
                    return "Relay connected — friends can join from any network.";
                case RelayHostState.Failed:
                    return "Relay unavailable — " + host.Failure;
                default:
                    return host.Status ?? "Connecting to the relay…";
            }
        }
    }

    public bool RelayFailed => relayHost?.State == RelayHostState.Failed;

    /// <summary>Set while hosting with no relay a friend elsewhere could reach, so the dock can say so instead of leaving a silent gap. Null otherwise.</summary>
    public string NoRelayNotice => IsHost && (relayHost == null || relayHost.State == RelayHostState.Stopped) ? RelayDefaults.NoRelayNotice(EffectiveRelayUrl(), sessionConfig?.RelayUrl) : null;

    /// <summary>What source synchronization is doing this frame; the dock reads it to explain a pause.</summary>
    public PlaySpectateDecision PlayDecision { get; private set; }

    public bool PausedForPlay => Connected && PlayDecision == PlaySpectateDecision.PausedForPlay;

    public int HeldSceneChanges => heldPackets.Count;

    /// <summary>The sentence shown while the local editor is playing and scene edits are held back. Null otherwise.</summary>
    public string PlayNotice
    {
        get
        {
            if (!PausedForPlay)
            {
                return null;
            }
            int held = heldPackets.Count;
            return held == 0
                ? "Playing: scene edits are paused. Your collaborators can still see you; changes resume when you stop."
                : "Playing: scene edits are paused. " + held + " change" + (held == 1 ? "" : "s") + " from collaborators will apply when you stop.";
        }
    }

    public string PortMappingStatus => upnp?.Status;

    public bool PortMappingFailed
    {
        get
        {
            UpnpPortMapper upnpPortMapper = upnp;
            return upnpPortMapper != null && upnpPortMapper.Current == UpnpPortMapper.State.Failed;
        }
    }

    public string DiscoveryStatus => discovery?.Failure;

    public bool Busy => CollaborationSessionResourceRules.IsBusy(wire != null, connecting != null, server != null, lifecycle.IsFailStop);

    public PlayModeLifecycle RuntimeLifecycle { get; } = new PlayModeLifecycle(optIn: false, "source-known-state");

    public PlayModeLifecycleEventState RuntimeLifecycleEvents { get; } = new PlayModeLifecycleEventState();

    public bool ExperimentalRuntimeOptIn
    {
        get
        {
            return RuntimeLifecycle.OptIn;
        }
        set
        {
            PlayModeTransitionResult playModeTransitionResult = RuntimeLifecycle.SetOptIn(value);
            if (!playModeTransitionResult.Accepted)
            {
                SetLifecycleError(playModeTransitionResult.ActionableError);
            }
        }
    }

    public string PlayModeStatus
    {
        get
        {
            PlayModeLifecycle runtimeLifecycle = RuntimeLifecycle;
            if (1 == 0)
            {
            }
            if (runtimeLifecycle == null)
            {
                goto IL_0059;
            }
            switch (runtimeLifecycle.State)
            {
            case PlayModeLifecycleState.Source:
                break;
            case PlayModeLifecycleState.RuntimeDisabled:
                goto IL_0039;
            case PlayModeLifecycleState.RuntimeStarting:
                goto IL_0041;
            case PlayModeLifecycleState.RuntimeActive:
                goto IL_0049;
            case PlayModeLifecycleState.RuntimeBlocked:
                goto IL_0051;
            default:
                goto IL_0059;
            }
            string result = "Source collaboration";
            goto IL_0061;
            IL_0051:
            result = "Runtime collaboration blocked • recovery required";
            goto IL_0061;
            IL_0059:
            result = "Source collaboration";
            goto IL_0061;
            IL_0061:
            if (1 == 0)
            {
            }
            return result;
            IL_0039:
            result = "Source collaboration • runtime disabled";
            goto IL_0061;
            IL_0041:
            result = "Source collaboration • starting runtime";
            goto IL_0061;
            IL_0049:
            result = "Experimental runtime collaboration enabled • transport and apply unsupported";
            goto IL_0061;
        }
    }

    public string RuntimeLifecycleDiagnostic => RuntimeLifecycle.LastDiagnostic;

    public string RuntimeLifecycleError => RuntimeLifecycle.LastActionableError;

    public bool HasPendingLifecycleCandidate => (object)RuntimeLifecycleEvents.Pending != null;

    public Dictionary<string, Packet> Roster { get; } = new Dictionary<string, Packet>();

    public string Peer { get; } = Guid.NewGuid().ToString();

    public long AcceptedOperations { get; private set; }

    public int DrawnMarkers { get; private set; }

    public int DrawnSelections { get; private set; }

    public string FocusNotice { get; private set; }

    public string RecoveryPath { get; private set; }

    public IReadOnlyList<ReadinessItem> Readiness { get; private set; } = Array.Empty<ReadinessItem>();

    public string ActiveSceneSupport
    {
        get
        {
            SceneEditorSession supportedActiveScene = SupportedActiveScene;
            return (supportedActiveScene != null) ? ("Active scene: " + SceneAdapter.PathOf(supportedActiveScene)) : "This view is outside the supported edit set (use a saved scene under Assets/scenes).";
        }
    }

    private static SceneEditorSession SupportedActiveScene
    {
        get
        {
            SceneEditorSession active = SceneEditorSession.Active;
            object result;
            if (active != null && !active.IsPrefabSession && !active.IsPlaying)
            {
                string text = SceneAdapter.PathOf(active);
                if (text != null && text.StartsWith("scenes/"))
                {
                    result = active;
                    goto IL_0036;
                }
            }
            result = null;
            goto IL_0036;
            IL_0036:
            return (SceneEditorSession)result;
        }
    }

    public string RecoveryNotice { get; private set; }

    public string SaveNotice { get; private set; }

    public bool AutoSaveIdle { get; set; }

    public bool AutoRecover
    {
        get
        {
            return autoRecover;
        }
        set
        {
            if (autoRecover != value)
            {
                autoRecover = value;
                if (value)
                {
                    recoverAttempts = 0;
                    recoverNextAttempt = default;
                    recoverDeclined = false;
                }
            }
        }
    }

    public string UndoNotice { get; private set; }

    public string ActionNotice { get; private set; }

    public string TerminalTeardownError => lifecycle.TerminalDiagnostic;

    public bool ReconnectRecovery => reconnectRecovery;

    private CollaborationSession()
    {
        EditorEvent.Register(this);
    }

    private void SetLifecycleError(string error)
    {
        if (!string.IsNullOrWhiteSpace(error))
        {
            RecoveryNotice = BoundedDiagnostic(error);
            GlobalGameNamespace.Log.Error("Collaboration lifecycle: " + RecoveryNotice);
        }
    }

    private void ApplyLifecycleResult(PlayModeTransitionResult result)
    {
        if (!result.Accepted)
        {
            SetLifecycleError(result.ActionableError);
        }
    }

    [Event("scene.startplay")]
    public static void OnStartPlay()
    {
        CollaborationSession current = Current;
        current.Checkpoint();
        SceneEditorSession active = SceneEditorSession.Active;
        SceneEditorSession sceneEditorSession = ((active != null && !active.IsPrefabSession) ? active : null);
        string text = ((sceneEditorSession == null) ? null : SceneAdapter.PathOf(sceneEditorSession));
        if (sceneEditorSession == null || !RuntimeStateRules.IsSafeScenePath(text))
        {
            current.SetLifecycleError("Play mode could not identify a saved source scene; source collaboration is unchanged.");
        }
        else
        {
            current.ApplyLifecycleResult(current.RuntimeLifecycleEvents.RecordStartIntent(current.RuntimeLifecycle, sceneEditorSession, text, Guid.NewGuid().ToString("N")));
        }
    }

    [Event("scene.play")]
    public static void OnPlay()
    {
        CollaborationSession current = Current;
        current.Checkpoint();
        GameEditorSession[] array = SceneEditorSession.All.OfType<GameEditorSession>().ToArray();
        if (array.Length != 1)
        {
            current.SetLifecycleError((array.Length == 0) ? "Play-mode lifecycle could not find a GameEditorSession in SceneEditorSession.All; source collaboration is unchanged." : "Multiple GameEditorSession objects are active; runtime collaboration was not enabled. Stop play mode and retry.");
            return;
        }
        GameEditorSession gameEditorSession = array[0];
        SceneEditorSession parent = gameEditorSession.Parent;
        string a = SceneAdapter.PathOf(parent) ?? parent?.Scene?.Source?.ResourcePath;
        PlayModePendingCandidate pending = current.RuntimeLifecycleEvents.Pending;
        if ((object)pending == null || parent != pending.SourceSessionToken || !string.Equals(a, pending.SourceScenePath, StringComparison.OrdinalIgnoreCase))
        {
            current.SetLifecycleError("Runtime GameEditorSession parent/path did not match the source captured by scene.startplay; runtime collaboration was not enabled.");
            return;
        }
        PlayModeTransitionResult result = current.RuntimeLifecycleEvents.AttachRuntime(current.RuntimeLifecycle, RuntimeSceneAdapter.Describe(gameEditorSession), pending.RuntimeRunId);
        current.ApplyLifecycleResult(result);
    }

    [Event("scene.stop")]
    public static void OnStop()
    {
        CollaborationSession current = Current;
        current.ApplyLifecycleResult(current.RuntimeLifecycleEvents.RecordStop(current.RuntimeLifecycle));
    }

    [Event("scene.saved")]
    public static void OnSceneSaved(Scene scene)
    {
        Current.ReconcileSavedScene(scene);
    }

    private void ReconcileSavedScene(Scene scene)
    {
        SceneEditorSession sceneEditorSession = SceneEditorSession.All.FirstOrDefault((SceneEditorSession x) => x.Scene == scene) ?? SceneEditorSession.Active;
        string text = SceneAdapter.PathOf(sceneEditorSession);
        Tracked tracked = null;
        DateTime dateTime = default;
        if (text != null && scenes.TryGetValue(text, out var value) && value.Session == sceneEditorSession)
        {
            tracked = value;
            dateTime = SceneAdapter.DiskStamp(text);
        }
        bool flag = Connected && Error == null;
        switch (SceneSaveRules.Decide(saving || (tracked != null && tracked.DiskStamp == dateTime), flag, tracked != null, flag && tracked != null && CanSave()))
        {
        case SceneSaveOutcome.Absorb:
            tracked.DiskStamp = dateTime;
            server?.AllowSavedBaseline(SceneAdapter.Baseline());
            SetSaveNotice("The scene was saved outside the session; the session accepted the save.");
            break;
        case SceneSaveOutcome.Warn:
            tracked.DiskStamp = dateTime;
            SetSaveNotice("The scene was saved outside the session while edits were in flight; disk and session state may differ.");
            break;
        }
    }

    private void SetSaveNotice(string text)
    {
        string text2 = BoundedDiagnostic(text);
        if (!string.Equals(SaveNotice, text2, StringComparison.Ordinal) || !(DateTime.UtcNow < saveNoticeExpires))
        {
            SaveNotice = text2;
            saveNoticeExpires = DateTime.UtcNow.AddSeconds(8.0);
        }
    }

    [EditorEvent.Hotload]
    public static void CodeChanged()
    {
        CollaborationSession current = Current;
        PlayModeTransitionResult playModeTransitionResult = current.RuntimeLifecycle.BlockForHotload("hotloaded");
        current.ApplyLifecycleResult(playModeTransitionResult);
        HotloadArchiveOutcome hotloadArchiveOutcome = current.ArchiveHotloadLifecycle(current.RuntimeLifecycle.ArchiveReason ?? "hotloaded");
        bool busy = current.Busy;
        HotloadCodeChangeReport hotloadCodeChangeReport = HotloadCodeChangeRules.Decide(new HotloadCodeChangeRequest(current.RuntimeLifecycle.State, playModeTransitionResult.Accepted, hotloadArchiveOutcome == HotloadArchiveOutcome.Archived, busy, playModeTransitionResult.Accepted ? null : playModeTransitionResult.ActionableError));
        if (hotloadCodeChangeReport.SurfaceError)
        {
            current.Fail(hotloadCodeChangeReport.Message);
        }
        if (!hotloadCodeChangeReport.ArchiveClaimed)
        {
            current.RecoveryNotice = CollaborationDiagnostics.Bounded(hotloadCodeChangeReport.LifecycleRejectionReason ?? current.RuntimeLifecycle.LastDiagnostic);
        }
        if (hotloadCodeChangeReport.TeardownTransport)
        {
            current.DisposeActiveResources();
        }
        current.BeginConnectionTeardown(hotloadCodeChangeReport.RequiresHotloadRecoveryAfterLeave);
    }

    private static string BoundedDiagnostic(string value)
    {
        return CollaborationDiagnostics.Bounded(value);
    }

    public void Start(bool host, string address, int port, string name, string token, string steamId = null, string steamToken = null, bool allowUnverifiedPeers = false)
    {
        StartCore(new StartPlan
        {
            Host = host,
            Address = address,
            Port = port,
            Name = name,
            Token = token,
            SteamId = steamId,
            SteamToken = steamToken,
            AllowUnverifiedPeers = allowUnverifiedPeers
        });
    }

    private void StartCore(StartPlan plan)
    {
        activeAddress = null;
        activePort = 0;
        soloHarness = ComputeSoloHarness();
        if (soloHarness && LocalSteamIdentity.TryGet(out var soloLocal, out var _, out var _))
        {
            // The reserved guest borrows the signed-in account's picture so its marker is not an empty ring.
            AvatarCache.SyntheticAvatarAlias = SteamIdentityRules.Canonical(soloLocal);
        }
        else
        {
            AvatarCache.SyntheticAvatarAlias = null;
        }
        PlayModeTransitionResult playModeTransitionResult = lifecycle.ValidateStart(new SessionStartGuardRequest(RuntimeLifecycle.ValidateSessionStart(), Busy, lifecycle.IsPending, lifecycle.HasTerminalFailure, lifecycle.TerminalDiagnostic));
        if (!playModeTransitionResult.Accepted)
        {
            throw new InvalidOperationException(playModeTransitionResult.ActionableError);
        }
        if (!plan.ZeroConfig && (string.IsNullOrWhiteSpace(plan.Token) || plan.Token.Length < 12))
        {
            throw new InvalidOperationException("Use a session key of at least 12 characters");
        }
        if (plan.ZeroConfig && !plan.Host && plan.SessionKey == null)
        {
            throw new InvalidOperationException("A zero-config join requires the session key derived from the invite code.");
        }
        if (string.IsNullOrWhiteSpace(plan.Name) || plan.Name.Length > 64)
        {
            throw new InvalidOperationException("No display name is available for the signed-in Steam account");
        }
        foreach (SceneEditorSession item in SceneEditorSession.All.Where((SceneEditorSession x) => !(x is GameEditorSession)))
        {
            if (item.HasUnsavedChanges)
            {
                throw new InvalidOperationException("Save or revert local scenes before starting a new collaboration session");
            }
        }
        string path = Path.Combine(SceneAdapter.Root, "ProjectSettings", "collaboration.json");
        if (plan.Host)
        {
            // The first Host click on a new project writes the shared settings itself: a fresh project id and the library's own gates, all on.
            // Only the host does this; a friend joining a project that has no settings is told to get the host's copy, since a project id they
            // invented would never match.
            SetupResult setup = CollaborationSetup.EnsureConfig(path, Project.Current?.Config?.Org, LibraryInfo.DefaultLibraryIdent(Project.Current?.Config?.Org), LibraryInfo.LibraryVersion);
            if (setup.Error != null)
            {
                throw new InvalidOperationException(setup.Error);
            }
            if (setup.Created)
            {
                ActionNotice = "Created ProjectSettings/collaboration.json for this project. Your friend needs the same file: send them the project as it is now.";
                actionNoticeExpires = DateTime.UtcNow.AddSeconds(30.0);
            }
        }
        if (!File.Exists(path))
        {
            throw new InvalidOperationException("This project has no ProjectSettings/collaboration.json. Ask the host for a copy of the project, so both of you have the same file.");
        }
        JsonNode jsonNode = JsonNode.Parse(File.ReadAllText(path))?["ProjectId"];
        if (jsonNode == null || !Guid.TryParse(jsonNode.GetValue<string>(), out var _))
        {
            throw new InvalidOperationException("ProjectId must be a GUID");
        }
        string preparedProject = jsonNode.GetValue<string>();
        string preparedBaseline = SceneAdapter.Baseline();
        string text = SceneAdapter.Baseline(includeScenes: false);
        CollaborationConfig collaborationConfig = CollaborationConfig.LoadFrom(path, out var _);
        UseConfig(collaborationConfig);
        if (plan.ZeroConfig && plan.Host)
        {
            PrepareHostSecret(plan, collaborationConfig);
        }
        string org = collaborationConfig.Org;
        string preparedLibraryVersion = LibraryInfo.LibraryVersion;
        string preparedManifest = ProjectManifest.Compute(Project.Current.RootDirectory.FullName);
        try
        {
            RefreshReadiness();
        }
        catch (Exception error2)
        {
            Fail("Readiness could not be evaluated: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error2));
            return;
        }
        string text2 = (ReadinessRules.CanStart(Readiness) ? null : (ReadinessRules.BlockReason(Readiness) ?? "Hosting is unavailable: the readiness checklist could not be evaluated."));
        if (text2 != null)
        {
            Fail(text2);
            return;
        }
        SessionServer preparedServer = null;
        Task<Wire> preparedConnect = null;
        int boundPort = plan.Port;
        string dialAddress = ((plan.Host && plan.Address == "0.0.0.0") ? "127.0.0.1" : plan.Address);
        PlayModeTransitionResult playModeTransitionResult2 = lifecycle.BeginStart(new SessionStartCommitRequest(playModeTransitionResult, () =>
        {
            if (plan.Host)
            {
                preparedServer = OpenHostServer(plan, preparedProject, preparedBaseline, out boundPort);
                preparedServer.AllowUnverifiedPeers = plan.AllowUnverifiedPeers;
                preparedServer.peerFacts = BuildPeerFacts;
                preparedServer.config = sessionConfig;
                preparedServer.hostLibraryVersion = preparedLibraryVersion;
                preparedServer.hostManifest = preparedManifest;
            }
            List<EndpointResolver.EndpointCandidate> candidates2 = plan.Candidates;
            preparedConnect = ((candidates2 != null && candidates2.Count > 0) ? ConnectCandidates(plan.Candidates, plan.SessionKey, plan.Secret) : Wire.Connect(dialAddress, boundPort, plan.SessionKey, plan.SessionKey != null));
            return true;
        }));
        if (!playModeTransitionResult2.Accepted)
        {
            if (preparedServer != null)
            {
                ResourceDisposalResult resourceDisposalResult = ResourceDisposalRules.DisposePair(null, "collaboration transport", preparedServer, "collaboration host server");
                if (!resourceDisposalResult.Succeeded)
                {
                    RecordTerminalTeardownFailure(resourceDisposalResult.Failure);
                }
            }
            connecting = preparedConnect;
            BeginConnectionTeardown(recoverWhenComplete: false);
            throw new InvalidOperationException(playModeTransitionResult2.ActionableError);
        }
        project = preparedProject;
        baseline = preparedBaseline;
        userName = plan.Name;
        sessionToken = plan.Token;
        activeSessionKey = plan.SessionKey;
        activeSecret = plan.Secret;
        activeCandidates = plan.Candidates;
        RouteDescription = null;
        List<EndpointResolver.EndpointCandidate> candidates = plan.Candidates;
        if (candidates == null || candidates.Count <= 0)
        {
            activeAddress = dialAddress;
            activePort = boundPort;
        }
        startAddress = dialAddress;
        startPort = boundPort;
        steamIdValue = plan.SteamId;
        steamTokenValue = plan.SteamToken;
        orgIdentValue = org;
        libraryVersionValue = preparedLibraryVersion;
        projectManifestValue = preparedManifest;
        sourceBaseline = text;
        hostSession = null;
        reconnectQueued = false;
        reconnectTokenTask = null;
        reconnectRecovery = false;
        reconnectRetrySpent = false;
        scenes.Clear();
        Roster.Clear();
        seen.Clear();
        Error = null;
        RecoveryNotice = null;
        // A path from an earlier session would otherwise sit under a new failure and point at an archive that has
        // nothing to do with it.
        RecoveryPath = null;
        restoring = false;
        errorClass = SessionFailureClass.Terminal;
        recoverAttempts = 0;
        recoverNextAttempt = default;
        recoverPromptShown = false;
        recoverArmed = false;
        recoverDeclined = false;
        SaveNotice = null;
        saveNoticeExpires = default;
        UndoNotice = null;
        undoNoticeExpires = default;
        ActionNotice = null;
        actionNoticeExpires = default;
        lastAutoSave = default;
        lastLocalEdit = default;
        nextAutoSaveCheck = default;
        endedByLeave = false;
        server = preparedServer;
        connecting = preparedConnect;
        if (plan.ZeroConfig && plan.Host)
        {
            BeginHostConnectivity(plan, boundPort);
        }
        Status = "Connecting…";
    }

    private void PrepareHostSecret(StartPlan plan, CollaborationConfig config)
    {
        if (string.IsNullOrEmpty(config.ProjectId))
        {
            throw new InvalidOperationException("ProjectSettings/collaboration.json must carry a shared ProjectId before a session can be hosted.");
        }
        plan.Secret = TeamCreate.InviteCode.NewSecret();
        plan.SessionKey = TeamCreate.InviteCode.DeriveKey(plan.Secret, config.ProjectId);
        plan.Token = Convert.ToHexString(plan.SessionKey);
    }

    private SessionServer OpenHostServer(StartPlan plan, string project, string baseline, out int boundPort)
    {
        int num = ((!plan.ZeroConfig) ? 1 : 5);
        SocketException error = null;
        for (int i = 0; i < num; i++)
        {
            int num2 = plan.Port + i;
            if (num2 > 65535)
            {
                break;
            }
            string text = Path.Combine(SceneAdapter.Root, ".collaboration", "journal-" + Guid.NewGuid().ToString() + ".jsonl");
            try
            {
                SessionServer result = new SessionServer(plan.Address, num2, project, baseline, plan.Token, text, null, (Packet hello) => SoloMayVouch(hello) ? Task.FromResult(SteamTokenVerdict.Verified) : new SteamTokenHttpVerifier(SandboxHttpTokenTransport.Default).VerifyAsync(hello, hostVerificationLifetime.Token), plan.SessionKey);
                boundPort = num2;
                return result;
            }
            catch (SocketException ex)
            {
                error = ex;
                try
                {
                    File.Delete(text);
                }
                catch
                {
                }
            }
        }
        boundPort = plan.Port;
        throw new IOException(CollaborationDiagnostics.DescribeExternalFailure((num == 1) ? $"Port {plan.Port} could not be opened: " : $"None of the {num} ports from {plan.Port} could be opened: ", error));
    }

    private async Task<Wire> ConnectCandidates(IReadOnlyList<EndpointResolver.EndpointCandidate> candidates, byte[] sessionKey, byte[] secret)
    {
        List<RouteAttempt> attempts = new List<RouteAttempt>();
        foreach (EndpointResolver.EndpointCandidate candidate in candidates)
        {
            try
            {
                Wire reached;
                if (candidate.Source == RelayProtocol.RouteSource)
                {
                    if (secret == null || sessionKey == null)
                    {
                        throw new InvalidOperationException("The relay route needs the invite code's secret.");
                    }
                    if (!soloHarness && !RelayProtocol.IsPublicRelay(candidate.Address) && !IsHost)
                    {
                        // A code naming localhost was made on the host's own computer. Dialling it here reaches THIS computer, so say so instead of
                        // reporting a raw socket error that sends the person looking for a network problem that does not exist.
                        throw new RelayRefusedException(RelayProtocol.LoopbackCode, "the code points to a relay on the host's own computer");
                    }
                    reached = await RelayGuest.ConnectAsync(() => new SandboxRelaySocket(), candidate.Address, secret, sessionKey);
                    RouteDescription = "Connected through the relay (" + RelayHostName(candidate.Address) + ")";
                }
                else
                {
                    reached = await Wire.Connect(candidate.Address, candidate.Port, sessionKey, sessionKey != null);
                    RouteDescription = candidate.Source == "lan" ? "Connected on the local network" : "Connected directly to " + candidate.Address + ":" + candidate.Port;
                }
                activeAddress = candidate.Address;
                activePort = candidate.Port;
                return reached;
            }
            catch (Exception ex)
            {
                string target = candidate.Source == RelayProtocol.RouteSource ? candidate.Address : candidate.Address + ":" + candidate.Port;
                attempts.Add(new RouteAttempt(candidate.Source, target, ex.Message, (ex as RelayRefusedException)?.Code));
            }
        }
        string summary = RouteFailureRules.Summarize(attempts);
        // A wrong invite secret is refused by the relay and cannot succeed on a second try; every other
        // route failure is a network fault that recovery may legitimately retry.
        bool terminal = attempts.Any((RouteAttempt a) => a.RelayCode != null && RelayProtocol.IsTerminal(a.RelayCode));
        if (terminal)
        {
            throw new InvalidOperationException(summary);
        }
        throw new IOException(summary);
    }

    private static string RelayHostName(string url)
    {
        return Uri.TryCreate(url, UriKind.Absolute, out Uri uri) ? uri.Host : "relay";
    }

    private void BeginHostConnectivity(StartPlan plan, int port)
    {
        hostSecret = plan.Secret;
        hostPort = port;
        PublicEndpointStatus = "Public endpoint pending…";
        stunEndpointFolded = false;
        stun = new StunClient();
        inviteCodeValue = BuildInviteCode(port);
        StartDiscovery();
        if (discovery != null)
        {
            string text = projectManifestValue;
            if (text != null && text.Length == 8)
            {
                try
                {
                    discovery.StartHost(port, Convert.ToHexString(hostSecret).ToLowerInvariant(), projectManifestValue);
                }
                catch (Exception error)
                {
                    GlobalGameNamespace.Log.Error("Collaboration discovery: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error));
                }
            }
        }
        upnp = new UpnpPortMapper();
        try
        {
            upnp.Map(port);
        }
        catch (Exception error2)
        {
            GlobalGameNamespace.Log.Error("Collaboration port mapping: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error2));
        }
        StartRelayHost(plan, port);
    }

    private void StartRelayHost(StartPlan plan, int port)
    {
        string relayUrl = EffectiveRelayUrl();
        if (string.IsNullOrWhiteSpace(relayUrl) || plan.Secret == null || plan.SessionKey == null)
        {
            return;
        }
        string accessKey = ReadRelayAccessKey();
        try
        {
            relayHost = new RelayHost(() => new SandboxRelaySocket(), relayUrl, plan.Secret, plan.SessionKey, accessKey);
            if (server != null)
            {
                server.Relay = relayHost;
            }
            relayHost.Start();
            inviteCarriesRelay = true;
            inviteCodeValue = BuildInviteCode(port);
        }
        catch (Exception error)
        {
            relayHost?.Dispose();
            relayHost = null;
            inviteCarriesRelay = false;
            GlobalGameNamespace.Log.Error("Collaboration relay: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error));
        }
    }

    // The access key is a credential, so it comes from an untracked local file and never from the shared,
    // committed project settings. A missing file is normal (a relay that needs no key); an unusable one is logged
    // by name and treated as absent, which the relay then answers with its own refusal.
    private static string ReadRelayAccessKey()
    {
        try
        {
            string path = Path.Combine(SceneAdapter.Root, ".collaboration", RelayAccessKeyFile.FileName);
            if (!File.Exists(path))
            {
                return null;
            }
            string key = RelayAccessKeyFile.Parse(File.ReadAllText(path), out string problem);
            if (problem != null)
            {
                GlobalGameNamespace.Log.Error("Collaboration relay: " + problem);
            }
            return key;
        }
        catch (Exception error)
        {
            GlobalGameNamespace.Log.Error("Collaboration relay: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("The access key file could not be read: ", error));
            return null;
        }
    }

    private string BuildInviteCode(int port)
    {
        return TeamCreate.InviteCode.Encode(new InviteData
        {
            Port = port,
            ProjectId = sessionConfig.ProjectId,
            Secret = hostSecret,
            Endpoints = EndpointsFor(stun?.PublicEndPoint),
            Relay = inviteCarriesRelay ? RelayProtocol.InviteRelayFor(EffectiveRelayUrl(), allowLoopback: soloHarness) : null
        });
    }

    private static List<byte[]> EndpointsFor(string address)
    {
        if (!IPAddress.TryParse(address, out IPAddress address2) || address2.AddressFamily != AddressFamily.InterNetwork)
        {
            return null;
        }
        return new List<byte[]> { address2.GetAddressBytes() };
    }

    public void StartFromInvite(string code, string name, string steamId = null, string steamToken = null, bool allowUnverifiedPeers = false, string advancedAddress = null, int advancedPort = 0)
    {
        string text = CurrentProjectId();
        if (string.IsNullOrEmpty(text))
        {
            throw new InvalidOperationException("Create ProjectSettings/collaboration.json with a shared ProjectId, then commit it to Git");
        }
        if (!TeamCreate.InviteCode.TryDecode(code, text, out var data, out var error))
        {
            throw new InvalidOperationException(error);
        }
        StartPlan startPlan = JoinPlan(data, text, name, steamId, steamToken, allowUnverifiedPeers, advancedAddress, advancedPort);
        if (startPlan.Candidates.Count == 0)
        {
            throw new InvalidOperationException("No address to try: this code carries no public address and discovery has not found that host on your network. Type the host's address under Advanced.");
        }
        StartCore(startPlan);
    }

    public void StartFromDiscovery(DiscoveredHost host, string name, string steamId = null, string steamToken = null, bool allowUnverifiedPeers = false)
    {
        if ((object)host == null)
        {
            throw new InvalidOperationException("Select a discovered host to join.");
        }
        string text = CurrentProjectId();
        if (string.IsNullOrEmpty(text))
        {
            throw new InvalidOperationException("Create ProjectSettings/collaboration.json with a shared ProjectId, then commit it to Git");
        }
        byte[] array;
        try
        {
            array = Convert.FromHexString(host.SecretHex ?? string.Empty);
        }
        catch (FormatException)
        {
            array = null;
        }
        if (array == null || array.Length != 16)
        {
            throw new InvalidOperationException("That host did not report a usable session secret.");
        }
        byte[] array2 = TeamCreate.InviteCode.DeriveKey(array, text);
        StartCore(new StartPlan
        {
            Host = false,
            Address = host.EndPoint,
            Port = host.Port,
            Name = name,
            Token = Convert.ToHexString(array2),
            SteamId = steamId,
            SteamToken = steamToken,
            AllowUnverifiedPeers = allowUnverifiedPeers,
            ZeroConfig = true,
            SessionKey = array2,
            Secret = array,
            Candidates = new List<EndpointResolver.EndpointCandidate>
            {
                new EndpointResolver.EndpointCandidate(host.EndPoint, host.Port, "lan")
            }
        });
    }

    public void StartHosted(int port, string name, string steamId = null, string steamToken = null, bool allowUnverifiedPeers = false)
    {
        StartCore(new StartPlan
        {
            Host = true,
            Address = "0.0.0.0",
            Port = port,
            Name = name,
            SteamId = steamId,
            SteamToken = steamToken,
            AllowUnverifiedPeers = allowUnverifiedPeers,
            ZeroConfig = true
        });
    }

    private StartPlan JoinPlan(InviteData invite, string projectId, string name, string steamId, string steamToken, bool allowUnverifiedPeers, string advancedAddress, int advancedPort)
    {
        byte[] array = TeamCreate.InviteCode.DeriveKey(invite.Secret, projectId);
        return new StartPlan
        {
            Host = false,
            Address = advancedAddress,
            Port = invite.Port,
            Name = name,
            Token = Convert.ToHexString(array),
            SteamId = steamId,
            SteamToken = steamToken,
            AllowUnverifiedPeers = allowUnverifiedPeers,
            ZeroConfig = true,
            SessionKey = array,
            Secret = invite.Secret,
            Candidates = EndpointResolver.Order(invite, DiscoveredHosts, advancedAddress, advancedPort).ToList()
        };
    }

    private string CurrentProjectId()
    {
        if (!string.IsNullOrEmpty(sessionConfig?.ProjectId))
        {
            return sessionConfig.ProjectId;
        }
        if (projectIdResolved)
        {
            return projectIdValue;
        }
        projectIdResolved = true;
        return projectIdValue = LoadConfig().ProjectId;
    }

    public void StartDiscovery()
    {
        if (discovery != null)
        {
            return;
        }
        try
        {
            string text = CurrentProjectId();
            if (!string.IsNullOrEmpty(text))
            {
                discovery = new LanDiscovery(DiscoveryProtocol.ProjectHash8(text));
            }
        }
        catch (Exception error)
        {
            GlobalGameNamespace.Log.Error("Collaboration discovery: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error));
        }
    }

    public void StopDiscovery()
    {
        discovery?.Dispose();
        discovery = null;
    }

    private void PollConnectivity()
    {
        discovery?.Poll();
        if (discovery != null && server == null && wire == null && connecting == null)
        {
            discovery.SendQuery();
        }
        stun?.Poll();
        FoldPublicEndpoint();
        if (relayHost != null && inviteCarriesRelay && relayHost.State == RelayHostState.Failed && inviteCodeValue != null)
        {
            // A code that names a relay this host has given up on would send friends to a room that will
            // never exist. Re-issue it without the relay so the direct routes are all it promises.
            inviteCarriesRelay = false;
            inviteCodeValue = BuildInviteCode(hostPort);
        }
        upnp?.Poll();
        if (upnp != null && upnp.Finished)
        {
            upnp.Dispose();
            upnp = null;
        }
    }

    private void FoldPublicEndpoint()
    {
        if (stun != null && inviteCodeValue != null && !stunEndpointFolded && (stun.PublicEndPoint != null || stun.Failure != null))
        {
            stunEndpointFolded = true;
            if (stun.PublicEndPoint != null)
            {
                PublicEndpointStatus = "Public endpoint: " + stun.PublicEndPoint;
                inviteCodeValue = BuildInviteCode(hostPort);
            }
            else
            {
                PublicEndpointStatus = CollaborationDiagnostics.Bounded("No public address found; invitees on this network join by code or LAN discovery, and a remote invitee needs the host's address under Advanced.");
            }
        }
    }

    private void TeardownConnectivity()
    {
        StopAdvertising();
        upnp?.Dispose();
        upnp = null;
        stun?.Dispose();
        stun = null;
        relayHost?.Dispose();
        relayHost = null;
        inviteCarriesRelay = false;
        inviteCodeValue = null;
        hostSecret = null;
        hostPort = 0;
        PublicEndpointStatus = null;
        stunEndpointFolded = false;
        StopDiscovery();
    }

    private void StopAdvertising()
    {
        discovery?.StopHost();
        upnp?.Unmap();
        // A failed or ended session must not keep taking guests through the relay either.
        relayHost?.Dispose();
    }

    private PlayModeTransitionResult ReconnectGuard()
    {
        CollaborationTransportState value = CollaborationTransportStateRules.Detect(wire != null, Connected && Error == null, wire?.StoredFailure != null || Error != null, connecting != null);
        return lifecycle.ValidateReconnect(new SessionReconnectGuardRequest(RuntimeLifecycle, baseline != null, connecting != null, wire != null, lifecycle.IsPending, lifecycle.HasTerminalFailure, lifecycle.TerminalDiagnostic, Connected && Error == null, value, sourceBaseline == SceneAdapter.Baseline(includeScenes: false), endedByLeave, IsHost));
    }

    public void Reconnect(string address, int port)
    {
        if (!recoverArming)
        {
            recoverArmed = false;
        }
        if (!reconnectQueued && reconnectTokenTask == null)
        {
            PlayModeTransitionResult playModeTransitionResult = ReconnectGuard();
            if (!playModeTransitionResult.Accepted)
            {
                throw new ActionRefusal(playModeTransitionResult.ActionableError ?? "Recovery is not possible in the session's current state.");
            }
            reconnectRecovery = true;
            reconnectRetrySpent = false;
            reconnectAddress = activeAddress ?? address;
            reconnectPort = ((activePort != 0) ? activePort : port);
            reconnectQueued = true;
            Status = "Reconnecting; preparing identity…";
            RequestReconnectToken();
        }
    }

    private void RequestReconnectToken()
    {
        if (reconnectTokenTask != null)
        {
            return;
        }
        try
        {
            reconnectTokenTask = Auth.GetToken("sbox-editor-collaboration");
        }
        catch (Exception)
        {
            AbandonReconnect("A Steam identity token could not be requested.");
        }
    }

    private void PollReconnect()
    {
        if (reconnectTokenTask != null)
        {
            if (!reconnectTokenTask.IsCompleted)
            {
                return;
            }
            string text = null;
            try
            {
                text = reconnectTokenTask.GetAwaiter().GetResult();
            }
            catch (Exception)
            {
            }
            finally
            {
                reconnectTokenTask = null;
            }
            if (string.IsNullOrWhiteSpace(text))
            {
                AbandonReconnect("A Steam identity token could not be obtained.");
            }
            else if (reconnectQueued)
            {
                RunReconnect(text);
            }
        }
        else if (reconnectQueued)
        {
            AbandonReconnect("A Steam identity token could not be obtained.");
        }
        else if (reconnectRecovery && !reconnectRetrySpent && Error != null && SteamIdentityAdmission.IsTokenRejection(Error))
        {
            reconnectRetrySpent = true;
            ClearRecordedFailure();
            reconnectQueued = true;
            Status = "Reconnecting; preparing identity…";
            RequestReconnectToken();
        }
    }

    private void AbandonReconnect(string reason)
    {
        reconnectQueued = false;
        reconnectTokenTask = null;
        reconnectRecovery = false;
        ClearRecordedFailure();
        Fail(reason);
    }

    private void RunReconnect(string freshToken)
    {
        reconnectQueued = false;
        PlayModeTransitionResult playModeTransitionResult = ReconnectGuard();
        if (!playModeTransitionResult.Accepted)
        {
            AbandonReconnect(playModeTransitionResult.ActionableError ?? "Reconnect is not possible in the session's current state.");
            return;
        }
        SessionReconnectCommitOutcome sessionReconnectCommitOutcome = lifecycle.BeginReconnect(new SessionReconnectCommitRequest(playModeTransitionResult, () =>
        {
            Preserve("Reconnect: local state before authoritative recovery");
            return DisposeActiveResources();
        }));
        if (!sessionReconnectCommitOutcome.Accepted)
        {
            AbandonReconnect((sessionReconnectCommitOutcome.Stage == ReconnectCommitStage.FailedDuringPrepare) ? (sessionReconnectCommitOutcome.ActionableError ?? "Reconnect preparation failed after the previous connection was closed.") : (sessionReconnectCommitOutcome.ActionableError ?? "Reconnect was refused before any connection was closed."));
            return;
        }
        Error = null;
        Connected = false;
        restoring = true;
        foreach (Tracked value in scenes.Values)
        {
            value.Ready = false;
            if (value.Session.Scene.IsValid())
            {
                value.Session.Reload();
                value.Settings = value.Session.Scene.SerializeProperties();
                value.DiskStamp = SceneAdapter.DiskStamp(SceneAdapter.PathOf(value.Session));
            }
        }
        steamTokenValue = freshToken;
        connecting = (activeCandidates != null && activeCandidates.Count > 0)
            ? ConnectCandidates(activeCandidates, activeSessionKey, activeSecret)
            : Wire.Connect(reconnectAddress, reconnectPort, activeSessionKey, activeSessionKey != null);
        Status = "Reconnecting; recovering authoritative state…";
    }

    public void Leave()
    {
        PlayModeTransitionResult playModeTransitionResult = lifecycle.ValidateLeave(new SessionLeaveGuardRequest(RuntimeLifecycle.IsTransitionInProgress));
        if (!playModeTransitionResult.Accepted)
        {
            throw new InvalidOperationException(playModeTransitionResult.ActionableError);
        }
        SceneEditorSession active = SceneEditorSession.Active;
        if (active == null || !active.IsPlaying)
        {
            Checkpoint();
        }
        bool archiveFailed = RuntimeLifecycle.State == PlayModeLifecycleState.RuntimeBlocked && ArchiveHotloadLifecycle(RuntimeLifecycle.ArchiveReason ?? "hotloaded") == HotloadArchiveOutcome.Failed;
        SessionLeaveOutcome sessionLeaveOutcome = SessionLeaveRules.Run(new SessionLeaveRequest(lifecycle, RuntimeLifecycle.IsTransitionInProgress, () => !archiveFailed && scenes.Values.Any((Tracked x) => x.Pending != null || (x.Ready && x.Session.Scene.IsValid() && StateRules.Diff(x.Known, SceneAdapter.Capture(x.Session.Scene)).Count > 0)), (string reason) =>
        {
            Preserve(reason);
            return true;
        }, () =>
        {
            wire?.Send(new Packet
            {
                Kind = "leave"
            });
        }, DisposeActiveResources));
        if (!sessionLeaveOutcome.Accepted)
        {
            throw new InvalidOperationException(sessionLeaveOutcome.ActionableError);
        }
        reconnectQueued = false;
        reconnectTokenTask = null;
        reconnectRecovery = false;
        reconnectRetrySpent = false;
        heldPackets.Clear();
        PlayDecision = PlaySpectateDecision.Normal;
        endedByLeave = true;
        ClearRecordedFailure();
        ActionNotice = null;
        actionNoticeExpires = default;
        ClearNativeUndoMirrors();
        recoverArmed = false;
        recoverAttempts = 0;
        recoverNextAttempt = default;
        recoverPromptShown = false;
        recoverDeclined = false;
        bool disposalFailed = sessionLeaveOutcome.DisposalFailed;
        if (sessionLeaveOutcome.PreserveFailed && !disposalFailed)
        {
            RecoveryNotice = sessionLeaveOutcome.Failure;
            GlobalGameNamespace.Log.Error("Collaboration lifecycle: " + RecoveryNotice);
        }
        Connected = false;
        Roster.Clear();
        foreach (Tracked value in scenes.Values)
        {
            value.Ready = false;
        }
        bool requiresHotloadRecoveryAfterLeave = RuntimeLifecycle.RequiresHotloadRecoveryAfterLeave;
        if (connecting != null)
        {
            BeginConnectionTeardown(requiresHotloadRecoveryAfterLeave);
        }
        if (disposalFailed)
        {
            Status = "Offline • connection teardown failed; session remains fail-stop";
            RecordTerminalTeardownFailure(lifecycle.TerminalDiagnostic);
            return;
        }
        if (archiveFailed)
        {
            Status = "Offline • hotload recovery failed";
            RecoveryNotice = BoundedDiagnostic(RuntimeLifecycle.LastDiagnostic);
            GlobalGameNamespace.Log.Error("Collaboration lifecycle: " + RecoveryNotice);
            return;
        }
        if (lifecycle.IsPending)
        {
            Status = (requiresHotloadRecoveryAfterLeave ? "Offline • waiting for connection teardown before hotload recovery" : "Offline • waiting for pending connection teardown");
            return;
        }
        if (lifecycle.HasTerminalFailure)
        {
            Status = "Offline • connection teardown failed; runtime remains blocked";
            return;
        }
        Status = "Offline";
        string error = null;
        if (requiresHotloadRecoveryAfterLeave)
        {
            CompleteHotloadRecovery(out error);
        }
        if (error != null)
        {
            SetLifecycleError(error);
        }
    }

    public void Fail(string error, SessionFailureClass classification = SessionFailureClass.Terminal)
    {
        if (Error != null)
        {
            return;
        }
        string recoveryWriteFailure = null;
        bool everConnected = hostSession != null;
        if (CollaborationFailureRules.ShouldArchive(everConnected, scenes.Values.Any((Tracked x) => x.Pending != null)))
        {
            try
            {
                Preserve(error);
            }
            catch (Exception error2)
            {
                recoveryWriteFailure = CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("", error2);
            }
        }
        CollaborationFailureOutcome collaborationFailureOutcome = CollaborationFailureRules.Compose(Error, error, recoveryWriteFailure, everConnected);
        Error = collaborationFailureOutcome.Error;
        Status = collaborationFailureOutcome.Status;
        errorClass = classification;
        GlobalGameNamespace.Log.Error("Collaboration: " + Error);
        try
        {
            StopAdvertising();
        }
        catch (Exception error3)
        {
            GlobalGameNamespace.Log.Error("Collaboration connectivity: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error3));
        }
    }

    public void ReportAction(Exception error)
    {
        if (error != null)
        {
            if (!ActionRefusalRules.IsRefusal(error))
            {
                Fail(error.Message);
            }
            else
            {
                SetActionNotice(error.Message);
            }
        }
    }

    public void ClearRecordedFailure()
    {
        if (Error != null && lifecycle.TerminalDiagnostic == null)
        {
            Error = null;
            errorClass = SessionFailureClass.Terminal;
        }
    }

    private HotloadArchiveOutcome ArchiveHotloadLifecycle(string reason)
    {
        if (RuntimeLifecycle.State != PlayModeLifecycleState.RuntimeBlocked)
        {
            return HotloadArchiveOutcome.NotApplicable;
        }
        try
        {
            Preserve("Runtime lifecycle hotload marker: " + reason);
            PlayModeTransitionResult playModeTransitionResult = lifecycle.ApplyArchiveMarker(RuntimeLifecycle.RecordHotloadArchive());
            if (!playModeTransitionResult.Accepted)
            {
                RecoveryNotice = BoundedDiagnostic(RuntimeLifecycle.LastActionableError);
                GlobalGameNamespace.Log.Error("Collaboration lifecycle: " + RecoveryNotice);
                return HotloadArchiveOutcome.Failed;
            }
            RecoveryNotice = BoundedDiagnostic("Runtime lifecycle blocked; recovery archived before connections were disposed. Leave, sync source in Git, and start a new session.");
            return HotloadArchiveOutcome.Archived;
        }
        catch (Exception error)
        {
            RuntimeLifecycle.RecordHotloadArchiveFailure(error);
            lifecycle.ApplyArchiveMarker(PlayModeTransitionResult.Rejected("archive failed"));
            RecoveryNotice = BoundedDiagnostic(RuntimeLifecycle.LastDiagnostic);
            GlobalGameNamespace.Log.Error("Collaboration lifecycle: " + RecoveryNotice);
            return HotloadArchiveOutcome.Failed;
        }
    }

    private bool DisposeActiveResources()
    {
        CancellationTokenSource cancellationTokenSource = hostVerificationLifetime;
        hostVerificationLifetime = new CancellationTokenSource();
        try
        {
            cancellationTokenSource.Cancel();
        }
        catch (ObjectDisposedException)
        {
        }
        try
        {
            cancellationTokenSource.Dispose();
        }
        catch (ObjectDisposedException)
        {
        }
        ResourceDisposalResult resourceDisposalResult = ResourceDisposalRules.DisposePair(wire, "collaboration transport", server, "collaboration host server");
        wire = null;
        server = null;
        Connected = false;
        try
        {
            TeardownConnectivity();
        }
        catch (Exception error)
        {
            GlobalGameNamespace.Log.Error("Collaboration connectivity: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error));
        }
        if (resourceDisposalResult.Succeeded)
        {
            return true;
        }
        lifecycle.RecordTerminalTeardownFailure(resourceDisposalResult.Failure);
        return false;
    }

    private void BeginConnectionTeardown(bool recoverWhenComplete)
    {
        Task<Wire> pendingConnect = connecting;
        connecting = null;
        ConnectionTeardownResult connectionTeardownResult = lifecycle.BeginDetachedTeardown(pendingConnect, recoverWhenComplete);
        if (connectionTeardownResult.Outcome == ConnectionTeardownOutcome.TerminalFailure)
        {
            RecordTerminalTeardownFailure(connectionTeardownResult.Failure);
        }
    }

    private void RecordTerminalTeardownFailure(string failure)
    {
        bool requiresHotloadRecoveryAfterLeave = RuntimeLifecycle.RequiresHotloadRecoveryAfterLeave;
        bool flag = !string.IsNullOrWhiteSpace(failure) && failure.StartsWith("Connection teardown failed;", StringComparison.Ordinal);
        string text = (string.IsNullOrWhiteSpace(failure) ? "unknown teardown error" : BoundedDiagnostic(failure));
        if (requiresHotloadRecoveryAfterLeave && !flag)
        {
            RuntimeLifecycle.RecordHotloadTeardownFailure(text);
        }
        string text2 = (RecoveryNotice = BoundedDiagnostic(requiresHotloadRecoveryAfterLeave ? (RuntimeLifecycle.LastActionableError ?? text) : (flag ? text : ("Connection teardown failed; this editor session remains offline and fail-stop: " + text))));
        if (requiresHotloadRecoveryAfterLeave)
        {
            SetLifecycleError(text2);
        }
        else
        {
            GlobalGameNamespace.Log.Error("Collaboration lifecycle: " + text2);
        }
    }

    private void CompleteHotloadRecovery(out string error)
    {
        error = null;
        if (!RuntimeLifecycle.RequiresHotloadRecoveryAfterLeave || !lifecycle.LeaveRequested)
        {
            return;
        }
        TeardownCompletionObservation teardownCompletionObservation = lifecycle.ObserveTeardownCompletion(requiresHotloadRecovery: true, connecting != null);
        if (!teardownCompletionObservation.Observed)
        {
            return;
        }
        if (teardownCompletionObservation.TeardownFailed)
        {
            RecordTerminalTeardownFailure(teardownCompletionObservation.TeardownFailure);
            return;
        }
        if (!teardownCompletionObservation.AttemptRecovery)
        {
            if (teardownCompletionObservation.Status == HotloadTeardownRecoveryStatus.ArchiveFailed && teardownCompletionObservation.Error != null)
            {
                RecoveryNotice = BoundedDiagnostic(teardownCompletionObservation.Error);
                GlobalGameNamespace.Log.Error("Collaboration lifecycle: " + RecoveryNotice);
            }
            return;
        }
        if (!RuntimeLifecycle.HotloadArchiveRecorded && RuntimeLifecycle.State == PlayModeLifecycleState.RuntimeBlocked && ArchiveHotloadLifecycle(RuntimeLifecycle.ArchiveReason ?? "hotloaded") == HotloadArchiveOutcome.Failed)
        {
            error = RecoveryNotice;
            return;
        }
        PlayModeTransitionResult playModeTransitionResult = RuntimeLifecycleEvents.RecoverFromHotload(RuntimeLifecycle);
        if (playModeTransitionResult.Accepted)
        {
            lifecycle.ClearDeferredHotloadRecovery();
            RecoveryNotice = "Hotload recovery reset completed after transport teardown and Leave. Sync source in Git, then start a new collaboration session.";
        }
        else
        {
            error = playModeTransitionResult.ActionableError;
        }
    }

    private void Preserve(string reason)
    {
        string text = Path.Combine(SceneAdapter.Root, ".collaboration", "recovery");
        Directory.CreateDirectory(text);
        string path = RecoveryPathRules.CreatePath(text, DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fffffff"));
        var captures = scenes.Where((KeyValuePair<string, Tracked> x) => x.Value.Session.Scene.IsValid()).ToDictionary((KeyValuePair<string, Tracked> x) => x.Key, (KeyValuePair<string, Tracked> x) => new
        {
            Live = x.Value.Session.Scene.Serialize(),
            Pending = x.Value.Pending,
            Known = x.Value.Known
        });
        Packet[] pending = (from x in scenes.Values
            where x.Pending != null
            select x.Pending).ToArray();
        RecoveryPathOutcome recoveryPathOutcome = RecoveryPathRules.PublishAfterWrite(path, () =>
        {
            File.WriteAllText(path, JsonSerializer.Serialize(new { reason, captures, pending }));
        });
        if (!recoveryPathOutcome.Succeeded)
        {
            throw new IOException("Recovery archive could not be written to " + path);
        }
        RecoveryPath = recoveryPathOutcome.Published;
    }

    [EditorEvent.Frame]
    public static void PumpCurrent()
    {
        Current.Frame();
    }

    public void Frame()
    {
        if (focusNoticeExpires != default(DateTime) && DateTime.UtcNow >= focusNoticeExpires)
        {
            FocusNotice = null;
            focusNoticeExpires = default;
        }
        if (saveNoticeExpires != default(DateTime) && DateTime.UtcNow >= saveNoticeExpires)
        {
            SaveNotice = null;
            saveNoticeExpires = default;
        }
        if (undoNoticeExpires != default(DateTime) && DateTime.UtcNow >= undoNoticeExpires)
        {
            UndoNotice = null;
            undoNoticeExpires = default;
        }
        if (actionNoticeExpires != default(DateTime) && DateTime.UtcNow >= actionNoticeExpires)
        {
            ActionNotice = null;
            actionNoticeExpires = default;
        }
        try
        {
            try
            {
                PollReconnect();
            }
            catch (Exception error)
            {
                AbandonReconnect(CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("Reconnect could not continue: ", error));
            }
            server?.Pump();
            try
            {
                orgDirectory?.Poll();
                PollPackageFetch();
                if (DateTime.UtcNow >= nextReadiness || (orgDirectory?.Revision ?? (-1)) != readinessOrgRevision)
                {
                    RefreshReadiness();
                }
            }
            catch (Exception error2)
            {
                GlobalGameNamespace.Log.Error("Collaboration readiness: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error2));
            }
            try
            {
                PollConnectivity();
            }
            catch (Exception error3)
            {
                GlobalGameNamespace.Log.Error("Collaboration connectivity: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error3));
            }
            try
            {
                TickIdleAutoSave();
            }
            catch (Exception error4)
            {
                GlobalGameNamespace.Log.Error("Collaboration auto-save: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error4));
            }
            try
            {
                TickAutoRecover();
            }
            catch (Exception error5)
            {
                GlobalGameNamespace.Log.Error("Collaboration auto-recover: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error5));
            }
            TeardownCompletionObservation teardownCompletionObservation = lifecycle.ObserveTeardownCompletion(RuntimeLifecycle.RequiresHotloadRecoveryAfterLeave, connecting != null);
            if (teardownCompletionObservation.Observed)
            {
                if (teardownCompletionObservation.TeardownFailed)
                {
                    RecordTerminalTeardownFailure(teardownCompletionObservation.TeardownFailure);
                }
                else if (teardownCompletionObservation.AttemptRecovery)
                {
                    CompleteHotloadRecovery(out var error6);
                    if (error6 != null)
                    {
                        SetLifecycleError(error6);
                    }
                }
                else if (teardownCompletionObservation.Error != null && RuntimeLifecycle.RequiresHotloadRecoveryAfterLeave)
                {
                    RecoveryNotice = BoundedDiagnostic(teardownCompletionObservation.Error);
                    GlobalGameNamespace.Log.Error("Collaboration lifecycle: " + RecoveryNotice);
                }
            }
            Task<Wire> task = connecting;
            if (task != null && task.IsCompleted)
            {
                SceneEditorSession sceneEditorSession = AuthoringSource();
                string sourceScenePath = SceneAdapter.PathOf(sceneEditorSession);
                bool flag = PlayModeSourceCoexistence.CanProcessSourcePackets(sceneEditorSession?.IsPlaying ?? false, sceneEditorSession, sourceScenePath, RuntimeLifecycle, CurrentRuntimeFor(sceneEditorSession));
                if (!ConnectionPromotionRules.ShouldPromote(flag, lifecycle.CanPromoteConnection))
                {
                    BeginConnectionTeardown(recoverWhenComplete: false);
                    if (!flag)
                    {
                        throw new InvalidOperationException("Stop play mode or complete the approved runtime lifecycle before promoting a source connection");
                    }
                    return;
                }
                Task<Wire> task2 = connecting;
                connecting = null;
                wire = task2.GetAwaiter().GetResult();
                wire.Send(new Packet
                {
                    Kind = "hello",
                    ProtocolVersion = 4,
                    Mode = CollaborationMode.Source,
                    Peer = Peer,
                    Project = project,
                    Baseline = baseline,
                    Name = userName,
                    Token = sessionToken,
                    SteamId = steamIdValue,
                    SteamToken = steamTokenValue,
                    OrgIdent = orgIdentValue,
                    LibraryVersion = libraryVersionValue,
                    ProjectManifest = projectManifestValue
                });
                lastReceive = DateTime.UtcNow;
            }
            if (wire == null)
            {
                return;
            }
            if (wire.StoredFailure != null)
            {
                Connected = false;
                throw new IOException("Disconnected: " + wire.StoredFailure);
            }
            SceneEditorSession sceneEditorSession2 = AuthoringSource();
            string sourceScenePath2 = SceneAdapter.PathOf(sceneEditorSession2);
            bool sourceAdmissible = PlayModeSourceCoexistence.CanProcessSourcePackets(sceneEditorSession2?.IsPlaying ?? false, sceneEditorSession2, sourceScenePath2, RuntimeLifecycle, CurrentRuntimeFor(sceneEditorSession2));
            PlaySpectateDecision decision = PlaySpectateRules.Decide(IsLocalPlaying(sceneEditorSession2), RuntimeLifecycle.State, sourceAdmissible, PlayModeSourceCoexistence.IsUsablePathKey(sourceScenePath2));
            if (decision != PlayDecision && decision == PlaySpectateDecision.Normal && !heldPackets.IsEmpty)
            {
                Status = "Connected • applying changes made while you were playing";
            }
            PlayDecision = decision;
            if (decision == PlaySpectateDecision.FailStop)
            {
                throw new InvalidOperationException("Stop play mode or complete the approved runtime lifecycle before applying source packets");
            }
            Packet result;
            if (decision == PlaySpectateDecision.Normal)
            {
                // Changes held while playing come first so they land in the order the host accepted them.
                while (heldPackets.TryTake(out result))
                {
                    ProcessIncoming(result);
                }
            }
            while (wire.Incoming.TryDequeue(out result))
            {
                if (decision != PlaySpectateDecision.Normal && HeldPacketBuffer.IsSceneChange(result.Kind))
                {
                    if (!heldPackets.TryHold(result))
                    {
                        throw new InvalidOperationException("Too many scene changes arrived while you were playing. Stop play mode, then use Recover & reconnect to catch up.");
                    }
                    lastReceive = DateTime.UtcNow;
                    continue;
                }
                ProcessIncoming(result);
            }
            if (DateTime.UtcNow - lastReceive > TimeSpan.FromSeconds(15L))
            {
                Connected = false;
                throw new IOException("Host heartbeat timed out");
            }
            if (!Connected)
            {
                return;
            }
            if (DateTime.UtcNow >= nextPresence)
            {
                // Polled every 100 ms as before, but only SENT when something changed or a refresh is due (SendCadence). Every packet an editor
                // sends is one the relay receives and a shared free relay is billed per message; an idle session used to send fifty a second.
                nextPresence = DateTime.UtcNow.AddMilliseconds(SendCadence.PresenceMinimum.TotalMilliseconds);
                SendPresence(force: false);
                if (SendCadence.ShouldSendPing(DateTime.UtcNow - lastPingSent))
                {
                    lastPingSent = DateTime.UtcNow;
                    wire.Send(new Packet
                    {
                        Kind = "ping"
                    });
                }
            }
            if (Error != null || DateTime.UtcNow < nextScan)
            {
                return;
            }
            if (decision != PlaySpectateDecision.Normal)
            {
                // Playing runs a copy of the scene. Sampling it would report the game's own state changes as edits,
                // so nothing is sampled until play stops; presence and the heartbeat above keep flowing.
                return;
            }
            nextScan = DateTime.UtcNow.AddMilliseconds(50.0);
            SceneEditorSession[] array = SceneEditorSession.All.Where((SceneEditorSession x) => !(x is GameEditorSession) && !x.IsPrefabSession).ToArray();
            foreach (SceneEditorSession sceneEditorSession3 in array)
            {
                string text = SceneAdapter.PathOf(sceneEditorSession3);
                if (sceneEditorSession3.IsPlaying && !PlayModeSourceCoexistence.CanSampleAuthoringSource(sceneEditorSession3, text, RuntimeLifecycle, CurrentRuntimeFor(sceneEditorSession3)))
                {
                    throw new InvalidOperationException("Stop play mode before editing collaboratively");
                }
                if (text == null || !text.StartsWith("scenes/"))
                {
                    continue;
                }
                if (!scenes.TryGetValue(text, out var value) || value.Session != sceneEditorSession3)
                {
                    if (value?.Pending != null)
                    {
                        throw new InvalidOperationException("Scene closed with an edit awaiting acknowledgement");
                    }
                    if (sceneEditorSession3.HasUnsavedChanges)
                    {
                        throw new InvalidOperationException("Newly opened scene has local unsaved edits; recover before subscribing");
                    }
                    value = new Tracked
                    {
                        Session = sceneEditorSession3,
                        Known = SceneAdapter.Capture(sceneEditorSession3.Scene),
                        Settings = sceneEditorSession3.Scene.SerializeProperties(),
                        DiskStamp = SceneAdapter.DiskStamp(text)
                    };
                    scenes[text] = value;
                    value.LastOrder = value.Known;
                    wire.Send(new Packet
                    {
                        Kind = "subscribe",
                        Scene = text,
                        Objects = value.Known.Values.ToList()
                    });
                }
                else
                {
                    if (!value.Ready || value.Pending != null)
                    {
                        continue;
                    }
                    DateTime dateTime = SceneAdapter.DiskStamp(text);
                    bool flag2 = dateTime == value.DiskStamp;
                    TimeSpan mismatchFor;
                    if (flag2)
                    {
                        mismatchFor = TimeSpan.Zero;
                        value.StampMismatchSince = default;
                    }
                    else if (value.StampMismatchSince == default(DateTime))
                    {
                        value.StampMismatchSince = DateTime.UtcNow;
                        mismatchFor = TimeSpan.Zero;
                    }
                    else
                    {
                        mismatchFor = DateTime.UtcNow - value.StampMismatchSince;
                    }
                    if (SceneSaveRules.ShouldFailOnDiskStamp(flag2, mismatchFor))
                    {
                        value.StampMismatchSince = default;
                        throw new InvalidOperationException("Scene file changed outside Collaboration Save. Local state retained; leave and reconcile the file in Git.");
                    }
                    if (!JsonNode.DeepEquals(value.Settings, sceneEditorSession3.Scene.SerializeProperties()))
                    {
                        throw new InvalidOperationException("Scene settings/metadata/navmesh changes are outside the supported edit set. Local changes retained.");
                    }
                    Dictionary<string, ObjectState> dictionary = SceneAdapter.Capture(sceneEditorSession3.Scene);
                    foreach (string item in value.Known.Keys.Except(dictionary.Keys))
                    {
                        GameObject gameObject = sceneEditorSession3.Scene.Directory.FindByGuid(Guid.Parse(item));
                        if (gameObject != null && gameObject.IsValid && !gameObject.IsDestroyed)
                        {
                            throw new InvalidOperationException("Object became non-serializable instead of being deleted; edit is outside the supported set");
                        }
                    }
                    SceneAdapter.ValidateSiblingOrder(value.LastOrder, dictionary);
                    value.LastOrder = dictionary;
                    List<Change> list = StateRules.Diff(value.Known, dictionary);
                    if (list.Count > 0)
                    {
                        lastLocalEdit = DateTime.UtcNow;
                    }
                    bool flag3 = continuousEdits == 0 && !Editor.Application.MouseButtons.HasFlag(MouseButtons.Left);
                    if (list.Count == 0 && (!flag3 || value.Group.Count == 0))
                    {
                        continue;
                    }
                    foreach (Change item2 in list)
                    {
                        StateRules.ValidateChange(item2);
                    }
                    SendEdit(text, value, list, flag3);
                }
            }
        }
        catch (Exception ex)
        {
            Fail(ex.Message, AutoRecoveryRules.Classify(ex));
        }
    }

    private void ProcessIncoming(Packet result)
    {
        string kind = result.Kind;
        if (HeldPacketBuffer.IsSceneChange(kind))
        {
            Dictionary<string, object> trackedByScene = ((IEnumerable<KeyValuePair<string, Tracked>>)scenes).ToDictionary((Func<KeyValuePair<string, Tracked>, string>)((KeyValuePair<string, Tracked> pair) => pair.Key), (Func<KeyValuePair<string, Tracked>, object>)((KeyValuePair<string, Tracked> pair) => pair.Value));
            ScenePacketRouteResult scenePacketRouteResult = ScenePacketRouting.RouteTrackedPacket(result.Kind, result.Scene, trackedByScene, (object target) => ((Tracked)target).Session.IsPlaying, (object target) => SceneAdapter.PathOf(((Tracked)target).Session), RuntimeLifecycle, (object target) => CurrentRuntimeFor(((Tracked)target).Session));
            if (!scenePacketRouteResult.Admitted)
            {
                throw new InvalidOperationException(scenePacketRouteResult.Error ?? "Scene packet was rejected by routing admission.");
            }
        }
        lastReceive = DateTime.UtcNow;
        Receive(result);
    }

    // Play can be running in the editor's own game session or as a flag on the source scene session; either means
    // the scene in front of the user is a copy being played, not the one being authored.
    private static bool IsLocalPlaying(SceneEditorSession source)
    {
        return (source?.IsPlaying ?? false) || SceneEditorSession.All.Any((SceneEditorSession x) => x is GameEditorSession);
    }

    private static CollaborationConfig LoadConfig()
    {
        string error;
        return CollaborationConfig.LoadFrom(Path.Combine(SceneAdapter.Root, "ProjectSettings", "collaboration.json"), out error);
    }

    private void UseConfig(CollaborationConfig config)
    {
        sessionConfig = config;
        projectIdResolved = false;
        if (orgDirectory == null || !string.Equals(orgDirectory.Org, config.Org, StringComparison.OrdinalIgnoreCase))
        {
            orgDirectory = new OrgDirectory(config);
        }
    }

    private void RefreshReadiness()
    {
        nextReadiness = DateTime.UtcNow.AddSeconds(1.0);
        CollaborationConfig collaborationConfig = LoadConfig();
        UseConfig(collaborationConfig);
        readinessOrgRevision = orgDirectory.Revision;
        orgDirectory.Poll();
        PollPackageFetch();
        bool flag = LocalSteamIdentity.TryGet(out var identity, out var personaName, out var _);
        string text = SceneAdapter.PathOf(SupportedActiveScene);
        (bool, Version) tuple = InstalledLibrary(collaborationConfig);
        Readiness = ReadinessRules.Evaluate(new ReadinessInput(flag, flag ? personaName : null, text != null, Project.Current?.IsPublished ?? false, packageReachable == true, tuple.Item1, tuple.Item2, collaborationConfig.LibraryVersion, flag && orgDirectory.Contains(SteamIdentityRules.Canonical(identity)), collaborationConfig, text));
    }

    private static (bool Installed, Version Version) InstalledLibrary(CollaborationConfig config)
    {
        foreach (LibraryProject item in LibrarySystem.All)
        {
            if (!LibraryInfo.IsThisLibrary(config?.LibraryIdent, item.Project?.Config?.Ident))
            {
                continue;
            }
            // Asset Party stamps its own build number into the installed copy's .version file (1.0.<build number>), so item.Version is a build id and can
            // never be compared with the release version the handshake and the settings file use. The code running here is the installed copy.
            Version.TryParse(LibraryInfo.LibraryVersion, out Version result);
            return (Installed: true, Version: result);
        }
        return (Installed: false, Version: null);
    }

    private void PollPackageFetch()
    {
        if (packageFetch != null)
        {
            if (packageFetch.IsCompleted)
            {
                try
                {
                    packageReachable = packageFetch.GetAwaiter().GetResult() != null;
                }
                catch (Exception)
                {
                    packageReachable = false;
                }
                packageFetch = null;
                nextPackageFetch = DateTime.UtcNow.AddSeconds(30.0);
            }
            return;
        }
        bool? flag = packageReachable;
        if (flag.HasValue && DateTime.UtcNow < nextPackageFetch)
        {
            return;
        }
        if (string.IsNullOrWhiteSpace(Project.Current?.Config?.FullIdent))
        {
            packageReachable = false;
            return;
        }
        try
        {
            packageFetch = Package.FetchAsync(Project.Current.Config.FullIdent, partial: false);
        }
        catch (Exception)
        {
            packageReachable = false;
            nextPackageFetch = DateTime.UtcNow.AddSeconds(30.0);
        }
    }

    private PeerFacts BuildPeerFacts(Packet hello)
    {
        string steamId = (SteamIdentityRules.TryParse(hello.SteamId, out var identity) ? SteamIdentityRules.Canonical(identity) : hello.SteamId);
        bool isFriend = false;
        if (SteamIdentityRules.TryParse(hello.SteamId, out var identity2))
        {
            try
            {
                isFriend = Steam.IsFriend(identity2.Value);
            }
            catch (Exception)
            {
                isFriend = false;
            }
        }
        bool isHostSelf = SteamIdentityRules.TryParse(hello.SteamId, out var identity3) && LocalSteamIdentity.TryGet(out var identity4, out var _, out var _) && string.Equals(SteamIdentityRules.Canonical(identity3), SteamIdentityRules.Canonical(identity4), StringComparison.Ordinal);
        bool isOrgMember = orgDirectory?.Contains(steamId) ?? false;
        if (SoloMayVouch(hello))
        {
            // The reserved test identity is neither a Steam friend nor a member of the organization; in a
            // disposable copy the host treats it as both so the remaining gates (project, baseline, library
            // version, manifest, duplicate account) still run for real.
            isFriend = true;
            isOrgMember = true;
        }
        return new PeerFacts(steamId, IdentityVerified: false, isFriend, isOrgMember, hello.LibraryVersion, hello.ProjectManifest, hello.OrgIdent, isHostSelf);
    }

    private static SceneEditorSession AuthoringSource()
    {
        SceneEditorSession active = SceneEditorSession.Active;
        if (1 == 0)
        {
        }
        SceneEditorSession result;
        if (active is GameEditorSession { Parent: var parent })
        {
            result = ((parent != null && !parent.IsPrefabSession) ? parent : null);
        }
        else
        {
            result = ((active == null || active.IsPrefabSession) ? null : active);
        }
        if (1 == 0)
        {
        }
        return result;
    }

    private RuntimeSessionDescriptor CurrentRuntimeFor(SceneEditorSession source)
    {
        if (source == null || !source.IsPlaying)
        {
            return null;
        }
        GameEditorSession[] array = SceneEditorSession.All.OfType<GameEditorSession>().ToArray();
        if (array.Length != 1)
        {
            return null;
        }
        GameEditorSession gameEditorSession = array[0];
        if (gameEditorSession.Parent != source)
        {
            return null;
        }
        return RuntimeSceneAdapter.Describe(gameEditorSession);
    }

    private void SendEdit(string scene, Tracked tracked, List<Change> changes, bool final)
    {
        tracked.Pending = new Packet
        {
            Kind = "edit",
            Scene = scene,
            Peer = Peer,
            Changes = changes,
            Final = final
        };
        StorePending(tracked.Pending);
        wire.Send(tracked.Pending);
        Status = "Connected • edit awaiting acknowledgement";
    }

    private string PendingPath(string id)
    {
        return Path.Combine(SceneAdapter.Root, ".collaboration", "outbox", Peer + "-" + id + ".json");
    }

    private void StorePending(Packet packet)
    {
        string text = PendingPath(packet.Id);
        Directory.CreateDirectory(Path.GetDirectoryName(text));
        byte[] bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { project, baseline, packet }));
        using (FileStream fileStream = new FileStream(text + ".tmp", FileMode.Create, FileAccess.Write, FileShare.None))
        {
            fileStream.Write(bytes);
            fileStream.Flush(flushToDisk: true);
        }
        File.Move(text + ".tmp", text, overwrite: true);
    }

    private void ForgetPending(Tracked tracked, string id)
    {
        if (!(tracked.Pending?.Id != id))
        {
            File.Delete(PendingPath(id));
            tracked.Pending = null;
        }
    }

    private void Receive(Packet p)
    {
        if (p.Kind == "welcome")
        {
            if (restoring && hostSession != null && hostSession != p.Session)
            {
                Fail("Host session ended or restarted. Preserve recovery data, sync saved scenes and start a new session.");
                return;
            }
            hostSession = p.Session;
            Connected = true;
            Status = "Connected • live";
            reconnectRecovery = false;
            if (!restoring)
            {
                return;
            }
            {
                foreach (KeyValuePair<string, Tracked> scene in scenes)
                {
                    if (scene.Value.Pending != null)
                    {
                        wire.Send(scene.Value.Pending);
                    }
                    if (scene.Value.Session.Scene.IsValid())
                    {
                        wire.Send(new Packet
                        {
                            Kind = "subscribe",
                            Scene = scene.Key,
                            Objects = scene.Value.Known.Values.ToList()
                        });
                    }
                }
                return;
            }
        }
        string canonical;
        if (p.Kind == "fatal" || p.Kind == "reject")
        {
            string text = CollaborationTransportDiagnostics.BoundedStoredDiagnostic(p.Error);
            if (p.Kind == "reject" && p.Scene == null)
            {
                Fail(text);
                return;
            }
            if (restoring && p.Kind == "reject")
            {
                RecoveryNotice = CollaborationDiagnostics.Bounded("Rejected offline edit retained in " + RecoveryPath + ": " + text);
            }
            else
            {
                Fail(text);
            }
            if (p.Kind == "reject")
            {
                if (!ScenePacketRouting.TryResolveTracked(p.Scene, ((IEnumerable<KeyValuePair<string, Tracked>>)scenes).ToDictionary((Func<KeyValuePair<string, Tracked>, string>)((KeyValuePair<string, Tracked> pair) => pair.Key), (Func<KeyValuePair<string, Tracked>, object>)((KeyValuePair<string, Tracked> pair) => pair.Value)), out canonical, out object target) || !(target is Tracked tracked))
                {
                    throw new InvalidOperationException("Rejected packet has no current tracked scene target.");
                }
                ForgetPending(tracked, p.Id);
            }
        }
        else if (p.Kind == "presence")
        {
            if (p.Peer != Peer)
            {
                Roster[p.Peer] = p;
            }
        }
        else if (p.Kind == "left")
        {
            Roster.Remove(p.Peer);
        }
        else
        {
            if (p.Kind == "pong")
            {
                return;
            }
            if (p.Scene == null)
            {
                throw new InvalidOperationException("Scene packet has no current tracked target.");
            }
            if (!ScenePacketRouting.TryResolveTracked(p.Scene, ((IEnumerable<KeyValuePair<string, Tracked>>)scenes).ToDictionary((Func<KeyValuePair<string, Tracked>, string>)((KeyValuePair<string, Tracked> pair) => pair.Key), (Func<KeyValuePair<string, Tracked>, object>)((KeyValuePair<string, Tracked> pair) => pair.Value)), out canonical, out object target2) || !(target2 is Tracked tracked2))
            {
                throw new InvalidOperationException("Scene packet has no current tracked target.");
            }
            if (Error != null)
            {
                return;
            }
            if (p.Kind == "snapshot")
            {
                Dictionary<string, ObjectState> dictionary = StateRules.Map(p.Objects);
                if (!tracked2.Session.Scene.IsValid())
                {
                    tracked2.Known = dictionary;
                    return;
                }
                if (!restoring && StateRules.Diff(tracked2.Known, SceneAdapter.Capture(tracked2.Session.Scene)).Count > 0)
                {
                    throw new InvalidOperationException("Scene edited during join; local state retained");
                }
                SceneAdapter.Apply(tracked2.Session, dictionary);
                tracked2.Known = dictionary;
                tracked2.Ready = true;
                tracked2.LastOrder = SceneAdapter.Capture(tracked2.Session.Scene);
                tracked2.Session.UndoSystem.Initialize();
                tracked2.Group.Clear();
                tracked2.Undo.Clear();
                MirrorNativeUndo(tracked2);
                if (scenes.Values.All((Tracked x) => x.Ready || !x.Session.Scene.IsValid()))
                {
                    restoring = false;
                }
            }
            else
            {
                if (p.Kind != "accepted")
                {
                    return;
                }
                if (!seen.Add(p.Peer + ":" + p.Id))
                {
                    ForgetPending(tracked2, p.Id);
                    return;
                }
                AcceptedOperations++;
                if (p.Peer == Peer)
                {
                    foreach (Change change in p.Changes)
                    {
                        if ((object)change.After == null)
                        {
                            tracked2.Known.Remove(change.Id);
                        }
                        else
                        {
                            tracked2.Known[change.Id] = change.After;
                        }
                        ObjectState before = (tracked2.Group.TryGetValue(change.Id, out var value) ? value.Before : change.Before);
                        tracked2.Group[change.Id] = new Change(change.Id, before, change.After);
                    }
                    ForgetPending(tracked2, p.Id);
                    if (p.Final)
                    {
                        if (!tracked2.UndoPending)
                        {
                            tracked2.Undo.Push(tracked2.Group.Values.ToList());
                        }
                        tracked2.Group.Clear();
                        tracked2.UndoPending = false;
                        if (tracked2.Session.Scene.IsValid())
                        {
                            tracked2.Session.UndoSystem.Initialize();
                        }
                        MirrorNativeUndo(tracked2);
                    }
                }
                else
                {
                    bool flag = tracked2.Session.Scene.IsValid() && SceneEditorSession.All.Contains(tracked2.Session);
                    Dictionary<string, ObjectState> dictionary2 = (flag ? SceneAdapter.Capture(tracked2.Session.Scene) : new Dictionary<string, ObjectState>(tracked2.Known));
                    foreach (Change change2 in p.Changes)
                    {
                        if (!StateRules.Equal(dictionary2.GetValueOrDefault(change2.Id), tracked2.Known.GetValueOrDefault(change2.Id)))
                        {
                            throw new InvalidOperationException("Concurrent edit on " + change2.Id + ". Your local version was preserved. Use Recover & reconnect to accept the host state.");
                        }
                        if ((object)change2.After == null)
                        {
                            dictionary2.Remove(change2.Id);
                            tracked2.Known.Remove(change2.Id);
                        }
                        else
                        {
                            dictionary2[change2.Id] = change2.After;
                            tracked2.Known[change2.Id] = change2.After;
                        }
                    }
                    if (flag)
                    {
                        SceneAdapter.Apply(tracked2.Session, dictionary2);
                        tracked2.LastOrder = SceneAdapter.Capture(tracked2.Session.Scene);
                        tracked2.Session.UndoSystem.Initialize();
                        MirrorNativeUndo(tracked2);
                    }
                }
                Status = "Connected • revision " + p.Revision;
            }
        }
    }

    public IDisposable BeginContinuousEdit()
    {
        if (!Connected || Error != null)
        {
            throw new InvalidOperationException("Connect before starting a continuous edit");
        }
        continuousEdits++;
        return new EditScope(this);
    }

    public bool CanSave()
    {
        if (sourceBaseline != SceneAdapter.Baseline(includeScenes: false))
        {
            return false;
        }
        if (!Connected || Error != null)
        {
            return false;
        }
        string text = SceneAdapter.PathOf(SceneEditorSession.Active);
        if (text == null || !scenes.TryGetValue(text, out var value))
        {
            return false;
        }
        if (!value.Ready || value.Pending != null || value.Group.Count > 0)
        {
            return false;
        }
        if (!JsonNode.DeepEquals(value.Settings, value.Session.Scene.SerializeProperties()))
        {
            return false;
        }
        return StateRules.Diff(value.Known, SceneAdapter.Capture(value.Session.Scene)).Count == 0;
    }

    /// <summary>Why a save is not possible right now, in words, or null when it is possible. Mirrors <see cref="CanSave"/> condition for condition.</summary>
    public string WhyCannotSave()
    {
        if (sourceBaseline != SceneAdapter.Baseline(includeScenes: false))
        {
            return "the project's source or assets changed since the session started";
        }
        if (!Connected || Error != null)
        {
            return "the session is not connected";
        }
        string text = SceneAdapter.PathOf(SceneEditorSession.Active);
        if (text == null || !scenes.TryGetValue(text, out var value))
        {
            return "the active scene is not part of the session (active: " + (text ?? "none") + ")";
        }
        if (!value.Ready)
        {
            return "the host has not finished sending the scene";
        }
        if (value.Pending != null)
        {
            return "an edit is still waiting for the host to acknowledge it";
        }
        if (value.Group.Count > 0)
        {
            return "a multi-part edit is still being grouped";
        }
        if (!JsonNode.DeepEquals(value.Settings, value.Session.Scene.SerializeProperties()))
        {
            return "the scene's own settings differ from what the host has";
        }
        List<Change> difference = StateRules.Diff(value.Known, SceneAdapter.Capture(value.Session.Scene));
        if (difference.Count > 0)
        {
            Change first = difference[0];
            string name = (first.After ?? first.Before)?.Data?["Name"]?.GetValue<string>() ?? first.Id;
            return difference.Count + " object" + (difference.Count == 1 ? "" : "s") + " differ from the host's copy (first: " + name + ")";
        }
        return null;
    }

    private string SaveRefusal()
    {
        if (sourceBaseline != SceneAdapter.Baseline(includeScenes: false))
        {
            return "Source/assets changed during collaboration; end the session and sync in Git before saving a checkpoint";
        }
        return "Wait for all edits to be acknowledged before saving";
    }

    public void Save()
    {
        if (!CanSave())
        {
            throw new InvalidOperationException(SaveRefusal());
        }
        string text = SceneAdapter.PathOf(SceneEditorSession.Active);
        if (text == null || !scenes.TryGetValue(text, out var value))
        {
            throw new InvalidOperationException(SaveRefusal());
        }
        saving = true;
        try
        {
            SendPresence();
            value.Session.Save(saveAs: false);
            value.DiskStamp = SceneAdapter.DiskStamp(text);
            server?.AllowSavedBaseline(SceneAdapter.Baseline());
        }
        finally
        {
            saving = false;
        }
    }

    private void Checkpoint()
    {
        if (AutoSaveRules.CheckpointEligible(SceneEditorSession.Active?.HasUnsavedChanges ?? false, CanSave))
        {
            RunAutoSave();
        }
    }

    private void RunAutoSave()
    {
        lastAutoSave = DateTime.UtcNow;
        try
        {
            Save();
            SetSaveNotice("Auto-saved the synchronized scene at " + DateTime.Now.ToString("HH:mm") + ".");
        }
        catch (Exception ex)
        {
            GlobalGameNamespace.Log.Error("Collaboration auto-save: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, ex));
            SetSaveNotice("The automatic save did not run: " + ex.Message);
        }
    }

    private void TickIdleAutoSave()
    {
        if (!AutoSaveIdle)
        {
            return;
        }
        DateTime utcNow = DateTime.UtcNow;
        if (!(utcNow < nextAutoSaveCheck))
        {
            nextAutoSaveCheck = utcNow.AddSeconds(1.0);
            if (AutoSaveRules.Eligible(new IdleSaveInput(Enabled: true, SceneEditorSession.Active?.HasUnsavedChanges ?? false, Roster.Values.Any((Packet x) => string.Equals(PresenceText(x.Presence, "state", 32), "editing", StringComparison.Ordinal)), SceneEditorSession.All.Any((SceneEditorSession x) => x.IsPlaying), lastLocalEdit, lastAutoSave, utcNow, CanSave)))
            {
                RunAutoSave();
            }
        }
    }

    private void TickAutoRecover()
    {
        if (Connected)
        {
            bool flag = recoverArmed;
            recoverArmed = false;
            recoverDeclined = false;
            recoverAttempts = 0;
            recoverNextAttempt = default;
            recoverPromptShown = false;
            if (flag)
            {
                ShowToast("Connection restored", "Automatic recovery reconnected this session.", Theme.Green, 6f);
            }
        }
        else
        {
            if (Error == null)
            {
                return;
            }
            bool flag2 = TerminalTeardownError != null || RuntimeLifecycle.HasTerminalTeardownFailure;
            if ((errorClass != SessionFailureClass.Transient) | flag2)
            {
                DeclineAutoRecover("This editor session remains fail-stop; automatic recovery cannot continue.");
            }
            else if (!AutoRecover)
            {
                if (!recoverPromptShown)
                {
                    bool flag3 = AutoRecoveryRules.PromptOffered(AutoRecover, Error != null, errorClass, flag2, RecoveryOffered(), recoverPromptShown);
                    recoverPromptShown = true;
                    if (flag3)
                    {
                        ShowToast("Connection lost", "Select Recover & reconnect, or press CTRL+ALT+R, to restore this session.", Theme.Primary, 15f);
                    }
                }
            }
            else
            {
                if (recoverDeclined || (recoverNextAttempt != default(DateTime) && DateTime.UtcNow < recoverNextAttempt))
                {
                    return;
                }
                if (recoverAttempts >= 3)
                {
                    DeclineAutoRecover("Three automatic attempts did not reconnect this session.");
                    return;
                }
                bool isHost = IsHost;
                bool flag4 = !isHost && HasUnsavedTrackedChanges();
                bool flag5 = reconnectQueued || reconnectTokenTask != null || connecting != null;
                if (isHost)
                {
                    DeclineAutoRecover("A hosted session is never recovered automatically.");
                }
                else
                {
                    if (flag4 | flag5)
                    {
                        return;
                    }
                    string text = activeAddress ?? startAddress;
                    int num = ((activePort != 0) ? activePort : startPort);
                    if (string.IsNullOrWhiteSpace(text) || num <= 0)
                    {
                        DeclineAutoRecover("Automatic recovery has no endpoint to redial.");
                        return;
                    }
                    bool reconnectAccepted = RecoveryOffered();
                    if (AutoRecoveryRules.Eligible(new AutoRecoveryInput(AutoRecover, Error != null, errorClass, flag2, isHost, flag4, reconnectAccepted, flag5, recoverAttempts, DateTime.UtcNow, recoverNextAttempt)))
                    {
                        recoverAttempts++;
                        recoverArmed = true;
                        recoverNextAttempt = DateTime.UtcNow.Add(AutoRecoveryRules.Backoff(recoverAttempts - 1));
                        try
                        {
                            recoverArming = true;
                            Reconnect(text, num);
                            return;
                        }
                        catch (Exception ex)
                        {
                            recoverArmed = false;
                            recoverDeclined = true;
                            ShowToast("Automatic recovery stopped", ex.Message, Theme.Red, 6f);
                            GlobalGameNamespace.Log.Error("Collaboration auto-recover: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, ex));
                            return;
                        }
                        finally
                        {
                            recoverArming = false;
                        }
                    }
                    DeclineAutoRecover("Automatic recovery is not possible in this session's current state.");
                }
            }
        }
    }

    private bool HasUnsavedTrackedChanges()
    {
        foreach (Tracked value in scenes.Values)
        {
            SceneEditorSession sceneEditorSession = value?.Session;
            if (sceneEditorSession != null && sceneEditorSession.HasUnsavedChanges)
            {
                return true;
            }
        }
        return false;
    }

    private void DeclineAutoRecover(string reason)
    {
        if (!recoverDeclined || recoverArmed)
        {
            bool flag = recoverArmed;
            recoverArmed = false;
            recoverDeclined = true;
            if (flag)
            {
                ShowToast("Automatic recovery stopped", reason, Theme.Red, 6f);
            }
        }
    }

    private bool RecoveryOffered()
    {
        if (Error == null)
        {
            return false;
        }
        try
        {
            return ReconnectGuard().Accepted;
        }
        catch (Exception)
        {
            return false;
        }
    }

    private static void ShowToast(string title, string subtitle, Color border, float seconds)
    {
        try
        {
            ToastWidget widget = new ToastWidget
            {
                Title = title,
                Subtitle = subtitle,
                BorderColor = border,
                DrawTimer = false,
                IsRunning = false,
                FixedWidth = 360f
            };
            ToastManager.Remove(widget, seconds);
        }
        catch (Exception error)
        {
            GlobalGameNamespace.Log.Error("Collaboration notice: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error));
        }
    }

    public void Undo()
    {
        string text = SceneAdapter.PathOf(SceneEditorSession.Active);
        if (!Connected || Error != null || text == null || !scenes.TryGetValue(text, out var value) || !value.Ready || value.Pending != null || value.Undo.Count == 0)
        {
            throw new InvalidOperationException("No acknowledged local operation available to undo");
        }
        List<Change> list = (from c in value.Undo.Peek()
            select new Change(c.Id, c.After, c.Before)).ToList();
        Dictionary<string, ObjectState> dictionary = SceneAdapter.Capture(value.Session.Scene);
        foreach (Change item in list)
        {
            if (!StateRules.Equal(dictionary.GetValueOrDefault(item.Id), item.Before))
            {
                throw new InvalidOperationException("Undo refused: object changed since your edit");
            }
            if ((object)item.After == null)
            {
                dictionary.Remove(item.Id);
            }
            else
            {
                dictionary[item.Id] = item.After;
            }
        }
        SceneAdapter.Apply(value.Session, dictionary);
        value.Undo.Pop();
        value.UndoPending = true;
        SendEdit(text, value, list, final: true);
    }

    private void MirrorNativeUndo(Tracked tracked)
    {
        try
        {
            if (tracked == null || tracked.Session == null || tracked.Undo.Count == 0)
            {
                return;
            }
            UndoSystem undoSystem = tracked.Session.UndoSystem;
            if (undoSystem == null)
            {
                return;
            }
            List<Change> changes = tracked.Undo.Peek();
            if (changes != null && changes.Count != 0)
            {
                undoSystem.Insert("Collaboration Undo", () =>
                {
                    TryUndo();
                }, () =>
                {
                    TryRedo(tracked, changes);
                });
            }
        }
        catch (Exception error)
        {
            GlobalGameNamespace.Log.Error("Collaboration undo mirror: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error));
        }
    }

    private void ClearNativeUndoMirrors()
    {
        try
        {
            UndoSystem undoSystem = SceneEditorSession.Active?.UndoSystem;
            if (IsNativeUndoMirror(undoSystem))
            {
                undoSystem.Initialize();
            }
            foreach (Tracked value in scenes.Values)
            {
                UndoSystem undoSystem2 = value?.Session?.UndoSystem;
                if (IsNativeUndoMirror(undoSystem2))
                {
                    undoSystem2.Initialize();
                }
            }
        }
        catch (Exception error)
        {
            GlobalGameNamespace.Log.Error("Collaboration undo mirror: " + CollaborationTransportDiagnostics.BoundedExceptionDiagnostic(string.Empty, error));
        }
    }

    private static bool IsNativeUndoMirror(UndoSystem undoSystem)
    {
        if (undoSystem == null)
        {
            return false;
        }
        UndoSystem.Entry result;
        return undoSystem.Back.TryPeek(out result) && string.Equals(result?.Name, "Collaboration Undo", StringComparison.Ordinal);
    }

    private bool TryUndo()
    {
        try
        {
            Undo();
            UndoNotice = null;
            undoNoticeExpires = default;
            return true;
        }
        catch (Exception ex)
        {
            SetUndoNotice(ex.Message);
            return false;
        }
    }

    private bool TryRedo(Tracked tracked, List<Change> changes)
    {
        try
        {
            if (changes == null || changes.Count == 0 || !Connected || Error != null || tracked == null || tracked.Session == null || !tracked.Session.Scene.IsValid())
            {
                SetUndoNotice("Redo is not available for this collaboration operation.");
                return false;
            }
            string text = SceneAdapter.PathOf(tracked.Session);
            if (text == null || !scenes.TryGetValue(text, out var value) || value != tracked || !value.Ready || value.Pending != null || value.UndoPending)
            {
                SetUndoNotice("Redo is not available for this collaboration operation.");
                return false;
            }
            Dictionary<string, ObjectState> dictionary = SceneAdapter.Capture(value.Session.Scene);
            foreach (Change change in changes)
            {
                if (StateRules.Equal(dictionary.GetValueOrDefault(change.Id), change.Before))
                {
                    continue;
                }
                if (StateRules.Equal(dictionary.GetValueOrDefault(change.Id), change.After))
                {
                    SetUndoNotice("Nothing to redo: this collaboration operation is still applied.");
                    return false;
                }
                SetUndoNotice("Redo refused: the scene changed since your edit.");
                return false;
            }
            foreach (Change change2 in changes)
            {
                if ((object)change2.After == null)
                {
                    dictionary.Remove(change2.Id);
                }
                else
                {
                    dictionary[change2.Id] = change2.After;
                }
            }
            SceneAdapter.Apply(value.Session, dictionary);
            value.Undo.Push(changes);
            value.UndoPending = true;
            SendEdit(text, value, changes, final: true);
            UndoNotice = null;
            undoNoticeExpires = default;
            return true;
        }
        catch (Exception ex)
        {
            SetUndoNotice(ex.Message);
            return false;
        }
    }

    private void SetUndoNotice(string text)
    {
        string text2 = BoundedDiagnostic(text);
        if (!string.Equals(UndoNotice, text2, StringComparison.Ordinal) || !(DateTime.UtcNow < undoNoticeExpires))
        {
            UndoNotice = text2;
            undoNoticeExpires = DateTime.UtcNow.AddSeconds(8.0);
        }
    }

    private void SetActionNotice(string text)
    {
        string text2 = BoundedDiagnostic(text);
        if (!string.Equals(ActionNotice, text2, StringComparison.Ordinal) || !(DateTime.UtcNow < actionNoticeExpires))
        {
            ActionNotice = text2;
            actionNoticeExpires = DateTime.UtcNow.AddSeconds(8.0);
        }
    }

    private void SendPresence(bool force = true)
    {
        if (wire == null)
        {
            return;
        }
        SceneEditorSession active = SceneEditorSession.Active;
        SceneViewportWidget sceneViewportWidget = SceneViewWidget.Current?.LastSelectedViewportWidget;
        JsonObject jsonObject = new JsonObject();
        SceneEditorSession source = AuthoringSource();
        GameEditorSession game = SceneEditorSession.All.OfType<GameEditorSession>().FirstOrDefault();
        bool playing = IsLocalPlaying(source);
        CameraComponent gameCamera = null;
        try
        {
            gameCamera = game?.Scene?.Camera;
        }
        catch (Exception)
        {
            gameCamera = null;
        }
        if (playing && gameCamera != null && gameCamera.IsValid())
        {
            // While playing, the collaborator's marker follows the game camera, so someone still in the editor
            // sees where the player is and which way they look. The scene view is not what they are looking at.
            jsonObject["position"] = JsonSerializer.SerializeToNode(gameCamera.WorldPosition);
            jsonObject["rotation"] = JsonSerializer.SerializeToNode(gameCamera.WorldRotation);
            float gameFov = gameCamera.FieldOfView;
            if (gameFov > 0f && gameFov < 180f)
            {
                jsonObject["fov"] = gameFov;
            }
        }
        else if (!playing && sceneViewportWidget != null && SceneViewWidget.Current.Session == active)
        {
            jsonObject["position"] = JsonSerializer.SerializeToNode(sceneViewportWidget.State.CameraPosition);
            jsonObject["rotation"] = JsonSerializer.SerializeToNode(sceneViewportWidget.State.CameraRotation);
            float? num = sceneViewportWidget.Renderer?.Camera?.FieldOfView;
            if (num.HasValue)
            {
                float valueOrDefault = num.GetValueOrDefault();
                if (valueOrDefault > 0f && valueOrDefault < 180f)
                {
                    jsonObject["fov"] = num.Value;
                }
            }
        }
        GameObject[] array = (playing ? null : active?.Selection.OfType<GameObject>().ToArray()) ?? Array.Empty<GameObject>();
        jsonObject["selection"] = JsonSerializer.SerializeToNode(array.Select((GameObject x) => x.Id.ToString()).ToArray());
        // The left mouse button is held down for gameplay too, so it only means "editing" outside play.
        bool flag = !playing && (continuousEdits > 0 || Editor.Application.MouseButtons.HasFlag(MouseButtons.Left));
        JsonObject jsonObject2 = jsonObject;
        string text = PlaySpectateRules.PresenceState(saving, playing, flag);
        jsonObject2["state"] = text;
        if (flag && array.Length != 0)
        {
            jsonObject["editing"] = array[0].Id.ToString();
        }
        string text2 = BoundPresenceText(EditorToolManager.CurrentModeName, 40);
        if (text2 != null)
        {
            jsonObject["tool"] = text2;
        }
        string presenceScene = SceneAdapter.PathOf(playing && source != null ? source : active);
        // What "changed" means: the same presence body and scene as the last one sent. The state word and selection are part of the body, so a
        // selection or a status change is sent at once; only an identical packet is held back.
        string fingerprint = presenceScene + "|" + jsonObject.ToJsonString();
        TimeSpan sinceLast = DateTime.UtcNow - lastPresenceSent;
        bool changed = !string.Equals(fingerprint, lastPresenceFingerprint, StringComparison.Ordinal);
        if (!force && !SendCadence.ShouldSendPresence(changed, sinceLast, text == "editing"))
        {
            return;
        }
        lastPresenceFingerprint = fingerprint;
        lastPresenceSent = DateTime.UtcNow;
        wire.Send(new Packet
        {
            Kind = "presence",
            Scene = presenceScene,
            Peer = Peer,
            Presence = jsonObject
        });
    }

    private static string BoundPresenceText(string value, int maxLength)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }
        string text = value.Trim();
        return (text.Length <= maxLength) ? text : text.Substring(0, maxLength);
    }

    private static string PresenceText(JsonObject presence, string key, int maxLength)
    {
        try
        {
            string value;
            return (presence?[key] is JsonValue jsonValue && jsonValue.TryGetValue<string>(out value)) ? BoundPresenceText(value, maxLength) : null;
        }
        catch (Exception)
        {
            return null;
        }
    }

    private static string[] ReadSelection(JsonObject presence)
    {
        try
        {
            return presence?["selection"]?.Deserialize<string[]>() ?? Array.Empty<string>();
        }
        catch (Exception)
        {
            return Array.Empty<string>();
        }
    }

    private static bool TryReadPresenceCamera(JsonObject presence, out Vector3 position, out Rotation rotation, out float? fieldOfView)
    {
        position = default;
        rotation = default;
        fieldOfView = null;
        if (presence?["position"] == null || presence["rotation"] == null)
        {
            return false;
        }
        try
        {
            position = presence["position"].Deserialize<Vector3>();
            rotation = presence["rotation"].Deserialize<Rotation>();
            if (presence["fov"] is JsonValue jsonValue && jsonValue.TryGetValue<float>(out var value) && value > 0f && value < 180f)
            {
                fieldOfView = value;
            }
        }
        catch (Exception)
        {
            return false;
        }
        return true;
    }

    private static Frustum PeerFrustum(Vector3 position, Rotation rotation, float fieldOfView)
    {
        float num = MathF.Tan(fieldOfView * 0.5f * ((float)Math.PI / 180f)) * 8f;
        float num2 = num * 1.7777778f;
        Vector3 vector = position + rotation.Forward * 8f;
        Vector3 vector2 = rotation.Up * num;
        Vector3 vector3 = rotation.Right * num2;
        return Frustum.FromCorners(new Ray(position, (vector + vector2 - vector3 - position).Normal), new Ray(position, (vector + vector2 + vector3 - position).Normal), new Ray(position, (vector - vector2 + vector3 - position).Normal), new Ray(position, (vector - vector2 - vector3 - position).Normal), 8f, 300f);
    }

    private static string StatusLineFor(Packet peer, string localScene, string edited, GameObject[] objects)
    {
        if (!string.Equals(peer.Scene, localScene, StringComparison.Ordinal))
        {
            return "scene: " + (BoundPresenceText(peer.Scene, 96) ?? "no saved scene");
        }
        string text = PresenceText(peer.Presence, "state", 32) ?? "idle";
        if (string.Equals(text, "playing", StringComparison.Ordinal))
        {
            return PlaySpectateRules.StatusText(text);
        }
        if (!string.Equals(text, "editing", StringComparison.Ordinal) || edited == null || objects == null)
        {
            return text;
        }
        GameObject gameObject = Array.Find(objects, (GameObject x) => string.Equals(x.Id.ToString(), edited, StringComparison.Ordinal));
        string text2 = ((gameObject == null) ? null : BoundPresenceText(gameObject.Name, 64));
        return (text2 == null) ? text : (text2 + " · " + text);
    }

    public void DrawGizmos(Scene scene)
    {
        DrawnMarkers = 0;
        DrawnSelections = 0;
        string text = scene.Source?.ResourcePath?.ToLowerInvariant();
        if (!Connected || text == null)
        {
            return;
        }
        AvatarCache.Poll();
        DateTime utcNow = DateTime.UtcNow;
        Vector3 vector = Gizmo.Camera?.Position ?? Vector3.Zero;
        GameObject[] array = null;
        foreach (KeyValuePair<string, Packet> item in Roster)
        {
            Packet value = item.Value;
            if (value.Presence == null)
            {
                continue;
            }
            bool flag = string.Equals(value.Scene, text, StringComparison.Ordinal);
            Color color = DockTheme.PeerColor(value.Peer);
            string text2 = BoundPresenceText(value.Name, 64) ?? "(unknown name)";
            string[] array2 = ReadSelection(value.Presence);
            string text3 = PresenceText(value.Presence, "editing", 64);
            if ((array2.Length != 0 || (flag && text3 != null)) && array == null)
            {
                array = SceneAdapter.Walk(scene).ToArray();
            }
            using (Gizmo.Scope("collaboration-" + value.Peer, Transform.Zero))
            {
                bool flag2 = (utcNow < focusedUntil && string.Equals(focusedPeer, item.Key, StringComparison.Ordinal)) || Gizmo.IsHovered;
                if (TryReadPresenceCamera(value.Presence, out var position, out var rotation, out var fieldOfView))
                {
                    float length = (position - vector).Length;
                    bool flag3 = length <= PresenceStyle.FarDistance;
                    string presenceState = PresenceText(value.Presence, "state", 32) ?? "idle";
                    PresenceMarker.Draw(new PresenceMarker.Subject
                    {
                        Position = position,
                        ViewRotation = rotation,
                        FieldOfView = fieldOfView,
                        Name = text2,
                        State = presenceState,
                        StatusLine = flag3 ? StatusLineFor(value, text, text3, array) : null,
                        Avatar = AvatarCache.GetTexture(value.SteamId),
                        SameScene = flag,
                        Emphasized = flag2,
                        ShowFrustum = fieldOfView.HasValue & flag,
                        Frustum = (fieldOfView.HasValue & flag) ? PeerFrustum(position, rotation, fieldOfView.Value) : null
                    });
                    DrawnMarkers++;
                }
                if (array2.Length == 0 || array == null)
                {
                    continue;
                }
                GameObject[] array3 = array;
                foreach (GameObject gameObject in array3)
                {
                    if (array2.Contains(gameObject.Id.ToString()))
                    {
                        Gizmo.Draw.Color = color;
                        Gizmo.Draw.LineThickness = (flag2 ? 2f : 1f);
                        Gizmo.Draw.LineBBox(gameObject.GetBounds());
                        Gizmo.Draw.Text(BoundPresenceText(gameObject.Name, 64) ?? "(unnamed)", new Transform(gameObject.WorldPosition + Vector3.Up * 16f), "Inter");
                        Texture texture2 = AvatarCache.GetTexture(value.SteamId);
                        if (texture2 != null)
                        {
                            Gizmo.Draw.Color = Color.White;
                            Gizmo.Draw.Sprite(gameObject.WorldPosition + Vector3.Up * 24f, 3.0, texture2, worldspace: true);
                        }
                        DrawnSelections++;
                    }
                }
                Gizmo.Draw.LineThickness = 1f;
            }
        }
    }

    public void FocusCamera(string peer)
    {
        FocusNotice = null;
        focusNoticeExpires = default;
        if (string.IsNullOrWhiteSpace(peer) || !Roster.TryGetValue(peer, out var value))
        {
            RefuseFocus("That collaborator is no longer connected.");
            return;
        }
        if (!string.Equals(value.Scene, SceneAdapter.PathOf(SceneEditorSession.Active), StringComparison.Ordinal))
        {
            RefuseFocus("That collaborator is in " + (BoundPresenceText(value.Scene, 96) ?? "no saved scene") + ". Open the scene first.");
            return;
        }
        SceneViewportWidget sceneViewportWidget = SceneViewWidget.Current?.LastSelectedViewportWidget;
        if (sceneViewportWidget?.State == null)
        {
            RefuseFocus("No scene view is open to move.");
            return;
        }
        if (value.Presence?["position"] == null || value.Presence["rotation"] == null)
        {
            RefuseFocus("That collaborator has not reported a camera position yet.");
            return;
        }
        Vector3 cameraPosition;
        Rotation cameraRotation;
        try
        {
            cameraPosition = value.Presence["position"].Deserialize<Vector3>();
            cameraRotation = value.Presence["rotation"].Deserialize<Rotation>();
        }
        catch (Exception)
        {
            RefuseFocus("That collaborator reported a camera position this build cannot read.");
            return;
        }
        sceneViewportWidget.State.CameraPosition = cameraPosition;
        sceneViewportWidget.State.CameraRotation = cameraRotation;
        focusedPeer = peer;
        focusedUntil = DateTime.UtcNow.AddSeconds(1.0);
    }

    private void RefuseFocus(string reason)
    {
        FocusNotice = BoundedDiagnostic(reason);
        focusNoticeExpires = DateTime.UtcNow.AddSeconds(5.0);
    }
}