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.5
plugins/RunicSentinelClient/RunicSentinelClient.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization.Json; 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 Runic.Localization; using RunicSentinel.Admission; using RunicSentinel.Core; using RunicSentinel.PlayerActions; using RunicSentinel.Runtime; 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.5.0")] [assembly: AssemblyInformationalVersion("1.0.5")] [assembly: InternalsVisibleTo("RunicSentinelClient.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.0.5.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 Runic.Localization { internal sealed class LanguageCatalog { private readonly Dictionary<string, string> _english; private Dictionary<string, string> _selected = new Dictionary<string, string>(); private readonly Func<string, string> _read; private string _language; internal LanguageCatalog(string english, Func<string, string> read) { _english = Parse(english); _read = read; } internal static Dictionary<string, string> Parse(string json) { if (string.IsNullOrWhiteSpace(json)) { return new Dictionary<string, string>(); } if (json.Length > 2097152) { throw new InvalidDataException("Language file exceeds 2 MiB."); } DataContractJsonSerializer dataContractJsonSerializer = new DataContractJsonSerializer(typeof(Dictionary<string, string>), new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json.TrimStart('\ufeff'))); return ((Dictionary<string, string>)dataContractJsonSerializer.ReadObject(stream)) ?? new Dictionary<string, string>(); } internal void Select(string language) { language = (string.IsNullOrWhiteSpace(language) ? "English" : language); if (string.Equals(language, _language, StringComparison.Ordinal)) { return; } _language = language; _selected = new Dictionary<string, string>(); if (language.IndexOfAny(new char[4] { '/', '\\', ':', '\0' }) >= 0 || language == "." || language == "..") { return; } try { _selected = Parse(_read(language)); } catch (Exception) { } } internal string Get(string key) { _english.TryGetValue(key, out var value); if (_selected.TryGetValue(key, out var value2) && !string.IsNullOrWhiteSpace(value2) && (value == null || SameFields(value, value2))) { return value2; } return value ?? key; } internal string English(string key) { if (!_english.TryGetValue(key, out var value)) { return key; } return value; } internal string TranslateEnglish(string text) { foreach (KeyValuePair<string, string> item in _english) { if (item.Value == text) { return Get(item.Key); } } return text; } internal string Format(string key, params object[] args) { _english.TryGetValue(key, out var value); string text = Get(key); if (value != null && !SameFields(value, text)) { text = value; } try { return string.Format(CultureInfo.CurrentCulture, text, args); } catch (FormatException) { try { return string.Format(CultureInfo.CurrentCulture, value ?? key, args); } catch (FormatException) { return value ?? key; } } } private static bool SameFields(string a, string b) { try { return Fields(a).SetEquals(Fields(b)); } catch (FormatException) { return false; } } private static HashSet<int> Fields(string text) { HashSet<int> hashSet = new HashSet<int>(); for (int i = 0; i < text.Length; i++) { if (text[i] == '}') { if (i + 1 >= text.Length || text[i + 1] != '}') { throw new FormatException(); } i++; } else { if (text[i] != '{') { continue; } if (i + 1 < text.Length && text[i + 1] == '{') { i++; continue; } int num = ++i; for (; i < text.Length && char.IsDigit(text[i]); i++) { } if (num == i || !int.TryParse(text.Substring(num, i - num), out var result)) { throw new FormatException(); } hashSet.Add(result); for (; i < text.Length && text[i] != '}'; i++) { if (text[i] == '{') { throw new FormatException(); } } if (i == text.Length) { throw new FormatException(); } } } return hashSet; } } internal static class RunicText { private static readonly Assembly Owner = typeof(RunicText).Assembly; private static readonly string Module = Owner.GetName().Name; private static readonly LanguageCatalog Catalog = Create(); private static LanguageCatalog Create() { using Stream stream = Owner.GetManifestResourceStream("Runic.Localization.English.json"); using StreamReader streamReader = ((stream == null) ? null : new StreamReader(stream)); return new LanguageCatalog(streamReader?.ReadToEnd() ?? "{}", ReadLanguage); } private static string ReadLanguage(string language) { string path = Path.Combine(Path.Combine(Path.GetDirectoryName(Owner.Location), "Translations", Module), language + ".json"); if (!File.Exists(path)) { return null; } return File.ReadAllText(path); } private static void SelectLanguage() { string language = "English"; try { Localization instance = Localization.instance; language = ((instance != null) ? instance.GetSelectedLanguage() : null) ?? "English"; } catch { } Catalog.Select(language); } internal static string Get(string key) { SelectLanguage(); return Catalog.Get(key); } internal static string English(string key) { return Catalog.English(key); } internal static string TranslateEnglish(string text) { SelectLanguage(); return Catalog.TranslateEnglish(text); } internal static string Format(string key, params object[] args) { SelectLanguage(); return Catalog.Format(key, args); } } } namespace RunicSentinel.PlayerActions { internal static class PlayerActionProtocol { internal const string CapabilityRpc = "runic.sentinel.player.capability.v1"; internal const string ActionRpc = "runic.sentinel.player.action.v1"; internal const string ResultRpc = "runic.sentinel.player.result.v1"; internal const int Schema = 2; internal const int MaximumBytes = 4096; internal static readonly string[] Actions = new string[12] { "raiseskill", "resetskill", "heal", "puke", "clearstatus", "adrenaline", "addstatus", "god", "ghost", "fly", "nocost", "modes" }; internal static string[] SkillsList() { return (from n in Enum.GetNames(typeof(SkillType)) where n != "None" select n).ToArray(); } internal static string Validate(string action, string args) { args = args ?? ""; if (!Actions.Contains(action) || args.Length > 160 || args.Any(char.IsControl)) { throw new InvalidOperationException("Unsupported player action or invalid arguments."); } string[] parts = args.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); switch (action) { case "raiseskill": case "resetskill": { int num2 = ((!(action == "raiseskill")) ? 1 : 2); if (parts.Length != num2 || !SkillsList().Any((string s) => s.Equals(parts[0], StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException("Choose a valid skill name."); } if (num2 == 2 && (!int.TryParse(parts[1], NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var result3) || result3 < -100 || result3 > 100 || result3 == 0)) { throw new InvalidOperationException("Skill change must be a nonzero whole number from -100 to 100."); } break; } case "adrenaline": { if (!int.TryParse(args, NumberStyles.None, CultureInfo.InvariantCulture, out var result2) || result2 < 0 || result2 > 100) { throw new InvalidOperationException("Adrenaline must be from 0 to 100."); } break; } case "god": case "ghost": case "fly": case "nocost": if (args != "on" && args != "off") { throw new InvalidOperationException("Choose On or Off."); } break; case "addstatus": { if (parts.Length < 1 || parts.Length > 3 || parts[0].Any((char c) => !char.IsLetterOrDigit(c) && c != '_' && c != '-' && c != '.')) { throw new InvalidOperationException("Choose a registered status effect name, optionally followed by duration and intensity."); } for (int num = 1; num < parts.Length; num++) { if (!float.TryParse(parts[num], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || float.IsNaN(result) || float.IsInfinity(result) || result < 0f) { throw new InvalidOperationException("Duration and intensity must be finite, nonnegative numbers."); } } break; } default: if (args.Length != 0) { throw new InvalidOperationException("This action takes no arguments."); } break; } return string.Join(" ", parts); } } internal sealed class PlayerActionReceiver : IDisposable { [HarmonyPatch(typeof(MessageHud), "ShowMessage")] private static class AdministratorNotificationPatch { internal static bool Prefix() { return _notificationDepth == 0; } } private sealed class Receipt { internal string Body; internal string Message; internal bool Success; internal long Expires; } [ThreadStatic] private static int _notificationDepth; private ZNet _network; private ZRpc _server; private float _nextHello; private static PlayerActionReceiver _active; private Player _modePlayer; private readonly Dictionary<string, bool> _modeOriginal = new Dictionary<string, bool>(); private readonly HashSet<string> _grantedModes = new HashSet<string>(); private readonly Dictionary<string, Receipt> _seen = new Dictionary<string, Receipt>(); internal static void WithoutNotifications(Action action) { _notificationDepth++; try { action(); } finally { _notificationDepth--; } } internal static bool Granted(string mode) { PlayerActionReceiver active = _active; if (active != null && active._server != null && (Object)(object)active._modePlayer != (Object)null && active._modePlayer == Player.m_localPlayer && active._network == ZNet.instance) { ZRpc server = active._server; ZNet instance = ZNet.instance; if (server == ((instance == null) ? null : instance.GetServerPeer()?.m_rpc)) { return active._grantedModes.Contains(mode); } } return false; } private static bool Mode(Player p, string mode) { return mode switch { "fly" => ((Character)p).IsDebugFlying(), "ghost" => ((Character)p).InGhostMode(), "god" => ((Character)p).InGodMode(), _ => p.NoCostCheat(), }; } private static void SetMode(Player p, string mode, bool value) { switch (mode) { case "god": p.SetGodMode(value); return; case "ghost": p.SetGhostMode(value); return; case "nocost": p.SetNoPlacementCost(value); return; } FieldInfo? field = typeof(Player).GetField("m_debugFly", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field == null) { throw new InvalidOperationException("Flight state is unavailable."); } if ((bool)field.GetValue(p) != value) { p.ToggleDebugFly(); } } private void SetGrantedMode(Player p, string mode, bool value) { if (_modePlayer != p) { ClearModes(); _modePlayer = p; } if (!_modeOriginal.ContainsKey(mode)) { _modeOriginal[mode] = Mode(p, mode); } _active = this; _grantedModes.Add(mode); try { SetMode(p, mode, value); } catch { _grantedModes.Remove(mode); throw; } if (!value) { _grantedModes.Remove(mode); } } private void ClearModes() { if ((Object)(object)_modePlayer != (Object)null) { foreach (KeyValuePair<string, bool> pair in _modeOriginal) { try { WithoutNotifications(delegate { SetMode(_modePlayer, pair.Key, pair.Value); }); } catch { } } } _modeOriginal.Clear(); _grantedModes.Clear(); _modePlayer = null; if (_active == this) { _active = null; } } private static string Modes(Player p) { return "God: " + (((Character)p).InGodMode() ? "On" : "Off") + " · Ghost: " + (((Character)p).InGhostMode() ? "On" : "Off") + " · Fly: " + (((Character)p).IsDebugFlying() ? "On" : "Off") + " · No-cost: " + (p.NoCostCheat() ? "On" : "Off"); } private static FieldInfo EffectField(Type type, string name) { Type type2 = type; while (type2 != null) { FieldInfo field = type2.GetField(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field; } type2 = type2.BaseType; } return null; } private static void SetEffectField(object target, string name, float value) { FieldInfo fieldInfo = EffectField(target.GetType(), name); if (fieldInfo == null) { throw new InvalidOperationException("Status parameter is unavailable: " + name); } fieldInfo.SetValue(target, value); } internal void Tick() { //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Expected O, but got Unknown ZNet instance = ZNet.instance; ZNetPeer val = (((Object)(object)instance != (Object)null && !instance.IsServer()) ? instance.GetServerPeer() : null); ZRpc val2 = ((val != null && val.IsReady()) ? val.m_rpc : null); if (instance != _network || val2 != _server) { Dispose(); _network = instance; _server = val2; _nextHello = 0f; if (val2 != null) { val2.Register<ZPackage>("runic.sentinel.player.action.v1", (Action<ZRpc, ZPackage>)Receive); } } if ((Object)(object)_modePlayer != (Object)null && _modePlayer != Player.m_localPlayer) { ClearModes(); } long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); string[] array = (from p in _seen where p.Value.Expires < now select p.Key).ToArray(); foreach (string key in array) { _seen.Remove(key); } if (_server != null && !((Object)(object)Player.m_localPlayer == (Object)null) && !(Time.realtimeSinceStartup < _nextHello)) { _nextHello = Time.realtimeSinceStartup + 5f; ZPackage val3 = new ZPackage(); val3.Write(2); val3.Write(Player.m_localPlayer.GetPlayerID()); _server.Invoke("runic.sentinel.player.capability.v1", new object[1] { val3 }); } } private void Receive(ZRpc rpc, ZPackage package) { if (_server == null || rpc != _server || _network != ZNet.instance || _network.IsServer() || _network.GetServerPeer()?.m_rpc != rpc || package == null || package.Size() > 4096) { return; } string text = ""; try { text = package.ReadString(); long num = package.ReadLong(); long num2 = package.ReadLong(); string action = package.ReadString(); string args = package.ReadString(); long num3 = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (text.Length != 32 || !Guid.TryParseExact(text, "N", out var _) || package.GetPos() != package.Size() || num2 < num3 || num2 > num3 + 45) { throw new InvalidOperationException("Invalid or expired player action."); } string text2 = num + "\n" + num2 + "\n" + action + "\n" + args; if (_seen.TryGetValue(text, out var value)) { if (value.Body != text2) { throw new InvalidOperationException("Reused action identity."); } Reply(text, value.Success, value.Message); return; } if (_seen.Count >= 128) { throw new InvalidOperationException("Player action capacity reached."); } Receipt receipt = new Receipt { Body = text2, Expires = num3 + 90, Message = "Action did not complete." }; _seen.Add(text, receipt); try { args = PlayerActionProtocol.Validate(action, args); Player player = Player.m_localPlayer; if ((Object)(object)player == (Object)null || player.GetPlayerID() != num || ((Character)player).IsDead()) { throw new InvalidOperationException("The selected character changed, is dead, or is not ready."); } if (action != "modes" && !Achievements.IsCheatedAtAll()) { throw new InvalidOperationException("This player must explicitly run confirmcheats in their own F5 console before receiving cheat actions. Sentinel does not confirm it for them."); } WithoutNotifications(delegate { Apply(player, action, args); }); receipt.Success = true; receipt.Message = ((action == "modes" || new string[4] { "god", "ghost", "fly", "nocost" }.Contains(action)) ? Modes(player) : (action + " completed on " + player.GetPlayerName() + ".")); } catch (Exception ex) { receipt.Message = ex.Message; } Reply(text, receipt.Success, receipt.Message); } catch (Exception ex2) { if (text.Length == 32) { Reply(text, ok: false, ex2.Message); } } } private void Apply(Player player, string action, string args) { string[] parts = args.Split(' '); if (action == null) { return; } switch (action.Length) { case 5: switch (action[0]) { default: return; case 'm': _ = action == "modes"; return; case 'g': break; } if (!(action == "ghost")) { break; } goto IL_0181; case 3: switch (action[0]) { default: return; case 'g': if (!(action == "god")) { return; } break; case 'f': if (!(action == "fly")) { return; } break; } goto IL_0181; case 10: switch (action[1]) { case 'a': if (action == "raiseskill") { ((Character)player).GetSkills().CheatRaiseSkill(parts[0], (float)int.Parse(parts[1], CultureInfo.InvariantCulture), true); } break; case 'e': if (action == "resetskill") { ((Character)player).GetSkills().CheatResetSkill(parts[0]); } break; case 'd': if (action == "adrenaline") { ((Character)player).AddAdrenaline((float)int.Parse(args, CultureInfo.InvariantCulture) - player.GetAdrenaline()); } break; case 'b': case 'c': break; } break; case 4: switch (action[0]) { case 'h': if (action == "heal") { ((Character)player).Heal(((Character)player).GetMaxHealth(), true); ((Character)player).AddStamina(((Character)player).GetMaxStamina()); ((Character)player).AddEitr(((Character)player).GetMaxEitr()); } break; case 'p': if (action == "puke") { player.ClearFood(); } break; } break; case 6: if (!(action == "nocost")) { break; } goto IL_0181; case 11: if (action == "clearstatus") { player.ClearHardDeath(); ((Character)player).GetSEMan().RemoveAllStatusEffects(false); } break; case 9: { if (!(action == "addstatus")) { break; } StatusEffect val = ((IEnumerable<StatusEffect>)ObjectDB.instance?.m_StatusEffects).FirstOrDefault((Func<StatusEffect, bool>)((StatusEffect e) => (Object)(object)e != (Object)null && ((Object)e).name.Equals(parts[0], StringComparison.OrdinalIgnoreCase))); if ((Object)(object)val == (Object)null) { throw new InvalidOperationException("Status effect is not installed on this player's client."); } if (parts.Length > 2 && !(val is SE_Shield) && !(val is SE_Burning) && !(val is SE_Poison)) { throw new InvalidOperationException("This status effect has no intensity parameter."); } StatusEffect val2 = ((Character)player).GetSEMan().AddStatusEffect(StringExtensionMethods.GetStableHashCode(((Object)val).name), true, 0, 0f, (short)(-1)); if ((Object)(object)val2 == (Object)null) { throw new InvalidOperationException("The player could not receive this status effect."); } if (parts.Length > 1) { val2.m_ttl = float.Parse(parts[1], CultureInfo.InvariantCulture); } if (parts.Length <= 2) { break; } float num = float.Parse(parts[2], CultureInfo.InvariantCulture); if (val2 is SE_Shield) { SetEffectField(val2, "m_absorbDamage", num); } SE_Burning val3 = (SE_Burning)(object)((val2 is SE_Burning) ? val2 : null); if (val3 != null) { bool flag = ((Object)val).name == "Burning"; SetEffectField(val2, flag ? "m_fireDamageLeft" : "m_spiritDamageLeft", 0f); MethodInfo? method = ((object)val2).GetType().GetMethod(flag ? "AddFireDamage" : "AddSpiritDamage", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method == null) { throw new InvalidOperationException("Burning intensity is unavailable."); } method.Invoke(val3, new object[1] { num }); } if (val2 is SE_Poison) { SetEffectField(val2, "m_damageLeft", num); FieldInfo fieldInfo = EffectField(((object)val2).GetType(), "m_damageInterval"); if (fieldInfo == null) { throw new InvalidOperationException("Poison intensity is unavailable."); } SetEffectField(val2, "m_damagePerHit", num / Math.Max(0.001f, val2.m_ttl) * (float)fieldInfo.GetValue(val2)); } break; } case 7: case 8: break; IL_0181: SetGrantedMode(player, action, args == "on"); break; } } private void Reply(string id, bool ok, string text) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(id); val.Write(ok); val.Write((text.Length > 600) ? text.Substring(0, 600) : text); ZRpc server = _server; if (server != null) { server.Invoke("runic.sentinel.player.result.v1", new object[1] { val }); } } public void Dispose() { ClearModes(); if (_server != null) { try { _server.Unregister("runic.sentinel.player.action.v1"); } catch { } } _server = null; _network = null; _seen.Clear(); } } } 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 RunicSentinel.Runtime { internal static class SentinelBuildingLimits { [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class ConnectionPatch { private static bool Prepare() { return ShouldInstall(); } private static void Prefix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { return; } Reset(); _serverRpc = peer.m_rpc; peer.m_rpc.Register<ZPackage>("runic.sentinel.building.settings.v1", (Action<ZRpc, ZPackage>)delegate(ZRpc rpc, ZPackage package) { //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown if (rpc != _serverRpc || ZNet.instance != __instance || __instance.IsServer()) { return; } try { if (package == null || package.Size() != 16) { throw new InvalidOperationException("Invalid building settings packet."); } _remote = BuildingLimits.Decode(package.GetArray()); rpc.Invoke("runic.sentinel.building.ack.v1", new object[1] { (object)new ZPackage(_remote.Encode()) }); } catch (Exception) { Reset(); __instance.Disconnect(peer); } }); } } [HarmonyPatch(typeof(ZNet), "Disconnect", new Type[] { typeof(ZNetPeer) })] private static class DisconnectPatch { private static bool Prepare() { return ShouldInstall(); } private static void Prefix(ZNetPeer peer) { if (peer?.m_rpc != null) { Acknowledged.Remove(peer.m_rpc); if (peer.m_rpc == _serverRpc) { Reset(); } } } } [HarmonyPatch(typeof(ZNet), "Awake")] private static class ResetPatch { private static bool Prepare() { return ShouldInstall(); } private static void Prefix() { Reset(); } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] private static class DestroyPatch { private static bool Prepare() { return ShouldInstall(); } private static void Postfix() { Reset(); } } [HarmonyPatch] internal static class TerrainPatch { private static bool Prepare() { return ShouldInstall(); } private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(TerrainComp), "LevelTerrain", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(TerrainComp), "RaiseTerrain", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(TerrainComp), "ApplyToHeightmap", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Heightmap), "LevelTerrain", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Heightmap), "AtMaxWorldLevelDepth", (Type[])null, (Type[])null); } internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod) { //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Expected O, but got Unknown List<CodeInstruction> list = instructions.ToList(); bool flag = __originalMethod.Name == "AtMaxWorldLevelDepth"; int num = 0; int num2 = 0; for (int i = 0; i < list.Count; i++) { bool flag2 = list[i].opcode == OpCodes.Ldc_R4; bool flag3 = list[i].opcode == OpCodes.Ldc_R8; if (!flag2 && !flag3) { continue; } double num3 = Convert.ToDouble(list[i].operand); string name; int num4; object obj; if (flag && flag2 && (float)num3 == 7.95f) { name = "DigThreshold"; num++; } else { if (flag || num3 != -8.0) { if (flag || num3 != 8.0) { continue; } if (i + 1 < list.Count) { num4 = ((list[i + 1].opcode == OpCodes.Sub) ? 1 : 0); if (num4 != 0) { obj = "Dig"; goto IL_00fc; } } else { num4 = 0; } obj = "Raise"; goto IL_00fc; } name = "NegativeDig"; num++; } goto IL_010a; IL_00fc: name = (string)obj; if (num4 != 0) { num++; } else { num2++; } goto IL_010a; IL_010a: list[i].opcode = OpCodes.Call; list[i].operand = typeof(SentinelBuildingLimits).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic); if (flag3) { list.Insert(++i, new CodeInstruction(OpCodes.Conv_R8, (object)null)); } } if (num != 1 || num2 != ((!flag) ? 1 : 0)) { throw new InvalidOperationException("Building limit patch does not match installed Valheim: " + __originalMethod.DeclaringType.Name + "." + __originalMethod.Name); } return list; } } [HarmonyPatch(typeof(WearNTear), "GetMaterialProperties")] private static class SupportPatch { private static bool Prepare() { return ShouldInstall(); } private static void Postfix(ref float verticalLoss) { verticalLoss = Active.VerticalLoss(verticalLoss); } } private const string SettingsRpc = "runic.sentinel.building.settings.v1"; private const string AckRpc = "runic.sentinel.building.ack.v1"; private static BuildingLimits _local = BuildingLimits.Vanilla; private static BuildingLimits _remote = BuildingLimits.Vanilla; private static ZRpc _serverRpc; private static readonly HashSet<ZRpc> Acknowledged = new HashSet<ZRpc>(); internal static BuildingLimits Active { get { if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer()) { return _remote; } return _local; } } internal static bool ShouldInstall() { PropertyInfo propertyInfo = Type.GetType("RunicSentinel.Plugin, RunicSentinel", throwOnError: false)?.GetProperty("BuildingLimitsAvailable", BindingFlags.Static | BindingFlags.Public); if (!(propertyInfo == null)) { return !(bool)propertyInfo.GetValue(null); } return true; } internal static void Reset() { _remote = BuildingLimits.Vanilla; _serverRpc = null; Acknowledged.Clear(); } internal static float Raise() { return Active.RaiseHeight; } internal static float Dig() { return Active.DigDepth; } internal static float NegativeDig() { return 0f - Active.DigDepth; } internal static float DigThreshold() { return Active.DigDepth - 0.05f; } } } namespace RunicSentinel.Core { internal sealed class BuildingLimits { internal static readonly BuildingLimits Vanilla = new BuildingLimits(1f, 8f, 8f); internal readonly float SupportMultiplier; internal readonly float RaiseHeight; internal readonly float DigDepth; internal bool IsVanilla { get { if (SupportMultiplier == 1f && RaiseHeight == 8f) { return DigDepth == 8f; } return false; } } internal BuildingLimits(float supportMultiplier, float raiseHeight, float digDepth) { if (!Valid(supportMultiplier, 1f, 10f) || !Valid(raiseHeight, 1f, 64f) || !Valid(digDepth, 1f, 64f)) { throw new ArgumentOutOfRangeException("supportMultiplier", "Building limits must be finite and within their documented ranges."); } SupportMultiplier = supportMultiplier; RaiseHeight = raiseHeight; DigDepth = digDepth; } private static bool Valid(float value, float min, float max) { if (!float.IsNaN(value) && value >= min) { return value <= max; } return false; } internal float VerticalLoss(float vanilla) { return vanilla / SupportMultiplier; } internal byte[] Encode() { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream); binaryWriter.Write(1); binaryWriter.Write(SupportMultiplier); binaryWriter.Write(RaiseHeight); binaryWriter.Write(DigDepth); return memoryStream.ToArray(); } internal static BuildingLimits Decode(byte[] bytes) { if (bytes == null || bytes.Length != 16) { throw new InvalidDataException("Invalid building settings length."); } using BinaryReader binaryReader = new BinaryReader(new MemoryStream(bytes, writable: false)); if (binaryReader.ReadInt32() != 1) { throw new InvalidDataException("Unsupported building settings schema."); } return new BuildingLimits(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()); } } } 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, RunicText.Get("text_c2c47dd903fc") + RunicText.Get("text_3fffa17a165a")); } } [BepInPlugin("chazman.RunicSentinelClient", "Runic Sentinel Client", "1.0.5")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "chazman.RunicSentinelClient"; public const string Name = "Runic Sentinel Client"; public const string Version = "1.0.5"; private Harmony _harmony; private readonly PlayerActionReceiver _playerReceiver = new PlayerActionReceiver(); 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.5 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 static bool FullPlayerReceiverActive() { PropertyInfo propertyInfo = Type.GetType("RunicSentinel.Plugin, RunicSentinel", throwOnError: false)?.GetProperty("PlayerActionsAvailable", BindingFlags.Static | BindingFlags.Public); if (propertyInfo != null) { return (bool)propertyInfo.GetValue(null); } return false; } private void Update() { try { ActiveRuntime?.Tick(); if (FullPlayerReceiverActive()) { _playerReceiver.Dispose(); } else if (ActiveRuntime != null) { _playerReceiver.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() { _playerReceiver.Dispose(); try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch { } _harmony = null; SentinelBuildingLimits.Reset(); 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 static readonly FieldInfo InviteSecretKeyField = typeof(ZNet).GetField("m_inviteSecretKey", BindingFlags.Static | 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 string _nativeHandshakeSecret = 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(); if (!TryReadInviteSecretKey(out var value)) { BlockLocked(network, peer, rpc); Notify(_warning, RunicText.Get("text_fa3bfcf34dd5")); return false; } int num = StableHash("chazman.RunicSentinel.Admission.v2"); if (!TryGetFunctionMap(rpc, out var functions)) { BlockLocked(network, peer, rpc); Notify(_warning, RunicText.Get("text_965172dfe6a8")); return false; } if (functions.Contains(num)) { BlockLocked(network, peer, rpc); Notify(_warning, RunicText.Get("text_47ce59380df7")); 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, RunicText.Get("text_80678fbd62f4")); return false; } _network = network; _serverPeer = peer; _serverRpc = rpc; _registeredHandler = obj; _nativeHandshakeSecret = value; ResetExchangeLocked(); Notify(_information, RunicText.Get("text_256802b36bf6")); return true; } catch (Exception ex) { try { if (functions.Contains(num)) { rpc.Unregister("chazman.RunicSentinel.Admission.v2"); } } catch { } BlockLocked(network, peer, rpc); Notify(_warning, RunicText.Get("text_f5547dddf418") + ex.GetType().Name + ")."); return false; } } } internal void Tick() { //IL_033c: Unknown result type (might be due to invalid IL or missing references) //IL_0342: 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, RunicText.Get("text_3154b686bf87") + 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, RunicText.Get("text_4b41a28d971b") + status + ")."); } return; } byte[] array; try { array = AdmissionProtocolV2.EncodeReport(AdmissionProtocolV2.CreateReport(challenge, profile, "1.0.5", nowUnixSeconds)); } catch (Exception ex) { Notify(_warning, RunicText.Get("text_6bb69ccb1357") + 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, RunicText.Get("text_7e3fa6436397") + profile.Plugins.Count + RunicText.Get("text_24c65f4c73bd")); } catch (Exception ex2) { lock (_gate) { if (string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal)) { _sentRequestId = string.Empty; } } Notify(_warning, RunicText.Get("text_f441b5e60c09") + 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, RunicText.Get("text_3154b686bf87") + 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, RunicText.Get("text_77a95578092e")); } } 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, RunicText.Get("text_862b290fb35b") + failure + "."); return; } bool flag = false; string text = string.Empty; 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++; text = _nativeHandshakeSecret; } else { _completedRequestId = value.RequestId; } } if (value.Accepted) { Notify(_information, RunicText.Get("text_cfad5f821987") + value.ReasonCode + ")."); } else { Notify(_warning, RunicText.Get("text_2d3f4fc3053f") + value.ReasonCode + ")."); } if (!flag) { return; } try { rpc.Invoke("ServerHandshake", new object[1] { text }); 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, RunicText.Get("text_66337c067689") + 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; _nativeHandshakeSecret = string.Empty; 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 bool TryReadInviteSecretKey(out string value) { value = string.Empty; if (InviteSecretKeyField == null || !InviteSecretKeyField.IsStatic || InviteSecretKeyField.FieldType != typeof(string)) { return false; } try { value = (InviteSecretKeyField.GetValue(null) as string) ?? string.Empty; return value.Length <= 1024; } catch { value = string.Empty; 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; } } }