Editor/Core/CollaborationConfig.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace TeamCreate;

public sealed class CollaborationConfig
{
    public string ProjectId;

    public string Org = "cyn";

    public string LibraryIdent;

    public Version LibraryVersion;

    public bool RequireSteamFriend = true;

    public bool RequireOrgMember = true;

    public bool RequirePublished = true;

    public string[] OrgMemberAllowlist = Array.Empty<string>();

    // Where friends behind home routers meet. Not a secret: it names a server, and the credential that
    // lets a host use it lives in .collaboration/relay-access-key.txt, which Git ignores. Null means the
    // project opted out of the relay and only the direct routes are offered.
    public string RelayUrl;

    public static CollaborationConfig Defaults()
    {
        return new CollaborationConfig();
    }

    public static CollaborationConfig LoadFrom(string path, out string error)
    {
        error = null;
        try
        {
            if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
            {
                return Defaults();
            }
            string json;
            try
            {
                json = File.ReadAllText(path);
            }
            catch (Exception ex) when ((ex is IOException || ex is UnauthorizedAccessException) ? true : false)
            {
                error = "Collaboration config " + path + " could not be read: " + ex.Message;
                return Defaults();
            }
            JsonNode jsonNode;
            try
            {
                jsonNode = JsonNode.Parse(json);
            }
            catch (JsonException ex2)
            {
                error = "Collaboration config " + path + " is not valid JSON: " + ex2.Message;
                return Defaults();
            }
            if (!(jsonNode is JsonObject document))
            {
                error = "Collaboration config " + path + " must contain a JSON object.";
                return Defaults();
            }
            return Read(document, out error);
        }
        catch (Exception ex3)
        {
            error = "Collaboration config " + path + " could not be loaded: " + ex3.Message;
            return Defaults();
        }
    }

    private static CollaborationConfig Read(JsonObject document, out string error)
    {
        CollaborationConfig config = Defaults();
        List<string> list = new List<string>();
        if (document.TryGetPropertyValue("ProjectId", out JsonNode jsonNode) && jsonNode != null)
        {
            string text = Text(jsonNode);
            if (text == null || !Guid.TryParse(text, out var result))
            {
                list.Add("ProjectId must be a GUID such as 2e9f8f81-8b38-4207-ad3e-4e7c0bea567f.");
            }
            else
            {
                config.ProjectId = result.ToString("D");
            }
        }
        if (document.TryGetPropertyValue("Org", out JsonNode jsonNode2) && jsonNode2 != null)
        {
            string text2 = Text(jsonNode2);
            if (text2 == null)
            {
                list.Add("Org must be a string.");
            }
            else if (string.IsNullOrWhiteSpace(text2))
            {
                list.Add("Org must be an organization ident such as cyn.");
            }
            else
            {
                config.Org = text2.Trim();
            }
        }
        if (document.TryGetPropertyValue("LibraryIdent", out JsonNode jsonNode3) && jsonNode3 != null)
        {
            string text3 = Text(jsonNode3);
            if (text3 == null)
            {
                list.Add("LibraryIdent must be a string.");
            }
            else
            {
                config.LibraryIdent = text3;
            }
        }
        if (document.TryGetPropertyValue("LibraryVersion", out JsonNode jsonNode4) && jsonNode4 != null)
        {
            string text4 = Text(jsonNode4);
            if (text4 == null || !Version.TryParse(text4, out Version result2))
            {
                list.Add("LibraryVersion must be a system version such as 1.0.0.");
            }
            else
            {
                config.LibraryVersion = result2;
            }
        }
        ReadFlag(document, "RequireSteamFriend", list, (bool value) =>
        {
            config.RequireSteamFriend = value;
        });
        ReadFlag(document, "RequireOrgMember", list, (bool value) =>
        {
            config.RequireOrgMember = value;
        });
        ReadFlag(document, "RequirePublished", list, (bool value) =>
        {
            config.RequirePublished = value;
        });
        if (document.TryGetPropertyValue("OrgMemberAllowlist", out JsonNode jsonNode5) && jsonNode5 != null)
        {
            if (!(jsonNode5 is JsonArray jsonArray))
            {
                list.Add("OrgMemberAllowlist must be an array of decimal account IDs.");
            }
            else
            {
                List<string> list2 = new List<string>();
                List<string> list3 = new List<string>();
                foreach (JsonNode item in jsonArray)
                {
                    string text5 = Text(item);
                    if (text5 != null && ulong.TryParse(text5, NumberStyles.None, CultureInfo.InvariantCulture, out var result3))
                    {
                        list2.Add(result3.ToString(CultureInfo.InvariantCulture));
                    }
                    else
                    {
                        list3.Add((text5 == null) ? "<non-string>" : text5);
                    }
                }
                config.OrgMemberAllowlist = list2.ToArray();
                if (list3.Count > 0)
                {
                    list.Add("OrgMemberAllowlist entries ignored because they are not decimal account IDs: " + string.Join(", ", list3) + ".");
                }
            }
        }
        if (document.TryGetPropertyValue("RelayUrl", out JsonNode relayNode) && relayNode != null)
        {
            string relayText = Text(relayNode);
            if (relayText == null)
            {
                list.Add("RelayUrl must be a string such as wss://relay.example.com/relay.");
            }
            else if (string.Equals(relayText.Trim(), RelayDefaults.Off, StringComparison.OrdinalIgnoreCase))
            {
                config.RelayUrl = RelayDefaults.Off;
            }
            else if (!string.IsNullOrWhiteSpace(relayText))
            {
                if (RelayProtocol.TryValidateUrl(relayText, out Uri relayUri, out string relayError))
                {
                    config.RelayUrl = relayUri.ToString();
                }
                else
                {
                    list.Add("RelayUrl is not usable: " + relayError);
                }
            }
        }
        error = ((list.Count == 0) ? null : string.Join(" ", list));
        return config;
    }

    private static void ReadFlag(JsonObject document, string name, List<string> problems, Action<bool> assign)
    {
        if (document.TryGetPropertyValue(name, out JsonNode jsonNode) && jsonNode != null)
        {
            if (jsonNode is JsonValue jsonValue && jsonValue.TryGetValue<bool>(out var value))
            {
                assign(value);
            }
            else
            {
                problems.Add(name + " must be true or false.");
            }
        }
    }

    private static string Text(JsonNode node)
    {
        string value;
        return (node is JsonValue jsonValue && jsonValue.TryGetValue<string>(out value)) ? value : null;
    }
}

public static class VersionRules
{
    public static bool IsOlder(Version version, Version required)
    {
        return Compare(version, required) < 0;
    }

    public static bool IsAtLeast(Version version, Version required)
    {
        return Compare(version, required) >= 0;
    }

    private static int Compare(Version version, Version required)
    {
        if ((object)version == null)
        {
            return ((object)required != null) ? (-1) : 0;
        }
        if ((object)required == null)
        {
            return 1;
        }
        return Normalized(version).CompareTo(Normalized(required));
    }

    private static (int Major, int Minor, int Build, int Revision) Normalized(Version version)
    {
        return (Major: version.Major, Minor: version.Minor, Build: (version.Build >= 0) ? version.Build : 0, Revision: (version.Revision >= 0) ? version.Revision : 0);
    }
}

public enum CollaborationMode
{
    Source,
    RuntimeExperimental
}

public enum RuntimeEnableResult
{
    Ok,
    AlreadyEnabled,
    RunAlreadyActive,
    InvalidRunId,
    InvalidScenePath
}