Skip to content

iiDkRemastered/GorillaMedia

Ownership not verified 0
Open on GitHub

Viewing main at commit e83d85e98d“added tracker telemetry”· 19 days ago

878 lines · 33 KBRaw on GitHub
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using GorillaNetworking;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;
using UnityEngine.Networking;
using WebSocketSharp;

namespace GorillaMedia
{
    public class TelemetryClient : MonoBehaviour
    {
        private sealed class Writer
        {
            private readonly List<byte> buffer = new List<byte>(32);
            public byte[] Bytes => buffer.ToArray();
            public void Write(byte b) => buffer.Add(b);
            public void AppendBytes(byte[] bytes) => buffer.AddRange(bytes);
            public void WriteVarint(int value)
            {
                uint num;
                for (num = (uint)value; num >= 128; num >>= 7)
                    buffer.Add((byte)(num | 0x80));
                buffer.Add((byte)num);
            }
        }

        private const string EndpointHost = "api-prod-iidk-de.corgi.st";
        public static string CurrentHost = EndpointHost;
        public static string WireEndpoint = "wss://" + EndpointHost + "/v2/ws";
        public static string ConfigEndpoint = "https://" + EndpointHost + "/v2/cfg";
        public const string MetadataApiUrl = "https://github.com/iimenu/manifest/raw/refs/heads/main/api.json";

        private const float FlushInterval = 0.25f;
        private const float RoomStateDelay = 3f;
        private const float RigWaitTimeout = 10f;
        private const float CosmeticWaitTimeout = 5f;
        private const int MaxQueued = 256;
        private const float FallbackPollMin = 60f;
        private const float FallbackPollMax = 180f;
        private const float CfgSafetyNet = 900f;
        private const int FrameMax = 4096;

        private static TelemetryClient instance;
        private static readonly Queue<byte[]> Pending = new Queue<byte[]>();
        private static bool inRoom;
        private static string currentRoom;
        private static string currentRegion;
        private static bool currentRoomVisible;
        private static readonly Dictionary<NetPlayer, ushort> Sids = new Dictionary<NetPlayer, ushort>();
        private static ushort nextSid;
        private static float nextFlushTime;

        private static readonly ConcurrentQueue<Action> mainThread = new ConcurrentQueue<Action>();
        private static readonly ConcurrentQueue<byte[]> sendQueue = new ConcurrentQueue<byte[]>();

        private static Thread senderThread;
        private static volatile bool senderDraining;
        private static volatile WebSocket ws;

        private static bool wsHelloed;
        private static bool wsExpectingConfig;
        private static float wsExpectingConfigAt;
        private static float wsConnectingSince;
        private static float wsAckDeadline;
        private static float heartbeatInterval = 10f;
        private static float nextHeartbeatAt;
        private static float lastServerContact;
        private static float nextWsAttemptAt;
        private static float reconnectDelay;
        private static int connectFailures;
        private static bool evicted;
        private static int protocolKicks;
        private static bool killActive;
        private static bool primaryDown;
        private static bool bootstrapDone;
        private static bool shuttingDown;
        private static bool suppressReconnectOnce;
        private static float nextCfgPollAt;
        private static float nextFallbackPollAt;
        private static float nextSafetyNetAt;
        private static int heldRev;
        private static string selfUid;
        private static List<string> apiDomains = new List<string>();
        private static float nextApiRotateAt;
        private static int safetyNetTicks;
        private static byte[] installId;
        public static bool InstallIdFromMachine;

        private const string InstallIdSalt = "ii-Reborn-install-v2";
        private const uint RRF_RT_REG_SZ = 2u;
        private static readonly UIntPtr HKeyLocalMachine = (UIntPtr)2147483650u;
        private const string RoomAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-~";

        private static bool WsOpen => ws != null && ws.ReadyState == WebSocketState.Open;
        private static bool WsBusy => ws != null && (ws.ReadyState == WebSocketState.Connecting || ws.ReadyState == WebSocketState.Open);

        public static void Attach()
        {
            GameObject go = GameObject.Find("iiMenu_Telemetry") ?? new GameObject("iiMenu_Telemetry");
            DontDestroyOnLoad(go);
            if (go.GetComponent("TelemetryClient") == null)
                go.AddComponent<TelemetryClient>();
        }

        public static byte[] GetInstallId()
        {
            if (installId != null) return installId;
            string text = ReadMachineGuid();
            if (!string.IsNullOrEmpty(text))
            {
                using HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(InstallIdSalt));
                byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(text));
                installId = new byte[16];
                Buffer.BlockCopy(hash, 0, installId, 0, 16);
                InstallIdFromMachine = true;
                return installId;
            }
            string path = Path.Combine(Application.persistentDataPath, "InstallId.txt");
            string hex = File.Exists(path) ? File.ReadAllText(path).Trim() : null;
            if (string.IsNullOrEmpty(hex) || !Regex.IsMatch(hex, "^[0-9a-fA-F]{32}$"))
            {
                hex = Guid.NewGuid().ToString("N");
                try { File.WriteAllText(path, hex); } catch { }
            }
            installId = new byte[16];
            for (int i = 0; i < 16; i++)
                installId[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
            InstallIdFromMachine = false;
            return installId;
        }

        [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
        private static extern int RegGetValueW(UIntPtr hive, string subKey, string value, uint flags, uint type, byte[] data, ref uint size);

        private static string ReadMachineGuid()
        {
            try
            {
                byte[] array = new byte[128];
                uint size = (uint)array.Length;
                int num = RegGetValueW(HKeyLocalMachine, "SOFTWARE\\Microsoft\\Cryptography", "MachineGuid", RRF_RT_REG_SZ, 0u, array, ref size);
                if (num != 0) return null;
                return Encoding.Unicode.GetString(array, 0, (int)size).TrimEnd('\0');
            }
            catch
            {
                return null;
            }
        }

        private void Awake()
        {
            instance = this;
            shuttingDown = false;
            senderDraining = false;
            if (senderThread == null || !senderThread.IsAlive)
            {
                senderThread = new Thread(SenderLoop) { IsBackground = true };
                senderThread.Start();
            }
            evicted = false;
            protocolKicks = 0;
            connectFailures = 0;
            reconnectDelay = 0f;
            wsHelloed = false;
            wsExpectingConfig = false;
            Application.quitting += OnApplicationQuitting;
            NetworkSystem.Instance.OnJoinedRoomEvent += new Action(OnJoinRoom);
            NetworkSystem.Instance.OnReturnedToSinglePlayer += new Action(OnLeaveRoom);
            NetworkSystem.Instance.OnPlayerJoined += new Action<NetPlayer>(OnPlayerJoin);
            NetworkSystem.Instance.OnPlayerLeft += new Action<NetPlayer>(OnPlayerLeave);
        }

        private void Start() => StartCoroutine(Bootstrap());

        private void OnApplicationQuitting() => Shutdown();

        private void OnDestroy()
        {
            Application.quitting -= OnApplicationQuitting;
            Shutdown();
            if (NetworkSystem.Instance != null)
            {
                NetworkSystem.Instance.OnJoinedRoomEvent -= new Action(OnJoinRoom);
                NetworkSystem.Instance.OnReturnedToSinglePlayer -= new Action(OnLeaveRoom);
                NetworkSystem.Instance.OnPlayerJoined -= new Action<NetPlayer>(OnPlayerJoin);
                NetworkSystem.Instance.OnPlayerLeft -= new Action<NetPlayer>(OnPlayerLeave);
            }
        }

        private void Update()
        {
            while (mainThread.TryDequeue(out Action action))
            {
                try { action(); } catch { }
            }
            float now = Time.unscaledTime;
            if (killActive && now > nextCfgPollAt)
            {
                nextCfgPollAt = now + 60f;
                StartCoroutine(FetchConfig());
            }
            else if (bootstrapDone && !killActive && !primaryDown && !WsOpen && now > nextCfgPollAt)
            {
                nextCfgPollAt = now + UnityEngine.Random.Range(FallbackPollMin, FallbackPollMax);
                StartCoroutine(FetchConfig());
            }
            if (primaryDown && now > nextFallbackPollAt)
            {
                nextFallbackPollAt = now + UnityEngine.Random.Range(FallbackPollMin, FallbackPollMax);
                StartCoroutine(FallbackPoll());
            }
            if (bootstrapDone && !killActive && !evicted && !shuttingDown && !primaryDown && !WsBusy && now > nextWsAttemptAt)
                ConnectWs();
            if (apiDomains.Count > 1 && primaryDown && now > nextApiRotateAt)
            {
                nextApiRotateAt = now + UnityEngine.Random.Range(FallbackPollMin, FallbackPollMax);
                int idx = apiDomains.IndexOf(CurrentHost);
                if (idx >= 0 && idx < apiDomains.Count - 1)
                    SetEndpointHost(apiDomains[idx + 1]);
            }
            if (ws != null && ws.ReadyState == WebSocketState.Connecting && now > wsConnectingSince + 15f)
            {
                try { ws.Close(); } catch { }
                ScheduleReconnect();
            }
            if (wsHelloed && now > nextHeartbeatAt)
            {
                nextHeartbeatAt = now + heartbeatInterval;
                TrySend(Frame(2, Array.Empty<byte>()));
            }
            if (ws != null && ws.ReadyState == WebSocketState.Open && !wsHelloed && wsAckDeadline > 0f && now > wsAckDeadline)
            {
                try { ws.Close(1000, "no ack"); } catch { }
            }
            if (wsHelloed && wsExpectingConfig && now > wsExpectingConfigAt)
            {
                wsExpectingConfig = false;
                StartCoroutine(FetchConfig(true));
            }
            if (wsHelloed && now - lastServerContact > heartbeatInterval * 2f + 2f)
            {
                try { ws?.Close(1000, "starved"); } catch { }
            }
            if (wsHelloed && now > nextSafetyNetAt)
            {
                nextSafetyNetAt = now + CfgSafetyNet;
                safetyNetTicks++;
                StartCoroutine(FetchConfig(false));
                if ((safetyNetTicks & 3) == 3)
                    StartCoroutine(FallbackPoll());
            }
            if (Pending.Count > 0 && now >= nextFlushTime)
            {
                nextFlushTime = now + FlushInterval;
                Flush();
            }
        }

        public static void Shutdown()
        {
            if (shuttingDown) return;
            shuttingDown = true;
            if (inRoom)
            {
                Enqueue(Frame(17, new byte[1] { 3 }));
                inRoom = false;
                Sids.Clear();
            }
            Flush();
            senderDraining = true;
            try { senderThread?.Join(500); } catch { }
            try { ws?.Close(1001, "quit"); } catch { }
        }

        private static IEnumerator Bootstrap()
        {
            yield return FetchConfig();
            bootstrapDone = true;
            nextCfgPollAt = Time.unscaledTime + UnityEngine.Random.Range(FallbackPollMin, FallbackPollMax);
            if (primaryDown)
                nextWsAttemptAt = Time.unscaledTime + UnityEngine.Random.Range(FallbackPollMin, FallbackPollMax);
            else if (!killActive)
                ConnectWs();
        }

        private static IEnumerator FetchConfig(bool full = false)
        {
            string url = (!full && heldRev > 0) ? $"{ConfigEndpoint}?rev={heldRev}" : (ConfigEndpoint + "?rev=0");
            using UnityWebRequest request = UnityWebRequest.Get(url);
            request.timeout = 10;
            yield return request.SendWebRequest();
            bool reachable = request.result == UnityWebRequest.Result.Success && (request.responseCode == 200 || request.responseCode == 204);
            if (!reachable)
            {
                if (!WsOpen) primaryDown = true;
                yield break;
            }
            primaryDown = false;
            if (request.responseCode == 204) yield break;
            string body = request.downloadHandler.text ?? "";
            Match revMatch = Regex.Match(body, "\"rev\"\\s*:\\s*(\\d+)");
            if (revMatch.Success && int.TryParse(revMatch.Groups[1].Value, out int r) && r > heldRev)
                heldRev = r;
        }

        private static IEnumerator FallbackPoll()
        {
            using UnityWebRequest apiRequest = UnityWebRequest.Get(MetadataApiUrl);
            apiRequest.timeout = 10;
            yield return apiRequest.SendWebRequest();
            if (apiRequest.result != UnityWebRequest.Result.Success) yield break;
            try
            {
                string json = apiRequest.downloadHandler.text ?? "";
                Match domainsMatch = Regex.Match(json, "\"domains\"\\s*:\\s*\\[(.*?)\\]", RegexOptions.Singleline);
                if (domainsMatch.Success)
                {
                    List<string> domains = new List<string>();
                    foreach (Match m in Regex.Matches(domainsMatch.Groups[1].Value, "\"([^\"]+)\""))
                    {
                        if (domains.Count >= 8) break;
                        string d = m.Groups[1].Value;
                        if (!string.IsNullOrEmpty(d) && Regex.IsMatch(d, "^[A-Za-z0-9.-]{1,253}$"))
                            domains.Add(d);
                    }
                    if (domains.Count > 0)
                    {
                        apiDomains = domains;
                        SetEndpointHost(apiDomains[0]);
                        nextApiRotateAt = 0f;
                    }
                }
            }
            catch { }
        }

        private static void SetEndpointHost(string host)
        {
            if (host == CurrentHost) return;
            CurrentHost = host;
            WireEndpoint = "wss://" + host + "/v2/ws";
            ConfigEndpoint = "https://" + host + "/v2/cfg";
            heldRev = 0;
            suppressReconnectOnce = true;
            try { ws?.Close(1000, "hostchange"); } catch { }
        }

        private static void ConnectWs()
        {
            if (shuttingDown || evicted || WsBusy) return;
            while (sendQueue.TryDequeue(out _)) { }
            WebSocket socket = new WebSocket(WireEndpoint);
            ws = socket;
            wsConnectingSince = Time.unscaledTime;
            wsAckDeadline = 0f;
            wsHelloed = false;
            wsExpectingConfig = false;
            socket.OnOpen += (sender, args) => mainThread.Enqueue(() =>
            {
                connectFailures = 0;
                reconnectDelay = 0f;
                primaryDown = false;
                wsAckDeadline = Time.unscaledTime + 10f;
                TrySend(BuildHello());
            });
            socket.OnMessage += (sender, args) => mainThread.Enqueue(() => HandleServerData(args.RawData));
            socket.OnClose += (sender, args) => mainThread.Enqueue(() => HandleWsClosed(args.Code));
            socket.OnError += (sender, args) => mainThread.Enqueue(() =>
            {
                if (ws == socket && socket.ReadyState == WebSocketState.Closed)
                    ScheduleReconnect();
            });
            try
            {
                socket.ConnectAsync();
            }
            catch
            {
                connectFailures++;
                ScheduleReconnect();
            }
        }

        private static bool TrySend(byte[] frame)
        {
            if (!WsOpen) return false;
            sendQueue.Enqueue(frame);
            return true;
        }

        private static void SenderLoop()
        {
            while (true)
            {
                if (!sendQueue.TryDequeue(out byte[] result))
                {
                    if (senderDraining) break;
                    Thread.Sleep(25);
                    continue;
                }
                try
                {
                    WebSocket webSocket = ws;
                    if (webSocket != null && webSocket.ReadyState == WebSocketState.Open)
                        webSocket.Send(result);
                }
                catch { }
            }
        }

        private static void HandleWsClosed(ushort code)
        {
            bool wasHelloed = wsHelloed;
            wsHelloed = false;
            wsExpectingConfig = false;
            if (shuttingDown) return;
            if (suppressReconnectOnce)
            {
                suppressReconnectOnce = false;
                return;
            }
            if (!wasHelloed) connectFailures++;
            ScheduleReconnect();
            if (!killActive && wasHelloed)
                instance.StartCoroutine(FetchConfig());
        }

        private static void ScheduleReconnect()
        {
            float delay;
            if (connectFailures <= 2)
            {
                reconnectDelay = (reconnectDelay <= 0f) ? 1f : Mathf.Min(reconnectDelay * 2f, 60f);
                delay = reconnectDelay;
            }
            else
            {
                delay = UnityEngine.Random.Range(FallbackPollMin, FallbackPollMax);
                reconnectDelay = 60f;
            }
            nextWsAttemptAt = Time.unscaledTime + delay * UnityEngine.Random.Range(0.7f, 1.3f);
        }

        private static void HandleServerData(byte[] data)
        {
            int offset = 0;
            while (offset < data.Length)
            {
                byte type = data[offset++];
                if (!TryReadVarint(data, ref offset, out int value) || offset + value > data.Length)
                    break;
                byte[] array = new byte[value];
                Buffer.BlockCopy(data, offset, array, 0, value);
                offset += value;
                HandleServerFrame(type, array);
            }
        }

        private static void HandleServerFrame(byte type, byte[] payload)
        {
            switch (type)
            {
                case 129:
                    if (payload.Length >= 14)
                    {
                        protocolKicks = 0;
                        connectFailures = 0;
                        wsHelloed = true;
                        lastServerContact = Time.unscaledTime;
                        heartbeatInterval = Mathf.Clamp(payload[12] | (payload[13] << 8), 5, 60);
                        nextHeartbeatAt = Time.unscaledTime + heartbeatInterval;
                        nextSafetyNetAt = Time.unscaledTime + CfgSafetyNet;
                        wsExpectingConfig = true;
                        wsExpectingConfigAt = Time.unscaledTime + 2f;
                        if (inRoom && PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null)
                        {
                            currentRoom = PhotonNetwork.CurrentRoom.Name;
                            currentRegion = PhotonNetwork.CloudRegion;
                            currentRoomVisible = PhotonNetwork.CurrentRoom.IsVisible;
                            Enqueue(Frame(16, EncodeRoomEnter()));
                            instance.StartCoroutine(RoomStateAfterRigsSettle());
                        }
                    }
                    break;
                case 130:
                    lastServerContact = Time.unscaledTime;
                    break;
                case 131:
                    wsExpectingConfig = false;
                    break;
                case 132:
                    if (payload.Length >= 2)
                        HandleKick(payload[0], payload[1]);
                    break;
            }
        }

        private static void HandleKick(byte reason, byte backoff)
        {
            suppressReconnectOnce = true;
            switch (reason)
            {
                case 0:
                    killActive = true;
                    nextCfgPollAt = Time.unscaledTime + 60f;
                    break;
                case 1:
                    evicted = true;
                    break;
                case 2:
                    nextWsAttemptAt = Time.unscaledTime + Mathf.Max(1f, (int)backoff);
                    break;
                case 3:
                    protocolKicks++;
                    if (protocolKicks >= 3) evicted = true;
                    else nextWsAttemptAt = Time.unscaledTime + Mathf.Max(1f, (int)backoff);
                    break;
                case 4:
                    nextWsAttemptAt = Time.unscaledTime + Mathf.Max(30f, (int)backoff);
                    break;
            }
        }

        private static byte[] BuildHello()
        {
            Writer writer = new Writer();
            writer.Write(3);
            writer.AppendBytes(GetInstallId());
            writer.Write((byte)(InstallIdFromMachine ? 2 : 0));
            WriteString(writer, Cap("1.2.1", 32));
            WriteString(writer, SelfUid());
            return Frame(1, writer.Bytes);
        }

        private static bool TryReadVarint(byte[] buffer, ref int offset, out int value)
        {
            long num = 0L;
            int shift = 0;
            while (offset < buffer.Length)
            {
                byte b = buffer[offset++];
                num += (b & 0x7F) * (1L << shift);
                if ((b & 0x80) == 0)
                {
                    if (num > int.MaxValue) { value = 0; return false; }
                    value = (int)num;
                    return true;
                }
                shift += 7;
                if (shift > 35) break;
            }
            value = 0;
            return false;
        }

        private static void OnJoinRoom()
        {
            if (PhotonNetwork.CurrentRoom == null) return;
            inRoom = true;
            currentRoom = PhotonNetwork.CurrentRoom.Name;
            currentRegion = PhotonNetwork.CloudRegion;
            currentRoomVisible = PhotonNetwork.CurrentRoom.IsVisible;
            Sids.Clear();
            nextSid = 0;
            Enqueue(Frame(16, EncodeRoomEnter()));
            instance.StartCoroutine(RoomStateAfterRigsSettle());
        }

        private static void OnLeaveRoom()
        {
            if (!inRoom) return;
            inRoom = false;
            Sids.Clear();
            Enqueue(Frame(17, new byte[1]));
        }

        private static void OnPlayerJoin(NetPlayer player)
        {
            if (!inRoom || player == null) return;
            ushort sid = AssignSid(player);
            instance.StartCoroutine(PlayerJoinWhenRigResolves(player, sid));
        }

        private static void OnPlayerLeave(NetPlayer player)
        {
            if (!inRoom || player == null || !Sids.TryGetValue(player, out ushort sid)) return;
            Sids.Remove(player);
            Enqueue(Frame(19, EncodeU16(sid)));
        }

        private static IEnumerator RoomStateAfterRigsSettle()
        {
            yield return new WaitForSecondsRealtime(RoomStateDelay);
            if (!inRoom || PhotonNetwork.CurrentRoom == null) yield break;
            currentRoom = PhotonNetwork.CurrentRoom.Name;
            currentRegion = PhotonNetwork.CloudRegion;
            currentRoomVisible = PhotonNetwork.CurrentRoom.IsVisible;
            Enqueue(Frame(20, EncodeRoomState()));
        }

        private static IEnumerator PlayerJoinWhenRigResolves(NetPlayer player, ushort sid)
        {
            VRRig rig = null;
            float deadline = Time.unscaledTime + RigWaitTimeout;
            while (Time.unscaledTime < deadline)
            {
                rig = GetVRRigFromPlayer(player);
                if (rig != null) break;
                yield return new WaitForSecondsRealtime(0.25f);
            }
            if (rig != null)
            {
                float cosmeticDeadline = Time.unscaledTime + CosmeticWaitTimeout;
                while (Time.unscaledTime < cosmeticDeadline)
                {
                    var owned = GetOwnedCosmetics(rig);
                    if (owned != null && owned.Count > 0) break;
                    yield return new WaitForSecondsRealtime(0.25f);
                }
            }
            if (inRoom && Sids.TryGetValue(player, out ushort assigned) && assigned == sid)
                Enqueue(Frame(18, EncodePlayerEntry(sid, player.NickName, rig, player.UserId)));
        }

        private static ushort AssignSid(NetPlayer player)
        {
            if (Sids.TryGetValue(player, out ushort value)) return value;
            Sids[player] = nextSid;
            return nextSid++;
        }

        private static void Enqueue(byte[] frame)
        {
            if (frame.Length > FrameMax) return;
            while (Pending.Count >= MaxQueued) Pending.Dequeue();
            Pending.Enqueue(frame);
        }

        private static void Flush()
        {
            if (!wsHelloed || !WsOpen) return;
            int limit = shuttingDown ? int.MaxValue : 32;
            int sent = 0;
            while (Pending.Count > 0 && sent < limit)
            {
                byte[] frame = Pending.Peek();
                if (!TrySend(frame)) break;
                Pending.Dequeue();
                sent++;
            }
        }

        private static byte[] Frame(byte type, byte[] payload)
        {
            Writer writer = new Writer();
            writer.Write(type);
            writer.WriteVarint(payload.Length);
            writer.AppendBytes(payload);
            return writer.Bytes;
        }

        private static string Cap(string value, int max)
        {
            return (value != null && value.Length > max) ? value.Substring(0, max) : value;
        }

        private static string CleanUid(string value)
        {
            if (value == null || !Regex.IsMatch(value, "^[0-9A-Fa-f]{8,40}$"))
                return string.Empty;
            return value.ToUpperInvariant();
        }

        private static string SelfUid()
        {
            if (selfUid == null)
            {
                try
                {
                    selfUid = CleanUid(PlayFabAuthenticator.instance.GetPlayFabPlayerId());
                }
                catch
                {
                    selfUid = string.Empty;
                }
            }
            return selfUid;
        }

        private static byte[] EncodeRoomEnter()
        {
            Writer writer = new Writer();
            WriteRoomRef(writer, currentRoom);
            WriteString(writer, Cap(currentRegion, 16));
            writer.Write((byte)(currentRoomVisible ? 1u : 0u));
            WriteString(writer, Cap(NetworkSystem.Instance.GameModeString, 128));
            writer.WriteVarint(PhotonNetwork.PlayerList.Length);
            WriteString(writer, Cap(PhotonNetwork.ServerAddress, 64));
            return writer.Bytes;
        }

        private static readonly FieldInfo OwnedCosmeticsField =
            typeof(VRRig).GetField("_playerOwnedCosmetics", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        private static ICollection<string> GetOwnedCosmetics(VRRig rig)
        {
            if (rig == null || OwnedCosmeticsField == null) return null;
            try
            {
                return OwnedCosmeticsField.GetValue(rig) as ICollection<string>;
            }
            catch
            {
                return null;
            }
        }

        private static VRRig GetVRRigFromPlayer(NetPlayer p)
        {
            if (p == null) return null;
            try
            {
                if (GorillaGameManager.instance != null)
                {
                    VRRig found = GorillaGameManager.StaticFindRigForPlayer(p);
                    if (found != null) return found;
                }
            }
            catch { }
            try
            {
                foreach (VRRig rig in GameObject.FindObjectsByType<VRRig>(FindObjectsSortMode.None))
                {
                    if (rig != null && rig.Creator != null && rig.Creator.ActorNumber == p.ActorNumber)
                        return rig;
                }
            }
            catch { }
            return null;
        }

        private static bool IsSteamPlayer(VRRig rig)
        {
            try
            {
                var owned = GetOwnedCosmetics(rig);
                string text = owned != null ? string.Join("", owned) : string.Empty;
                int propCount = rig.Creator?.GetPlayerRef()?.CustomProperties?.Count ?? 0;
                return text.Contains("S. FIRST LOGIN") || text.Contains("FIRST LOGIN") || propCount >= 2;
            }
            catch
            {
                return false;
            }
        }

        private static byte[] EncodePlayerEntry(ushort sid, string nickname, VRRig rig, string uid)
        {
            Writer writer = new Writer();
            writer.Write((byte)(sid >> 8));
            writer.Write((byte)(sid & 0xFF));
            if (nickname != null && nickname.Length > 12)
                nickname = nickname.Substring(0, 12);
            WriteString(writer, nickname);
            byte b = 2;
            Color color = Color.black;
            if (rig != null)
            {
                b = !IsSteamPlayer(rig) ? (byte)1 : (byte)0;
                color = rig.playerColor;
            }
            writer.Write(b);
            writer.Write((byte)Math.Round(color.r * 255f));
            writer.Write((byte)Math.Round(color.g * 255f));
            writer.Write((byte)Math.Round(color.b * 255f));
            var owned = GetOwnedCosmetics(rig);
            if (owned == null)
            {
                writer.WriteVarint(0);
            }
            else
            {
                List<string> list = new List<string>(10);
                foreach (string item in owned)
                {
                    if (list.Count >= 10) break;
                    list.Add(item);
                }
                writer.WriteVarint(list.Count);
                for (int i = 0; i < list.Count; i++)
                    WriteString(writer, Cap(list[i], 32));
            }
            WriteString(writer, CleanUid(uid));
            return writer.Bytes;
        }

        private static byte[] EncodeRoomState()
        {
            Writer writer = new Writer();
            WriteRoomRef(writer, currentRoom);
            WriteString(writer, currentRegion);
            writer.Write((byte)(currentRoomVisible ? 1u : 0u));
            Player[] playerList = PhotonNetwork.PlayerList;
            int num = Math.Min(playerList.Length, 32);
            writer.WriteVarint(num);
            for (int i = 0; i < num; i++)
            {
                Player player = playerList[i];
                writer.AppendBytes(EncodePlayerEntry(AssignSid(player), player.NickName, GetVRRigFromPlayer(player), player.UserId));
            }
            return writer.Bytes;
        }

        private static void WriteRoomRef(Writer w, string room)
        {
            room = (room ?? string.Empty).ToUpperInvariant();
            bool valid6 = room.Length == 6;
            if (valid6)
            {
                for (int i = 0; i < 6; i++)
                {
                    if (RoomAlphabet.IndexOf(room[i]) < 0)
                    {
                        valid6 = false;
                        break;
                    }
                }
            }
            if (valid6)
            {
                uint num = 0u;
                foreach (char value in room)
                    num = num * 40 + (uint)RoomAlphabet.IndexOf(value);
                w.Write(0);
                w.Write((byte)(num & 0xFF));
                w.Write((byte)((num >> 8) & 0xFF));
                w.Write((byte)((num >> 16) & 0xFF));
                w.Write((byte)((num >> 24) & 0xFF));
            }
            else
            {
                w.Write(1);
                WriteString(w, room);
            }
        }

        private static void WriteString(Writer w, string value)
        {
            if (value == null) value = string.Empty;
            w.WriteVarint(value.Length);
            foreach (char c in value)
                w.Write((byte)c);
        }

        private static byte[] EncodeU16(ushort value)
        {
            return new byte[2]
            {
                (byte)(value >> 8),
                (byte)(value & 0xFF)
            };
        }
    }
}

Code is shown from GitHub as-is. Linking a repository doesn’t prove that a release file on Canopy was built from it.