using System.IO; using System.Text.Json; using System.Text.Json.Serialization; namespace DorkNet.Launcher.Backend; /// Persisted user choices that should survive across launches: /// host-vs-join mode (asked on first launch, switchable in settings), /// the picked Rec Room install path, the selected server version, and /// any Photon AppIds the user has configured. Serialized as JSON at /// . public sealed class AppState { [JsonPropertyName("mode")] public AppMode Mode { get; set; } = AppMode.Unset; /// Path to the user's Recroom_Release_Data folder. First /// run auto-fills via (scans /// common manual-install paths only, never Steam library); user /// can override via the folder dialog. [JsonPropertyName("recRoomPath")] public string? RecRoomPath { get; set; } /// The version key currently selected for host mode /// (e.g. december_2020_12_18). For join mode this is set /// from the parsed join code. [JsonPropertyName("selectedVersion")] public string? SelectedVersion { get; set; } /// Photon Realtime AppId for host mode. The user gets /// their own (free at dashboard.photonengine.com); DorkNet never /// ships one. Empty until set. [JsonPropertyName("photonAppId")] public string PhotonAppId { get; set; } = string.Empty; [JsonPropertyName("photonVoiceAppId")] public string PhotonVoiceAppId { get; set; } = string.Empty; [JsonPropertyName("photonRegion")] public string PhotonRegion { get; set; } = "us"; /// How the host exposes the server: SingleOriginTunnel /// (default) opens one public Localtunnel HTTPS URL plus a /// launcher-hosted local name server; LocalNetwork binds on /// the LAN and uses an sslip.io hostname in the join code (joiners /// must be on the same network); RemoteWildcard is the /// legacy provider-owned-apex mode kept for back-compat. [JsonPropertyName("hostingMode")] public HostingMode HostingMode { get; set; } = HostingMode.SingleOriginTunnel; /// Provider-owned wildcard tunnel base for remote hosting /// without Cloudflare, e.g. dorknet.tunnelto.me. The server /// publishes service URLs under api.{base}, /// auth.{base}, etc. [JsonPropertyName("remoteWildcardApex")] public string RemoteWildcardApex { get; set; } = string.Empty; /// Host's server name shown in the join code. Empty until /// the user fills it in on first-run host setup. [JsonPropertyName("serverName")] public string ServerName { get; set; } = string.Empty; /// True once the user has dismissed the branded welcome /// screen. Subsequent launches skip straight to the setup wizard /// (or the main host/join view if setup is also done). [JsonPropertyName("welcomeSeen")] public bool WelcomeSeen { get; set; } /// True once the user has finished the multi-step setup /// wizard. Future launches skip the wizard and open the /// host/join view directly. Settings -> "Re-run setup" can clear /// this if the user wants to walk through again. [JsonPropertyName("setupComplete")] public bool SetupComplete { get; set; } /// When true, Launch Rec Room passes +forcemode:screen /// so the game opens in 2D desktop mode instead of looking for a /// VR headset. Toggled from a checkbox next to the Launch button. [JsonPropertyName("launchInScreenMode")] public bool LaunchInScreenMode { get; set; } /// When true, the patched ClientMod skips BouncyCastle's /// TLS chain validation — needed only for self-signed certs on a /// LAN-only / dev tunnel. Localtunnel + Tunnelto both terminate /// TLS with valid certs, so the default is off. Leaving it on /// triggers a CLR detour-compile crash (0x80131506) on the /// december build — see project_build_layout_per_branch + /// MonoMod's CompileMethodHook comment in /// DorkNet.ClientMod/Mod.cs. [JsonPropertyName("enableTlsTrustBypass")] public bool EnableTlsTrustBypass { get; set; } private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, }; public static AppState Load() { if (!File.Exists(AppPaths.StateFile)) return new AppState(); try { var json = File.ReadAllText(AppPaths.StateFile); var state = JsonSerializer.Deserialize(json, JsonOptions) ?? new AppState(); // Backfill the welcome/setup flags for users upgrading from a // pre-wizard build. If they already had a working host setup // they've effectively passed those screens — don't drag them // back through the wizard on first launch of the new build. var migrated = false; if (!state.WelcomeSeen && state.Mode != AppMode.Unset) { state.WelcomeSeen = true; migrated = true; } if (!state.SetupComplete && state.Mode != AppMode.Unset && !string.IsNullOrEmpty(state.RecRoomPath) && (state.Mode == AppMode.Join || !string.IsNullOrEmpty(state.PhotonAppId))) { state.SetupComplete = true; migrated = true; } // Flush the migration so subsequent launches read the // backfilled values directly from disk — keeps the file as // the authoritative source instead of needing to re-derive // them every load. if (migrated) { try { state.Save(); } catch { } } if (state.HostingMode is HostingMode.RemoteWildcard or HostingMode.Internet) { state.HostingMode = HostingMode.SingleOriginTunnel; try { state.Save(); } catch { } } return state; } catch { // Corrupt state file — start fresh rather than crash on every // launch. The user's mode pick + Photon config gets lost but // they can re-enter; the alternative is unrecoverable. return new AppState(); } } public void Save() { var json = JsonSerializer.Serialize(this, JsonOptions); File.WriteAllText(AppPaths.StateFile, json); } } public enum AppMode { /// First launch — show the host-vs-join wizard. Unset, Host, Join, } public enum HostingMode { /// Easy Launcher remote mode — one public HTTPS tunnel URL. /// The launcher answers the client's localhost name-server bootstrap /// and points every RecNet service at path prefixes under that URL. SingleOriginTunnel, /// Legacy "Friends anywhere" mode kept for migration /// purposes only. New installs are normalised to /// at time /// (see the migration block there). Internet, /// LAN only — bind on all local interfaces, skip the /// tunnel, and publish an sslip.io hostname for the machine's LAN /// IP. Joiners must be on the same network. LocalNetwork, /// Remote tunnel supplied by another provider. The user /// provides a wildcard-capable provider hostname, and the launcher /// uses service subdomains under that base. RemoteWildcard, }