Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RunicSentinelClient v1.0.0
RunicSentinelClient.dll
Decompiled 2 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using HarmonyLib; using Microsoft.CodeAnalysis; using RunicSentinel.Admission; using RunicSentinelClient.Runtime; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Runic Sentinel Client")] [assembly: AssemblyDescription("Minimal client-only profile reporter for authoritative Runic Sentinel admission")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic Sentinel Client")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: InternalsVisibleTo("RunicSentinelClient.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RunicSentinel.Admission { internal sealed class AdmissionPluginEvidence { internal string Id { get; } internal string Version { get; } internal string Sha256 { get; } internal AdmissionPluginEvidence(string id, string version, string sha256) { Id = AdmissionValidation.RequireAtom(id, 1, 128, "id"); Version = AdmissionValidation.RequireAtom(version, 1, 64, "version"); Sha256 = AdmissionValidation.RequireLowerHex(sha256, 64, "sha256"); } } internal sealed class AdmissionClientProfile { internal long CapturedUnixSeconds { get; } internal string Digest { get; } internal IReadOnlyList<AdmissionPluginEvidence> Plugins { get; } internal AdmissionClientProfile(long capturedUnixSeconds, string digest, IList<AdmissionPluginEvidence> plugins) { if (capturedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("capturedUnixSeconds"); } CapturedUnixSeconds = capturedUnixSeconds; Digest = AdmissionValidation.RequireLowerHex(digest, 64, "digest"); if (plugins == null) { throw new ArgumentNullException("plugins"); } Plugins = new ReadOnlyCollection<AdmissionPluginEvidence>(new List<AdmissionPluginEvidence>(plugins)); } } internal sealed class AdmissionChallenge { private readonly byte[] _nonce; internal string RequestId { get; } internal byte[] Nonce => (byte[])_nonce.Clone(); internal long IssuedUnixSeconds { get; } internal long DeadlineUnixSeconds { get; } internal AdmissionChallenge(string requestId, byte[] nonce, long issuedUnixSeconds, long deadlineUnixSeconds) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); if (nonce == null || nonce.Length != 32) { throw new ArgumentOutOfRangeException("nonce"); } if (issuedUnixSeconds < 0 || deadlineUnixSeconds < issuedUnixSeconds) { throw new ArgumentOutOfRangeException("issuedUnixSeconds"); } _nonce = (byte[])nonce.Clone(); IssuedUnixSeconds = issuedUnixSeconds; DeadlineUnixSeconds = deadlineUnixSeconds; } } internal sealed class AdmissionReport { internal string RequestId { get; } internal string ClientVersion { get; } internal long CapturedUnixSeconds { get; } internal long IssuedUnixSeconds { get; } internal string ProfileDigest { get; } internal string NonceBinding { get; } internal IReadOnlyList<AdmissionPluginEvidence> Plugins { get; } internal AdmissionReport(string requestId, string clientVersion, long capturedUnixSeconds, long issuedUnixSeconds, string profileDigest, string nonceBinding, IList<AdmissionPluginEvidence> plugins) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); ClientVersion = AdmissionValidation.RequireAtom(clientVersion, 1, 32, "clientVersion"); if (capturedUnixSeconds < 0 || issuedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("capturedUnixSeconds"); } ProfileDigest = AdmissionValidation.RequireLowerHex(profileDigest, 64, "profileDigest"); NonceBinding = AdmissionValidation.RequireLowerHex(nonceBinding, 64, "nonceBinding"); if (plugins == null || plugins.Count > 512) { throw new ArgumentOutOfRangeException("plugins"); } CapturedUnixSeconds = capturedUnixSeconds; IssuedUnixSeconds = issuedUnixSeconds; Plugins = new ReadOnlyCollection<AdmissionPluginEvidence>(new List<AdmissionPluginEvidence>(plugins)); } } internal sealed class AdmissionDecisionMessage { internal string RequestId { get; } internal bool Accepted { get; } internal bool ResumeHandshake { get; } internal string ReasonCode { get; } internal long PolicySequence { get; } internal string PolicyProfile { get; } internal long IssuedUnixSeconds { get; } internal AdmissionDecisionMessage(string requestId, bool accepted, bool resumeHandshake, string reasonCode, long policySequence, string policyProfile, long issuedUnixSeconds) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); ReasonCode = AdmissionValidation.RequireReason(reasonCode, "reasonCode"); if (policySequence < 0 || issuedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("policySequence"); } if (!string.IsNullOrEmpty(policyProfile)) { AdmissionValidation.RequireAtom(policyProfile, 1, 64, "policyProfile"); } if (resumeHandshake && !accepted) { throw new ArgumentException("Only an accepted decision may resume the native handshake.", "resumeHandshake"); } Accepted = accepted; ResumeHandshake = resumeHandshake; PolicySequence = policySequence; PolicyProfile = policyProfile ?? string.Empty; IssuedUnixSeconds = issuedUnixSeconds; } } internal static class AdmissionValidation { internal static string RequireAtom(string value, int minimum, int maximum, string parameterName) { if (!IsAtom(value, minimum, maximum)) { throw new ArgumentException("Value is not a canonical ASCII atom.", parameterName); } return value; } internal static bool IsAtom(string value, int minimum, int maximum) { if (value == null || value.Length < minimum || value.Length > maximum) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { return false; } } return true; } internal static string RequireLowerHex(string value, int length, string parameterName) { if (!IsLowerHex(value, length)) { throw new ArgumentException("Value is not canonical lowercase hexadecimal.", parameterName); } return value; } internal static bool IsLowerHex(string value, int length) { if (value == null || value.Length != length) { return false; } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { return false; } } return true; } internal static string RequireReason(string value, string parameterName) { if (!IsReason(value)) { throw new ArgumentException("Reason code is not canonical.", parameterName); } return value; } internal static bool IsReason(string value) { if (value == null || value.Length < 1 || value.Length > 96) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-' && c != '.') { return false; } } return true; } } internal static class AdmissionProfileCanonicalizer { internal const int MaximumCanonicalBytes = 262144; private const string Header = "RUNIC-SENTINEL-CLIENT-PROFILE/1\n"; internal static bool TryCreate(IEnumerable<AdmissionPluginEvidence> source, long capturedUnixSeconds, out AdmissionClientProfile profile, out string failure) { profile = null; failure = string.Empty; if (source == null || capturedUnixSeconds < 0) { failure = "profile-missing"; return false; } List<AdmissionPluginEvidence> list = new List<AdmissionPluginEvidence>(); foreach (AdmissionPluginEvidence item in source) { if (item == null) { failure = "profile-entry-null"; return false; } if (list.Count >= 512) { failure = "profile-plugin-cap"; return false; } list.Add(item); } list.Sort((AdmissionPluginEvidence left, AdmissionPluginEvidence right) => string.CompareOrdinal(left.Id, right.Id)); string a = null; StringBuilder stringBuilder = new StringBuilder("RUNIC-SENTINEL-CLIENT-PROFILE/1\n", Math.Min(32768, 64 + list.Count * 160)); foreach (AdmissionPluginEvidence item2 in list) { if (string.Equals(a, item2.Id, StringComparison.Ordinal)) { failure = "profile-plugin-duplicate"; return false; } a = item2.Id; stringBuilder.Append(item2.Id).Append('|').Append(item2.Version) .Append('|') .Append(item2.Sha256) .Append('\n'); if (stringBuilder.Length > 262144) { failure = "profile-size"; return false; } } byte[] bytes = Encoding.UTF8.GetBytes(stringBuilder.ToString()); if (bytes.Length > 262144) { failure = "profile-size"; return false; } string digest; using (SHA256 sHA = SHA256.Create()) { digest = Hex(sHA.ComputeHash(bytes)); } profile = new AdmissionClientProfile(capturedUnixSeconds, digest, list); return true; } internal static string Hex(byte[] bytes) { if (bytes == null) { throw new ArgumentNullException("bytes"); } StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); for (int i = 0; i < bytes.Length; i++) { stringBuilder.Append(bytes[i].ToString("x2")); } return stringBuilder.ToString(); } internal static bool FixedTimeEquals(string left, string right) { if (left == null || right == null) { return false; } int num = left.Length ^ right.Length; int num2 = Math.Max(left.Length, right.Length); for (int i = 0; i < num2; i++) { char c = ((i < left.Length) ? left[i] : '\0'); char c2 = ((i < right.Length) ? right[i] : '\0'); num |= c ^ c2; } return num == 0; } } internal enum AdmissionMessageKind : byte { Challenge = 1, Report, Decision } internal static class AdmissionProtocolV2 { internal const string DirectRpcName = "chazman.RunicSentinel.Admission.v2"; internal const int WireSchema = 2; internal const int MaximumPlugins = 512; internal const int MaximumFrameBytes = 262144; internal const int NonceBytes = 32; internal const int RequestIdHexLength = 32; internal const long MaximumClockSkewSeconds = 60L; internal const long MaximumMessageAgeSeconds = 300L; internal const long MaximumChallengeLifetimeSeconds = 120L; private const int Magic = 843141970; private const int Terminal = 843337285; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static AdmissionChallenge CreateChallenge(long nowUnixSeconds, long lifetimeSeconds) { if (nowUnixSeconds < 0 || lifetimeSeconds < 1 || lifetimeSeconds > 120 || nowUnixSeconds > long.MaxValue - lifetimeSeconds) { throw new ArgumentOutOfRangeException("lifetimeSeconds"); } byte[] array = new byte[32]; byte[] array2 = new byte[16]; using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create()) { randomNumberGenerator.GetBytes(array); randomNumberGenerator.GetBytes(array2); } return new AdmissionChallenge(AdmissionProfileCanonicalizer.Hex(array2), array, nowUnixSeconds, nowUnixSeconds + lifetimeSeconds); } internal static bool IsChallengeCurrent(AdmissionChallenge challenge, long nowUnixSeconds, out string failure) { failure = string.Empty; if (challenge == null || nowUnixSeconds < 0) { failure = "challenge-missing"; return false; } if (challenge.DeadlineUnixSeconds < challenge.IssuedUnixSeconds || challenge.DeadlineUnixSeconds - challenge.IssuedUnixSeconds > 120) { failure = "challenge-lifetime"; return false; } if (challenge.IssuedUnixSeconds < nowUnixSeconds - 300 || challenge.IssuedUnixSeconds > nowUnixSeconds + 60 || nowUnixSeconds > challenge.DeadlineUnixSeconds) { failure = "challenge-stale"; return false; } return true; } internal static AdmissionReport CreateReport(AdmissionChallenge challenge, AdmissionClientProfile profile, string clientVersion, long nowUnixSeconds) { if (!IsChallengeCurrent(challenge, nowUnixSeconds, out var failure)) { throw new InvalidOperationException(failure); } if (profile == null) { throw new ArgumentNullException("profile"); } if (!TryComputeNonceBinding(challenge.RequestId, challenge.Nonce, profile.Digest, out var binding)) { throw new InvalidOperationException("nonce-binding-failed"); } return new AdmissionReport(challenge.RequestId, clientVersion, profile.CapturedUnixSeconds, nowUnixSeconds, profile.Digest, binding, new List<AdmissionPluginEvidence>(profile.Plugins)); } internal static bool TryValidateReport(AdmissionChallenge challenge, AdmissionReport report, long receivedUnixSeconds, out AdmissionClientProfile profile, out string failure) { profile = null; failure = string.Empty; if (!IsChallengeCurrent(challenge, receivedUnixSeconds, out failure)) { return false; } if (report == null || !AdmissionProfileCanonicalizer.FixedTimeEquals(challenge.RequestId, report.RequestId)) { failure = "report-request-mismatch"; return false; } if (report.IssuedUnixSeconds < receivedUnixSeconds - 300 || report.IssuedUnixSeconds > receivedUnixSeconds + 60 || report.CapturedUnixSeconds > receivedUnixSeconds + 60) { failure = "report-stale"; return false; } if (!AdmissionProfileCanonicalizer.TryCreate(report.Plugins, report.CapturedUnixSeconds, out profile, out failure)) { return false; } if (!AdmissionProfileCanonicalizer.FixedTimeEquals(profile.Digest, report.ProfileDigest)) { profile = null; failure = "report-digest-mismatch"; return false; } if (!TryComputeNonceBinding(challenge.RequestId, challenge.Nonce, profile.Digest, out var binding) || !AdmissionProfileCanonicalizer.FixedTimeEquals(binding, report.NonceBinding)) { profile = null; failure = "report-binding-mismatch"; return false; } return true; } internal static bool TryComputeNonceBinding(string requestId, byte[] nonce, string profileDigest, out string binding) { binding = string.Empty; if (!AdmissionValidation.IsLowerHex(requestId, 32) || nonce == null || nonce.Length != 32 || !AdmissionValidation.IsLowerHex(profileDigest, 64)) { return false; } string s = "RUNIC-SENTINEL-ADMISSION-BINDING/2\n" + requestId + "\n" + AdmissionProfileCanonicalizer.Hex(nonce) + "\n" + profileDigest + "\n"; using (SHA256 sHA = SHA256.Create()) { binding = AdmissionProfileCanonicalizer.Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes(s))); } return true; } internal static byte[] EncodeChallenge(AdmissionChallenge value) { if (value == null) { throw new ArgumentNullException("value"); } return Encode(AdmissionMessageKind.Challenge, delegate(BinaryWriter writer) { WriteString(writer, value.RequestId, 32); byte[] nonce = value.Nonce; writer.Write(nonce.Length); writer.Write(nonce); writer.Write(value.IssuedUnixSeconds); writer.Write(value.DeadlineUnixSeconds); }); } internal static byte[] EncodeReport(AdmissionReport value) { if (value == null) { throw new ArgumentNullException("value"); } if (value.Plugins.Count > 512) { throw new ArgumentOutOfRangeException("value"); } string previous = null; return Encode(AdmissionMessageKind.Report, delegate(BinaryWriter writer) { WriteString(writer, value.RequestId, 32); WriteString(writer, value.ClientVersion, 32); writer.Write(value.CapturedUnixSeconds); writer.Write(value.IssuedUnixSeconds); WriteString(writer, value.ProfileDigest, 64); WriteString(writer, value.NonceBinding, 64); writer.Write(value.Plugins.Count); foreach (AdmissionPluginEvidence plugin in value.Plugins) { if (plugin == null || (previous != null && string.CompareOrdinal(previous, plugin.Id) >= 0)) { throw new InvalidDataException("Profile entries must be strictly ordered."); } previous = plugin.Id; WriteString(writer, plugin.Id, 128); WriteString(writer, plugin.Version, 64); WriteString(writer, plugin.Sha256, 64); } }); } internal static byte[] EncodeDecision(AdmissionDecisionMessage value) { if (value == null) { throw new ArgumentNullException("value"); } return Encode(AdmissionMessageKind.Decision, delegate(BinaryWriter writer) { WriteString(writer, value.RequestId, 32); writer.Write(value.Accepted ? ((byte)1) : ((byte)0)); writer.Write(value.ResumeHandshake ? ((byte)1) : ((byte)0)); WriteString(writer, value.ReasonCode, 96); writer.Write(value.PolicySequence); WriteString(writer, value.PolicyProfile, 64); writer.Write(value.IssuedUnixSeconds); }); } internal static bool TryGetKind(byte[] bytes, out AdmissionMessageKind kind, out string failure) { kind = (AdmissionMessageKind)0; failure = string.Empty; try { using MemoryStream input = NewReadStream(bytes); using BinaryReader reader = new BinaryReader(input, StrictUtf8); if (!ReadHeader(reader, out kind, out failure)) { return false; } return true; } catch { kind = (AdmissionMessageKind)0; failure = "frame-malformed"; return false; } } internal static bool TryDecodeChallenge(byte[] bytes, out AdmissionChallenge value, out string failure) { value = null; failure = string.Empty; try { using MemoryStream memoryStream = NewReadStream(bytes); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8); if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Challenge, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2) || !AdmissionValidation.IsLowerHex(value2, 32)) { failure = "challenge-shape"; return false; } int num = binaryReader.ReadInt32(); if (num != 32 || Remaining(memoryStream) < (long)num + 20L) { failure = "challenge-nonce"; return false; } byte[] nonce = binaryReader.ReadBytes(num); long num2 = binaryReader.ReadInt64(); long num3 = binaryReader.ReadInt64(); if (num2 < 0 || num3 < num2 || num3 - num2 > 120 || !ReadTerminal(binaryReader, memoryStream)) { failure = "challenge-shape"; return false; } value = new AdmissionChallenge(value2, nonce, num2, num3); return true; } catch { value = null; failure = "challenge-malformed"; return false; } } internal static bool TryDecodeReport(byte[] bytes, out AdmissionReport value, out string failure) { value = null; failure = string.Empty; try { using MemoryStream memoryStream = NewReadStream(bytes); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8); if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Report, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2) || !TryReadString(binaryReader, memoryStream, 32, out var value3)) { failure = "report-shape"; return false; } long num = binaryReader.ReadInt64(); long num2 = binaryReader.ReadInt64(); if (!TryReadString(binaryReader, memoryStream, 64, out var value4) || !TryReadString(binaryReader, memoryStream, 64, out var value5)) { failure = "report-shape"; return false; } int num3 = binaryReader.ReadInt32(); if (num3 < 0 || num3 > 512) { failure = "report-plugin-cap"; return false; } List<AdmissionPluginEvidence> list = new List<AdmissionPluginEvidence>(num3); string text = null; for (int i = 0; i < num3; i++) { if (!TryReadString(binaryReader, memoryStream, 128, out var value6) || !TryReadString(binaryReader, memoryStream, 64, out var value7) || !TryReadString(binaryReader, memoryStream, 64, out var value8) || !AdmissionValidation.IsAtom(value6, 1, 128) || !AdmissionValidation.IsAtom(value7, 1, 64) || !AdmissionValidation.IsLowerHex(value8, 64) || (text != null && string.CompareOrdinal(text, value6) >= 0)) { failure = "report-plugin-shape"; return false; } text = value6; list.Add(new AdmissionPluginEvidence(value6, value7, value8)); } if (!AdmissionValidation.IsLowerHex(value2, 32) || !AdmissionValidation.IsAtom(value3, 1, 32) || num < 0 || num2 < 0 || !AdmissionValidation.IsLowerHex(value4, 64) || !AdmissionValidation.IsLowerHex(value5, 64) || !ReadTerminal(binaryReader, memoryStream)) { failure = "report-shape"; return false; } value = new AdmissionReport(value2, value3, num, num2, value4, value5, list); return true; } catch { value = null; failure = "report-malformed"; return false; } } internal static bool TryDecodeDecision(byte[] bytes, out AdmissionDecisionMessage value, out string failure) { value = null; failure = string.Empty; try { using MemoryStream memoryStream = NewReadStream(bytes); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8); if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Decision, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2)) { failure = "decision-shape"; return false; } byte b = binaryReader.ReadByte(); byte b2 = binaryReader.ReadByte(); if (!TryReadString(binaryReader, memoryStream, 96, out var value3)) { failure = "decision-shape"; return false; } long num = binaryReader.ReadInt64(); if (!TryReadString(binaryReader, memoryStream, 64, out var value4)) { failure = "decision-shape"; return false; } long num2 = binaryReader.ReadInt64(); if (!AdmissionValidation.IsLowerHex(value2, 32) || b > 1 || b2 > 1 || (b2 == 1 && b != 1) || !AdmissionValidation.IsReason(value3) || num < 0 || num2 < 0 || (value4.Length > 0 && !AdmissionValidation.IsAtom(value4, 1, 64)) || !ReadTerminal(binaryReader, memoryStream)) { failure = "decision-shape"; return false; } value = new AdmissionDecisionMessage(value2, b == 1, b2 == 1, value3, num, value4, num2); return true; } catch { value = null; failure = "decision-malformed"; return false; } } internal static bool IsDecisionFresh(AdmissionDecisionMessage value, long nowUnixSeconds) { if (value != null && nowUnixSeconds >= 0 && value.IssuedUnixSeconds >= nowUnixSeconds - 300) { return value.IssuedUnixSeconds <= nowUnixSeconds + 60; } return false; } private static byte[] Encode(AdmissionMessageKind kind, Action<BinaryWriter> body) { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8); binaryWriter.Write(843141970); binaryWriter.Write(2); binaryWriter.Write((byte)kind); body(binaryWriter); binaryWriter.Write(843337285); binaryWriter.Flush(); if (memoryStream.Length <= 0 || memoryStream.Length > 262144) { throw new InvalidDataException("Admission frame exceeds its wire bound."); } return memoryStream.ToArray(); } private static MemoryStream NewReadStream(byte[] bytes) { if (bytes == null || bytes.Length == 0 || bytes.Length > 262144) { throw new InvalidDataException("Admission frame size is invalid."); } return new MemoryStream(bytes, writable: false); } private static bool ReadHeader(BinaryReader reader, out AdmissionMessageKind kind, out string failure) { kind = (AdmissionMessageKind)0; failure = string.Empty; if (reader.ReadInt32() != 843141970 || reader.ReadInt32() != 2) { failure = "frame-header"; return false; } kind = (AdmissionMessageKind)reader.ReadByte(); if ((int)kind < 1 || (int)kind > 3) { kind = (AdmissionMessageKind)0; failure = "frame-kind"; return false; } return true; } private static bool ReadExpectedHeader(BinaryReader reader, AdmissionMessageKind expected, out string failure) { if (!ReadHeader(reader, out var kind, out failure)) { return false; } if (kind == expected) { return true; } failure = "frame-kind"; return false; } private static void WriteString(BinaryWriter writer, string value, int maximumCharacters) { if (value == null || value.Length > maximumCharacters) { throw new InvalidDataException("Admission string exceeds its bound."); } byte[] bytes = StrictUtf8.GetBytes(value); if (bytes.Length > maximumCharacters) { throw new InvalidDataException("Admission string exceeds its byte bound."); } writer.Write(bytes.Length); writer.Write(bytes); } private static bool TryReadString(BinaryReader reader, MemoryStream stream, int maximumBytes, out string value) { value = string.Empty; int num = reader.ReadInt32(); if (num < 0 || num > maximumBytes || Remaining(stream) < num) { return false; } byte[] array = reader.ReadBytes(num); if (array.Length != num) { return false; } value = StrictUtf8.GetString(array); return value.Length <= maximumBytes; } private static bool ReadTerminal(BinaryReader reader, MemoryStream stream) { if (Remaining(stream) == 4 && reader.ReadInt32() == 843337285) { return Remaining(stream) == 0; } return false; } private static long Remaining(MemoryStream stream) { return stream.Length - stream.Position; } } } namespace RunicSentinelClient { internal static class ClientConfiguration { internal static ConfigEntry<bool> Enabled { get; private set; } internal static void Bind(ConfigFile config) { Enabled = config.Bind<bool>("General", "Enabled", true, "Enable client-only Runic Sentinel admission profile reporting. The setting is sampled at startup; the plugin remains inert on servers."); } } [BepInPlugin("chazman.RunicSentinelClient", "Runic Sentinel Client", "1.0.0")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "chazman.RunicSentinelClient"; public const string Name = "Runic Sentinel Client"; public const string Version = "1.0.0"; private Harmony _harmony; internal static ClientAdmissionRuntime ActiveRuntime { get; private set; } private void Awake() { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected O, but got Unknown ClientConfiguration.Bind(((BaseUnityPlugin)this).Config); ConfigEntry<bool> enabled = ClientConfiguration.Enabled; if (enabled != null && !enabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client is disabled; no worker or RPC handler was created."); return; } if (Application.isBatchMode) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client is client-only and remains inert in batch mode."); return; } try { ActiveRuntime = new ClientAdmissionRuntime(delegate(string message) { ((BaseUnityPlugin)this).Logger.LogInfo((object)message); }, delegate(string message) { ((BaseUnityPlugin)this).Logger.LogWarning((object)message); }); _harmony = new Harmony("chazman.RunicSentinelClient"); _harmony.PatchAll(typeof(Plugin).Assembly); if (!ClientTransportPatches.IsInstalled("chazman.RunicSentinelClient")) { throw new InvalidOperationException("The pre-admission connection patch was not installed."); } ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client v1.0.0 initialized. It activates only for a client connection and reports self-observed compatibility evidence to the server."); } catch (Exception ex) { Shutdown(); ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Client failed safely: " + ex.GetType().Name + ".")); } } private void Update() { try { ActiveRuntime?.Tick(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel Client admission tick failed safely (" + ex.GetType().Name + ").")); } } private void OnDestroy() { Shutdown(); } private void Shutdown() { try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch { } _harmony = null; try { ActiveRuntime?.Dispose(); } catch { } ActiveRuntime = null; } } } namespace RunicSentinelClient.Runtime { internal sealed class ClientAdmissionRuntime : IDisposable { private readonly Action<string> _information; private readonly Action<string> _warning; private ClientProfileCollector _profiles; private ClientAdmissionTransport _transport; private bool _disposed; internal ClientAdmissionRuntime(Action<string> information, Action<string> warning) { _information = information; _warning = warning; } internal void OnConnectionStarted(ZNet network, ZNetPeer peer) { if (!_disposed && !((Object)(object)network == (Object)null) && ZNet.instance == network && !network.IsServer() && peer != null && peer.m_server && peer.m_rpc != null) { EnsureStarted(); _transport.AttachServerPeer(network, peer); } } internal void Tick() { if (!_disposed) { ZNet instance = ZNet.instance; if ((Object)(object)instance != (Object)null && instance.IsServer()) { StopClientServices(); return; } if ((Object)(object)instance == (Object)null) { StopClientServices(); return; } EnsureStarted(); _transport.Tick(); } } public void Dispose() { if (!_disposed) { _disposed = true; StopClientServices(); } } private void EnsureStarted() { if (_profiles == null) { _profiles = new ClientProfileCollector(); _transport = new ClientAdmissionTransport(_profiles, _information, _warning); _profiles.Start(); _information?.Invoke("Sentinel Client started a bounded local plugin profile."); } } private void StopClientServices() { try { _transport?.Dispose(); } catch { } _transport = null; try { _profiles?.Dispose(); } catch { } _profiles = null; } } internal sealed class ClientAdmissionTransport : IDisposable { internal const int MaximumNativeResumeAttempts = 3; internal const int MaximumTrackedPeers = 64; private static readonly FieldInfo RpcFunctionsField = typeof(ZRpc).GetField("m_functions", BindingFlags.Instance | BindingFlags.NonPublic); private readonly object _gate = new object(); private readonly ClientProfileCollector _profiles; private readonly Action<string> _information; private readonly Action<string> _warning; private ZNet _network; private ZNetPeer _serverPeer; private ZRpc _serverRpc; private object _registeredHandler; private ZNet _blockedNetwork; private ZNetPeer _blockedPeer; private ZRpc _blockedRpc; private AdmissionChallenge _challenge; private string _sentRequestId = string.Empty; private string _completedRequestId = string.Empty; private string _reportedProfileFailureStatus = string.Empty; private int _nativeResumeAttempts; private bool _nativeHandshakeResumed; private bool _disposed; internal ClientAdmissionTransport(ClientProfileCollector profiles, Action<string> information, Action<string> warning) { _profiles = profiles ?? throw new ArgumentNullException("profiles"); _information = information; _warning = warning; } internal bool AttachServerPeer(ZNet network, ZNetPeer peer) { if ((Object)(object)network == (Object)null || ZNet.instance != network || network.IsServer() || peer == null || !peer.m_server || peer.m_rpc == null) { return false; } ZRpc rpc = peer.m_rpc; lock (_gate) { if (_disposed) { return false; } if (_network == network && _serverPeer == peer && _serverRpc == rpc) { return true; } if (_blockedNetwork == network && _blockedPeer == peer && _blockedRpc == rpc) { return false; } DetachLocked(); int num = StableHash("chazman.RunicSentinel.Admission.v2"); if (!TryGetFunctionMap(rpc, out var functions)) { BlockLocked(network, peer, rpc); Notify(_warning, "Sentinel Client could not access the direct RPC registry."); return false; } if (functions.Contains(num)) { BlockLocked(network, peer, rpc); Notify(_warning, "Sentinel Client left the direct RPC with its existing Sentinel responder."); return false; } try { rpc.Register<ZPackage>("chazman.RunicSentinel.Admission.v2", (Action<ZRpc, ZPackage>)ReceivePackage); object obj = functions[num]; if (obj == null) { rpc.Unregister("chazman.RunicSentinel.Admission.v2"); BlockLocked(network, peer, rpc); Notify(_warning, "Sentinel Client could not verify its direct RPC registration."); return false; } _network = network; _serverPeer = peer; _serverRpc = rpc; _registeredHandler = obj; ResetExchangeLocked(); Notify(_information, "Sentinel Client bound its admission handler before native peer admission."); return true; } catch (Exception ex) { try { if (functions.Contains(num)) { rpc.Unregister("chazman.RunicSentinel.Admission.v2"); } } catch { } BlockLocked(network, peer, rpc); Notify(_warning, "Sentinel Client direct RPC registration failed safely (" + ex.GetType().Name + ")."); return false; } } } internal void Tick() { //IL_032a: Unknown result type (might be due to invalid IL or missing references) //IL_0330: Expected O, but got Unknown ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || instance.IsServer()) { ClearDisconnected(); return; } ZNetPeer val; try { val = instance.GetServerPeer(); } catch { val = null; } if (val?.m_rpc != null) { AttachServerPeer(instance, val); } ZRpc serverRpc; AdmissionChallenge challenge; lock (_gate) { if (_disposed) { return; } if (_serverRpc == null) { if ((Object)(object)_blockedNetwork != (Object)null && (_blockedNetwork != instance || !IsTrackedServerPeer(instance, _blockedPeer))) { ClearBlockedLocked(); } return; } if (_network != instance || _serverPeer == null || !_serverPeer.m_server || _serverPeer.m_rpc != _serverRpc || !IsTrackedServerPeer(instance, _serverPeer)) { DetachLocked(); return; } if (_challenge == null || string.Equals(_sentRequestId, _challenge.RequestId, StringComparison.Ordinal)) { return; } serverRpc = _serverRpc; challenge = _challenge; } long nowUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (!AdmissionProtocolV2.IsChallengeCurrent(challenge, nowUnixSeconds, out var failure)) { lock (_gate) { if (_challenge == challenge) { _challenge = null; } } Notify(_warning, "Sentinel admission challenge was rejected: " + failure + "."); return; } if (!_profiles.TryGet(out var profile, out var status)) { bool flag = false; lock (_gate) { if (!string.Equals(status, "capturing-profile", StringComparison.Ordinal) && !string.Equals(status, "not-started", StringComparison.Ordinal) && !string.Equals(_reportedProfileFailureStatus, status, StringComparison.Ordinal)) { _reportedProfileFailureStatus = status; flag = true; } } if (flag) { Notify(_warning, "Sentinel Client could not answer the admission challenge (" + status + ")."); } return; } byte[] array; try { array = AdmissionProtocolV2.EncodeReport(AdmissionProtocolV2.CreateReport(challenge, profile, "1.0.0", nowUnixSeconds)); } catch (Exception ex) { Notify(_warning, "Sentinel admission report creation failed safely (" + ex.GetType().Name + ")."); return; } lock (_gate) { if (_disposed || _serverRpc != serverRpc || _challenge != challenge || string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal)) { return; } _sentRequestId = challenge.RequestId; } try { if (!RpcConnected(serverRpc)) { lock (_gate) { if (string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal)) { _sentRequestId = string.Empty; } return; } } serverRpc.Invoke("chazman.RunicSentinel.Admission.v2", new object[1] { (object)new ZPackage(array) }); Notify(_information, "Sentinel Client submitted an admission profile report (" + profile.Plugins.Count + " plugins)."); } catch (Exception ex2) { lock (_gate) { if (string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal)) { _sentRequestId = string.Empty; } } Notify(_warning, "Sentinel admission report send failed safely (" + ex2.GetType().Name + ")."); } } public void Dispose() { lock (_gate) { if (!_disposed) { _disposed = true; DetachLocked(); } } } private void ReceivePackage(ZRpc rpc, ZPackage package) { if (rpc == null || package == null) { return; } lock (_gate) { if (_disposed || _serverRpc != rpc || (Object)(object)_network == (Object)null || ZNet.instance != _network || _network.IsServer() || _serverPeer == null || !_serverPeer.m_server || _serverPeer.m_rpc != rpc) { return; } } byte[] array; try { if (package.Size() <= 0 || package.Size() > 262144) { return; } array = package.GetArray(); } catch { return; } if (AdmissionProtocolV2.TryGetKind(array, out var kind, out var _)) { switch (kind) { case AdmissionMessageKind.Challenge: ReceiveChallenge(rpc, array); break; case AdmissionMessageKind.Decision: ReceiveDecision(rpc, array); break; } } } private void ReceiveChallenge(ZRpc rpc, byte[] bytes) { if (!AdmissionProtocolV2.TryDecodeChallenge(bytes, out var value, out var failure) || !AdmissionProtocolV2.IsChallengeCurrent(value, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), out failure)) { Notify(_warning, "Sentinel admission challenge was rejected: " + failure + "."); return; } bool flag; lock (_gate) { if (_disposed || _serverRpc != rpc || string.Equals(_completedRequestId, value.RequestId, StringComparison.Ordinal)) { return; } flag = _challenge == null || !string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal); _challenge = value; _sentRequestId = string.Empty; } if (flag) { Notify(_information, "Sentinel Client received an admission challenge."); } } private void ReceiveDecision(ZRpc rpc, byte[] bytes) { if (!AdmissionProtocolV2.TryDecodeDecision(bytes, out var value, out var failure) || !AdmissionProtocolV2.IsDecisionFresh(value, DateTimeOffset.UtcNow.ToUnixTimeSeconds())) { Notify(_warning, "Sentinel admission decision was rejected: " + failure + "."); return; } bool flag = false; lock (_gate) { if (_disposed || _serverRpc != rpc || _challenge == null || !string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal) || !string.Equals(_sentRequestId, value.RequestId, StringComparison.Ordinal) || string.Equals(_completedRequestId, value.RequestId, StringComparison.Ordinal)) { return; } flag = value.Accepted && value.ResumeHandshake; if (flag) { if (_nativeHandshakeResumed || _nativeResumeAttempts >= 3) { return; } _nativeResumeAttempts++; } else { _completedRequestId = value.RequestId; } } if (value.Accepted) { Notify(_information, "Sentinel admission accepted (" + value.ReasonCode + ")."); } else { Notify(_warning, "Sentinel admission denied (" + value.ReasonCode + ")."); } if (!flag) { return; } try { rpc.Invoke("ServerHandshake", Array.Empty<object>()); lock (_gate) { if (!_disposed && _serverRpc == rpc && _challenge != null && string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal)) { _nativeHandshakeResumed = true; _completedRequestId = value.RequestId; } } } catch (Exception ex) { Notify(_warning, "Sentinel could not resume the native handshake (" + ex.GetType().Name + ")."); } } private void ClearDisconnected() { lock (_gate) { ClearDisconnectedLocked(); } } private void ClearDisconnectedLocked() { if ((!((Object)(object)_network != (Object)null) || ZNet.instance != _network || _network.IsServer() || _serverPeer == null || !_serverPeer.m_server || _serverRpc == null || _serverPeer.m_rpc != _serverRpc || !IsTrackedServerPeer(_network, _serverPeer)) && (_serverRpc != null || !((Object)(object)_blockedNetwork != (Object)null) || ZNet.instance != _blockedNetwork || _blockedNetwork.IsServer() || !IsTrackedServerPeer(_blockedNetwork, _blockedPeer))) { DetachLocked(); } } private void DetachLocked() { ZRpc serverRpc = _serverRpc; object registeredHandler = _registeredHandler; _network = null; _serverPeer = null; _serverRpc = null; _registeredHandler = null; ClearBlockedLocked(); ResetExchangeLocked(); if (serverRpc == null || registeredHandler == null || !TryGetFunctionMap(serverRpc, out var functions)) { return; } int num = StableHash("chazman.RunicSentinel.Admission.v2"); try { if (functions[num] == registeredHandler) { serverRpc.Unregister("chazman.RunicSentinel.Admission.v2"); } } catch { } } private void BlockLocked(ZNet network, ZNetPeer peer, ZRpc rpc) { _blockedNetwork = network; _blockedPeer = peer; _blockedRpc = rpc; } private void ClearBlockedLocked() { _blockedNetwork = null; _blockedPeer = null; _blockedRpc = null; } private static bool IsTrackedServerPeer(ZNet network, ZNetPeer expected) { if ((Object)(object)network == (Object)null || expected == null || !expected.m_server) { return false; } List<ZNetPeer> peers; try { peers = network.GetPeers(); } catch { return false; } if (peers == null) { return false; } int num = 0; foreach (ZNetPeer item in peers) { if (num++ >= 64) { break; } if (item == expected) { return true; } } return false; } private static bool RpcConnected(ZRpc rpc) { try { return rpc != null && rpc.IsConnected(); } catch { return false; } } private static void Notify(Action<string> sink, string message) { try { sink?.Invoke(message); } catch { } } private static bool TryGetFunctionMap(ZRpc rpc, out IDictionary functions) { functions = null; if (rpc == null || RpcFunctionsField == null) { return false; } try { functions = RpcFunctionsField.GetValue(rpc) as IDictionary; return functions != null; } catch { functions = null; return false; } } private static int StableHash(string value) { int num = 5381; int num2 = num; for (int i = 0; i < value.Length && value[i] != 0; i += 2) { num = ((num << 5) + num) ^ value[i]; if (i == value.Length - 1 || value[i + 1] == '\0') { break; } num2 = ((num2 << 5) + num2) ^ value[i + 1]; } return num + num2 * 1566083941; } private void ResetExchangeLocked() { _challenge = null; _sentRequestId = string.Empty; _completedRequestId = string.Empty; _reportedProfileFailureStatus = string.Empty; _nativeResumeAttempts = 0; _nativeHandshakeResumed = false; } } internal sealed class ClientProfileCollector : IDisposable { private readonly object _gate = new object(); private CancellationTokenSource _cancellation; private AdmissionClientProfile _profile; private string _status = "not-started"; private bool _started; private bool _disposed; internal void Start() { CancellationToken token; lock (_gate) { if (_disposed) { throw new ObjectDisposedException("ClientProfileCollector"); } if (_started) { return; } _started = true; _status = "capturing-profile"; _cancellation = new CancellationTokenSource(); token = _cancellation.Token; } ClientPluginFile[] files; try { files = CaptureLoadedPlugins(); } catch (Exception ex) { Publish(null, "profile-capture-failed-" + ex.GetType().Name, token); return; } Task.Run(delegate { if (ClientProfileBuilder.TryBuild(files, DateTimeOffset.UtcNow.ToUnixTimeSeconds, token, out var profile, out var failure)) { Publish(profile, "ready", token); } else if (!token.IsCancellationRequested) { Publish(null, failure, token); } }, token); } internal bool TryGet(out AdmissionClientProfile profile, out string status) { lock (_gate) { profile = _profile; status = _status; return profile != null; } } public void Dispose() { CancellationTokenSource cancellation; lock (_gate) { if (_disposed) { return; } _disposed = true; cancellation = _cancellation; _cancellation = null; _profile = null; _status = "disposed"; } if (cancellation == null) { return; } try { cancellation.Cancel(); } catch { } try { cancellation.Dispose(); } catch { } } private void Publish(AdmissionClientProfile profile, string status, CancellationToken token) { lock (_gate) { if (!_disposed && !token.IsCancellationRequested) { _profile = profile; _status = (AdmissionValidation.IsReason(status) ? status : "profile-build-failed"); } } } private static ClientPluginFile[] CaptureLoadedPlugins() { if (Chainloader.PluginInfos.Count > 512) { throw new InvalidDataException("Plugin cap exceeded."); } List<ClientPluginFile> list = new List<ClientPluginFile>(Chainloader.PluginInfos.Count); foreach (KeyValuePair<string, PluginInfo> item in Chainloader.PluginInfos.OrderBy<KeyValuePair<string, PluginInfo>, string>((KeyValuePair<string, PluginInfo> keyValuePair) => keyValuePair.Key, StringComparer.Ordinal)) { if (list.Count >= 512) { throw new InvalidDataException("Plugin cap exceeded."); } PluginInfo value = item.Value; string text = ((value != null) ? value.Location : null); object obj; if (value == null) { obj = null; } else { BepInPlugin metadata = value.Metadata; obj = ((metadata != null) ? metadata.Version?.ToString() : null); } string version = (string)obj; list.Add(new ClientPluginFile(item.Key, version, Path.GetFullPath(text ?? string.Empty))); } return list.ToArray(); } } internal sealed class ClientPluginFile { internal string Id { get; } internal string Version { get; } internal string Path { get; } internal ClientPluginFile(string id, string version, string path) { Id = AdmissionValidation.RequireAtom(id, 1, 128, "id"); Version = AdmissionValidation.RequireAtom(version, 1, 64, "version"); if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("Path is required.", "path"); } Path = System.IO.Path.GetFullPath(path); } } internal static class ClientProfileBuilder { private sealed class FileHash { internal string Digest { get; } internal long Length { get; } internal long WriteTicks { get; } internal FileHash(string digest, long length, long writeTicks) { Digest = digest; Length = length; WriteTicks = writeTicks; } } internal const long MaximumPluginBytes = 536870912L; internal const long MaximumTotalPluginBytes = 4294967296L; private static readonly StringComparer PathComparer = (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); internal static bool TryBuild(IEnumerable<ClientPluginFile> source, Func<long> unixTime, CancellationToken cancellation, out AdmissionClientProfile profile, out string failure) { profile = null; failure = string.Empty; try { if (source == null || unixTime == null) { failure = "profile-input-missing"; return false; } List<ClientPluginFile> list = new List<ClientPluginFile>(); foreach (ClientPluginFile item in source) { if (item == null) { failure = "profile-entry-null"; return false; } if (list.Count >= 512) { failure = "profile-plugin-cap"; return false; } list.Add(item); } List<AdmissionPluginEvidence> list2 = new List<AdmissionPluginEvidence>(list.Count); Dictionary<string, FileHash> dictionary = new Dictionary<string, FileHash>(PathComparer); byte[] buffer = new byte[65536]; long num = 0L; foreach (ClientPluginFile item2 in list) { cancellation.ThrowIfCancellationRequested(); string fullPath = Path.GetFullPath(item2.Path); if (!dictionary.TryGetValue(fullPath, out var value)) { FileInfo fileInfo = new FileInfo(fullPath); if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > 536870912 || num > 4294967296L - fileInfo.Length) { failure = "profile-file-bound"; return false; } long length = fileInfo.Length; long ticks = fileInfo.LastWriteTimeUtc.Ticks; num += length; string digest = HashExact(fullPath, length, buffer, cancellation); fileInfo.Refresh(); if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc.Ticks != ticks) { failure = "profile-file-changed"; return false; } value = new FileHash(digest, length, ticks); dictionary.Add(fullPath, value); } list2.Add(new AdmissionPluginEvidence(item2.Id, item2.Version, value.Digest)); } long num2 = unixTime(); if (num2 < 0) { failure = "profile-clock"; return false; } return AdmissionProfileCanonicalizer.TryCreate(list2, num2, out profile, out failure); } catch (OperationCanceledException) { failure = "profile-cancelled"; return false; } catch (Exception) { failure = "profile-build-failed"; return false; } } private static string HashExact(string path, long expectedLength, byte[] buffer, CancellationToken cancellation) { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, FileOptions.SequentialScan); using SHA256 sHA = SHA256.Create(); if (fileStream.Length != expectedLength) { throw new IOException("Plugin changed before hashing."); } long num = expectedLength; while (num > 0) { cancellation.ThrowIfCancellationRequested(); int count = (int)Math.Min(buffer.Length, num); int num2 = fileStream.Read(buffer, 0, count); if (num2 <= 0) { throw new EndOfStreamException("Plugin ended during hashing."); } sHA.TransformBlock(buffer, 0, num2, buffer, 0); num -= num2; } if (fileStream.ReadByte() != -1 || fileStream.Length != expectedLength) { throw new IOException("Plugin grew during hashing."); } sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0); return AdmissionProfileCanonicalizer.Hex(sHA.Hash); } } [HarmonyPatch(typeof(ZNet), "OnNewConnection", new Type[] { typeof(ZNetPeer) })] internal static class ClientTransportPatches { [HarmonyPrefix] [HarmonyPriority(800)] private static void ZNetOnNewConnectionPrefix(ZNet __instance, [HarmonyArgument(0)] ZNetPeer peer) { try { Plugin.ActiveRuntime?.OnConnectionStarted(__instance, peer); } catch { } } internal static bool IsInstalled(string owner) { if (string.IsNullOrEmpty(owner)) { return false; } MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "OnNewConnection", new Type[1] { typeof(ZNetPeer) }, (Type[])null); return ((methodInfo == null) ? null : Harmony.GetPatchInfo((MethodBase)methodInfo))?.Prefixes.Any((Patch patch) => string.Equals(patch.owner, owner, StringComparison.Ordinal) && patch.priority == 800 && patch.PatchMethod?.DeclaringType == typeof(ClientTransportPatches)) ?? false; } } }