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 RustyHeathensModGuard v0.3.0
RustyHeathensModGuard.dll
Decompiled a week agousing System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using UnityEngine; [assembly: AssemblyTitle("Rusty Heathens ModGuard")] [assembly: AssemblyDescription("Heathens' Haven approved-client-mod checker")] [assembly: AssemblyCompany("RustyLaserGaming")] [assembly: AssemblyProduct("Rusty Heathens ModGuard")] [assembly: AssemblyFileVersion("0.3.0.0")] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("0.2.2.0")] namespace RustyHeathens.ModGuard; internal static class AdmissionRules { internal const double RetrySeconds = 3.0; internal static IList Snapshot(IList peers) { return new ArrayList(peers); } internal static bool DeadlineCanStart(bool ready, bool reported) { return ready && !reported; } internal static bool ShouldRetry(bool acknowledged, double now, double nextSend) { return !acknowledged && now >= nextSend; } } public enum GuardMode { Disabled, Audit, Enforce } [BepInPlugin("com.rustylasergaming.modguard", "Rusty Heathens ModGuard", "0.3.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginId = "com.rustylasergaming.modguard"; public const string PluginName = "Rusty Heathens ModGuard"; public const string PluginVersion = "0.3.0"; private const string RpcReport = "RHMG_Report_v1"; private const string RpcResult = "RHMG_Result_v1"; private const int CurrentConfigSchemaVersion = 2; private ConfigEntry<GuardMode> mode; private ConfigEntry<int> timeoutSeconds; private ConfigEntry<string> regularRules; private ConfigEntry<string> adminRules; private ConfigEntry<bool> collectHashes; private ConfigEntry<bool> logAccepted; private ConfigEntry<string> configuredAdminSteamIds; private ConfigEntry<int> configSchemaVersion; private bool rpcReady; private ZNet lastNet; private bool clientSent; private long clientServerPeer; private double clientNextSend; private string clientPayload; private readonly Dictionary<long, double> peerDeadlines = new Dictionary<long, double>(); private readonly HashSet<long> peerReported = new HashSet<long>(); private readonly HashSet<long> peerDenied = new HashSet<long>(); private Dictionary<string, string> regular; private Dictionary<string, string> adminOnly; private HashSet<string> configuredAdmins = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private MethodInfo disconnectMethod; private MethodInfo adminMethod; private FieldInfo peerUidField; private FieldInfo peerSocketField; private MethodInfo socketHostMethod; private void Awake() { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Expected O, but got Unknown mode = ((BaseUnityPlugin)this).Config.Bind<GuardMode>("Admission", "Mode", GuardMode.Enforce, "Disabled does nothing. Audit logs pass/fail but never disconnects. Enforce disconnects clients that fail. Restart after changing."); timeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Admission", "HandshakeTimeoutSeconds", 60, new ConfigDescription("Report deadline after the peer is ready for routed RPC. Clients retry until acknowledged.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 180), Array.Empty<object>())); collectHashes = ((BaseUnityPlugin)this).Config.Bind<bool>("Admission", "CollectPluginHashes", true, "Clients include SHA-256 hashes of loaded plugin DLLs when readable. Hashes are diagnostic only; policy enforcement uses plugin IDs and optional version pins from the server config."); logAccepted = ((BaseUnityPlugin)this).Config.Bind<bool>("Admission", "LogAcceptedReports", true, "Log a compact PASS line for accepted reports."); regularRules = ((BaseUnityPlugin)this).Config.Bind<string>("Policy", "AllowedForPlayers", string.Empty, "SERVER CONFIG IS AUTHORITATIVE. Comma-separated BepInEx plugin IDs allowed for normal players. Use bare GUIDs to allow any version (recommended so normal mod updates do not break admission). Use guid@version only when you intentionally want to pin one exact version. Allowed entries are optional; loaded plugins not listed here or in AdminOnly are rejected in Enforce mode."); adminRules = ((BaseUnityPlugin)this).Config.Bind<string>("Policy", "AdminOnly", string.Empty, "SERVER CONFIG IS AUTHORITATIVE. Comma-separated BepInEx plugin IDs allowed only for verified admins. Use bare GUIDs to allow any version; use guid@version only for an intentional exact-version pin."); configuredAdminSteamIds = ((BaseUnityPlugin)this).Config.Bind<string>("Policy", "AdminSteamIds", string.Empty, "Server-side ModGuard admin fallback. Accepts numeric, Steam_, or V_ account IDs. No admin IDs are built into ModGuard. This does not replace Valheim's real adminlist.txt, which other mods may use."); configSchemaVersion = ((BaseUnityPlugin)this).Config.Bind<int>("Policy", "ConfigSchemaVersion", 0, "Internal config schema marker. ModGuard may migrate syntax generically, but it never inserts an approved-mod list or admin IDs from code."); ApplyConfigSchemaUpgrade(); RebuildPolicy(); BindServerMethods(); ((BaseUnityPlugin)this).Logger.LogInfo((object)string.Concat("[RHMG] 0.3.0 loaded. mode=", mode.Value, ". Policy source=server config only; bare GUIDs allow any version. Same DLL must be installed on server and player clients. Client reports are not tamper-proof attestation.")); } private void ApplyConfigSchemaUpgrade() { if (configSchemaVersion != null && configSchemaVersion.Value < 2) { if (configSchemaVersion.Value < 1) { regularRules.Value = PolicyRules.StripVersions(regularRules.Value); adminRules.Value = PolicyRules.StripVersions(adminRules.Value); } if (timeoutSeconds.Value == 20) { timeoutSeconds.Value = 60; } configSchemaVersion.Value = 2; ((BaseUnityPlugin)this).Config.Save(); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHMG] Config schema upgraded to " + 2 + ": allowlists/admin IDs preserved; old default 20-second timeout upgraded to 60 seconds.")); } } private void RebuildPolicy() { regular = PolicyRules.ParseRules(regularRules.Value); adminOnly = PolicyRules.ParseRules(adminRules.Value); configuredAdmins = PolicyRules.ParseAdminIds((configuredAdminSteamIds != null) ? configuredAdminSteamIds.Value : string.Empty); if (mode != null && mode.Value == GuardMode.Enforce && regular.Count == 0 && adminOnly.Count == 0) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[RHMG] Enforce mode is active but the server policy lists are empty. Every reported plugin except ModGuard itself will be rejected until [Policy] AllowedForPlayers/AdminOnly are configured."); } } private void BindServerMethods() { BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; peerUidField = typeof(ZNetPeer).GetField("m_uid", bindingAttr); peerSocketField = typeof(ZNetPeer).GetField("m_socket", bindingAttr); disconnectMethod = typeof(ZNet).GetMethod("Disconnect", bindingAttr, null, new Type[1] { typeof(ZNetPeer) }, null); if (disconnectMethod == null) { disconnectMethod = typeof(ZNet).GetMethod("SendDisconnect", bindingAttr, null, new Type[1] { typeof(ZNetPeer) }, null); } adminMethod = typeof(ZNet).GetMethod("PlayerIsAdmin", bindingAttr, null, new Type[1] { typeof(string) }, null); } private void Update() { try { ZNet instance = ZNet.instance; if ((Object)(object)instance != (Object)(object)lastNet) { lastNet = instance; rpcReady = false; clientSent = false; clientServerPeer = 0L; clientPayload = null; peerDeadlines.Clear(); peerReported.Clear(); peerDenied.Clear(); RebuildPolicy(); } if ((Object)(object)instance == (Object)null || mode.Value == GuardMode.Disabled || ZRoutedRpc.instance == null) { return; } EnsureRpc(); if (rpcReady) { if (instance.IsServer()) { ServerTick(instance); } else { ClientTick(instance); } } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("[RHMG] Runtime error; no automatic policy action taken this frame. " + ex)); } } private void EnsureRpc() { if (!rpcReady) { ZRoutedRpc.instance.Register<string>("RHMG_Report_v1", (Action<long, string>)OnClientReport); ZRoutedRpc.instance.Register<string>("RHMG_Result_v1", (Action<long, string>)OnServerResult); rpcReady = true; } } private void ClientTick(ZNet net) { ZNetPeer val = null; try { val = net.GetServerPeer(); } catch { return; } if (val == null) { return; } long peerUid = GetPeerUid(val); if (peerUid == 0) { return; } double num = Time.realtimeSinceStartup; if (clientServerPeer != peerUid) { clientServerPeer = peerUid; clientSent = false; clientPayload = null; clientNextSend = num + 1.0; } if (AdmissionRules.ShouldRetry(clientSent, num, clientNextSend)) { if (clientPayload == null) { clientPayload = BuildClientReport(); } ZRoutedRpc.instance.InvokeRoutedRPC(peerUid, "RHMG_Report_v1", new object[1] { clientPayload }); clientNextSend = num + 3.0; ((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHMG] Client mod report sent to server. plugins=" + Chainloader.PluginInfos.Count)); } } private void ServerTick(ZNet net) { IList list = AdmissionRules.Snapshot(GetPeers(net)); double num = Time.realtimeSinceStartup; HashSet<long> hashSet = new HashSet<long>(); foreach (object item in list) { ZNetPeer val = (ZNetPeer)((item is ZNetPeer) ? item : null); if (val == null) { continue; } long peerUid = GetPeerUid(val); if (peerUid == 0) { continue; } hashSet.Add(peerUid); if (AdmissionRules.DeadlineCanStart(val.IsReady(), peerReported.Contains(peerUid))) { if (!peerDeadlines.ContainsKey(peerUid)) { peerDeadlines[peerUid] = num + (double)timeoutSeconds.Value; } if (!peerReported.Contains(peerUid) && !peerDenied.Contains(peerUid) && num >= peerDeadlines[peerUid]) { DenyPeer(net, val, peerUid, "ModGuard handshake was not received within " + timeoutSeconds.Value + " seconds."); } } } CleanupMissing(hashSet); } private IList GetPeers(ZNet net) { BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; MethodInfo method = typeof(ZNet).GetMethod("GetPeers", bindingAttr, null, Type.EmptyTypes, null); if (method != null) { object obj = method.Invoke(net, null); if (obj is IList result) { return result; } } FieldInfo field = typeof(ZNet).GetField("m_peers", bindingAttr); if (field != null && field.GetValue(net) is IList result2) { return result2; } return new ArrayList(); } private void CleanupMissing(HashSet<long> live) { List<long> list = new List<long>(); foreach (long key in peerDeadlines.Keys) { if (!live.Contains(key)) { list.Add(key); } } foreach (long item in list) { peerDeadlines.Remove(item); peerReported.Remove(item); peerDenied.Remove(item); } } private void OnClientReport(long sender, string payload) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } try { ZNetPeer val = FindPeer(instance, sender); if (val == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[RHMG] Report arrived from an unknown routed peer; ignored."); return; } peerReported.Add(sender); List<ModRecord> list = ParseClientReport(payload); bool flag = IsAdmin(instance, val); PolicyResult policyResult = PolicyRules.Evaluate(list, regular, adminOnly, flag, "com.rustylasergaming.modguard"); if (policyResult.Allowed) { if (logAccepted.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHMG] PASS client report. admin=" + flag + " plugins=" + list.Count)); } ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RHMG_Result_v1", new object[1] { "PASS|Approved mod profile." }); return; } string text = ((policyResult.Reasons.Count != 0) ? string.Join("; ", policyResult.Reasons.ToArray()) : "Mod profile did not match policy."); ((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHMG] FAIL client report. admin=" + flag + " reason=" + text)); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RHMG_Result_v1", new object[1] { "FAIL|" + text }); if (mode.Value == GuardMode.Enforce) { DenyPeer(instance, val, sender, text); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("[RHMG] Could not evaluate client report. " + ex)); } } private void OnServerResult(long sender, string result) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || instance.IsServer()) { return; } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null && GetPeerUid(serverPeer) == sender && !string.IsNullOrEmpty(result)) { if (result.StartsWith("PASS|", StringComparison.Ordinal) || result.StartsWith("FAIL|", StringComparison.Ordinal)) { clientSent = true; } if (result.StartsWith("PASS|", StringComparison.Ordinal)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"[RHMG] Server approved this mod profile."); } else if (result.StartsWith("FAIL|", StringComparison.Ordinal)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHMG] Server rejected this mod profile: " + result.Substring(5))); } } } private ZNetPeer FindPeer(ZNet net, long uid) { foreach (object peer in GetPeers(net)) { ZNetPeer val = (ZNetPeer)((peer is ZNetPeer) ? peer : null); if (val != null && GetPeerUid(val) == uid) { return val; } } return null; } private long GetPeerUid(ZNetPeer peer) { if (peerUidField == null) { return 0L; } object value = peerUidField.GetValue(peer); return (!(value is long)) ? 0 : ((long)value); } private bool IsAdmin(ZNet net, ZNetPeer peer) { string source; bool flag = IsAdmin(net, peer, out source); if (mode != null && mode.Value == GuardMode.Audit) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHMG] Admin check result=" + flag + " source=" + source + ". Account identifier omitted from log.")); } return flag; } private bool IsAdmin(ZNet net, ZNetPeer peer, out string source) { source = "none"; try { if (peerSocketField == null) { return false; } object value = peerSocketField.GetValue(peer); if (value == null) { return false; } if (socketHostMethod == null || socketHostMethod.DeclaringType != value.GetType()) { socketHostMethod = value.GetType().GetMethod("GetHostName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); } if (socketHostMethod == null) { return false; } string text = socketHostMethod.Invoke(value, null) as string; string text2 = PolicyRules.NormalizeAdminId(text); if (adminMethod != null) { string[] array = (string.IsNullOrEmpty(text2) ? new string[1] { text } : new string[3] { text, text2, "Steam_" + text2 }); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string[] array2 = array; foreach (string text3 in array2) { if (!string.IsNullOrWhiteSpace(text3) && hashSet.Add(text3)) { object obj = adminMethod.Invoke(net, new object[1] { text3 }); if (obj is bool && (bool)obj) { source = "valheim-admin-list"; return true; } } } } if (!string.IsNullOrEmpty(text2) && configuredAdmins.Contains(text2)) { source = "modguard-config"; return true; } } catch (Exception ex) { source = "error:" + ex.GetType().Name; } return false; } private void DenyPeer(ZNet net, ZNetPeer peer, long uid, string reason) { if (peerDenied.Contains(uid)) { return; } peerDenied.Add(uid); ((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHMG] DENY " + reason)); try { ZRoutedRpc.instance.InvokeRoutedRPC(uid, "RHMG_Result_v1", new object[1] { "FAIL|" + reason }); } catch { } if (mode.Value != GuardMode.Enforce) { return; } if (disconnectMethod == null) { ((BaseUnityPlugin)this).Logger.LogError((object)"[RHMG] Enforce requested but no supported ZNet disconnect method was found. Connection was NOT automatically disconnected."); return; } try { disconnectMethod.Invoke(net, new object[1] { peer }); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("[RHMG] Disconnect failed. " + ex)); } } private string BuildClientReport() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("protocol=1"); foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; string s = value.Metadata.GUID ?? pluginInfo.Key ?? string.Empty; string s2 = ((!(value.Metadata.Version == null)) ? value.Metadata.Version.ToString() : string.Empty); string s3 = string.Empty; if (collectHashes.Value) { try { string location = value.Location; if (!string.IsNullOrEmpty(location) && File.Exists(location)) { s3 = Sha256(location); } } catch { } } stringBuilder.Append(E(s)).Append('|').Append(E(s2)) .Append('|') .Append(E(s3)) .Append('\n'); } return stringBuilder.ToString(); } private static List<ModRecord> ParseClientReport(string payload) { List<ModRecord> list = new List<ModRecord>(); if (string.IsNullOrEmpty(payload)) { return list; } string[] array = payload.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0 || array[0].Trim() != "protocol=1") { return list; } for (int i = 1; i < array.Length; i++) { string[] array2 = array[i].Split('|'); if (array2.Length >= 2) { list.Add(new ModRecord { Guid = U(array2[0]), Version = U(array2[1]), Hash = ((array2.Length <= 2) ? string.Empty : U(array2[2])) }); } } return list; } private static string E(string s) { return Uri.EscapeDataString(s ?? string.Empty); } private static string U(string s) { return Uri.UnescapeDataString(s ?? string.Empty); } private static string Sha256(string path) { using SHA256 sHA = SHA256.Create(); using FileStream inputStream = File.OpenRead(path); byte[] array = sHA.ComputeHash(inputStream); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } } public sealed class ModRecord { public string Guid; public string Version; public string Hash; } public sealed class PolicyResult { public bool Allowed; public readonly List<string> Reasons = new List<string>(); } public static class PolicyRules { public static string NormalizeAdminId(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } string text = value.Trim(); if (text.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase)) { text = text.Substring(6); } else if (text.StartsWith("V_", StringComparison.OrdinalIgnoreCase)) { text = text.Substring(2); } if (text.Length < 15 || text.Length > 20) { return string.Empty; } for (int i = 0; i < text.Length; i++) { if (!char.IsDigit(text[i])) { return string.Empty; } } return text; } public static HashSet<string> ParseAdminIds(string text) { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(text)) { return hashSet; } string[] array = text.Split(new char[4] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); foreach (string value in array) { string text2 = NormalizeAdminId(value); if (text2.Length != 0) { hashSet.Add(text2); } } return hashSet; } public static string MergeAdminIds(string existing, string required) { List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string[] array = new string[2] { existing ?? string.Empty, required ?? string.Empty }; foreach (string text in array) { string[] array2 = text.Split(new char[4] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); foreach (string value in array2) { string text2 = NormalizeAdminId(value); if (text2.Length != 0 && hashSet.Add(text2)) { list.Add(text2); } } } return string.Join(",", list.ToArray()); } public static Dictionary<string, string> ParseRules(string text) { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(text)) { return dictionary; } string[] array = text.Split(new char[4] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text2 in array) { string text3 = text2.Trim(); if (text3.Length != 0) { int num = text3.LastIndexOf('@'); string text4 = ((num <= 0) ? text3 : text3.Substring(0, num).Trim()); string text5 = ((num <= 0) ? "*" : text3.Substring(num + 1).Trim()); if (text4.Length != 0) { dictionary[text4] = ((text5.Length != 0) ? text5 : "*"); } } } return dictionary; } public static string StripVersions(string text) { List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } string[] array = text.Split(new char[4] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text2 in array) { string text3 = text2.Trim(); if (text3.Length != 0) { int num = text3.LastIndexOf('@'); string text4 = ((num <= 0) ? text3 : text3.Substring(0, num).Trim()); if (text4.Length != 0 && hashSet.Add(text4)) { list.Add(text4); } } } return string.Join(",", list.ToArray()); } private static bool VersionAllowed(string actual, string expected) { return expected == "*" || string.Equals(actual ?? string.Empty, expected ?? string.Empty, StringComparison.OrdinalIgnoreCase); } public static PolicyResult Evaluate(IEnumerable<ModRecord> mods, IDictionary<string, string> regular, IDictionary<string, string> adminOnly, bool isAdmin, string selfGuid) { PolicyResult policyResult = new PolicyResult(); policyResult.Allowed = true; PolicyResult policyResult2 = policyResult; HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (ModRecord mod in mods) { if (mod == null || string.IsNullOrWhiteSpace(mod.Guid)) { continue; } if (!hashSet.Add(mod.Guid)) { policyResult2.Allowed = false; policyResult2.Reasons.Add("duplicate plugin id: " + mod.Guid); } else { if (string.Equals(mod.Guid, selfGuid, StringComparison.OrdinalIgnoreCase)) { continue; } if (regular.TryGetValue(mod.Guid, out var value)) { if (!VersionAllowed(mod.Version, value)) { policyResult2.Allowed = false; policyResult2.Reasons.Add(mod.Guid + " version " + mod.Version + " is not approved (expected " + value + ")"); } } else if (adminOnly.TryGetValue(mod.Guid, out value)) { if (!isAdmin) { policyResult2.Allowed = false; policyResult2.Reasons.Add(mod.Guid + " is admin-only"); } else if (!VersionAllowed(mod.Version, value)) { policyResult2.Allowed = false; policyResult2.Reasons.Add(mod.Guid + " admin build " + mod.Version + " is not approved (expected " + value + ")"); } } else { policyResult2.Allowed = false; policyResult2.Reasons.Add("unapproved plugin: " + mod.Guid + " " + mod.Version); } } } return policyResult2; } }
RustyHeathensControllerAudit.dll
Decompiled a week agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("RustyHeathensControllerAudit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+53644460c4405c784c83f30f8a806ea966ac4b58")] [assembly: AssemblyProduct("RustyHeathensControllerAudit")] [assembly: AssemblyTitle("RustyHeathensControllerAudit")] [assembly: AssemblyVersion("0.1.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 RustyHeathens.ControllerAudit { [BepInPlugin("com.rustylasergaming.controlleraudit", "Rusty Heathens Controller Audit", "0.1.0")] public sealed class Plugin : BaseUnityPlugin { private sealed class DeviceSnapshot { public List<string> Legacy = new List<string>(); public List<string> InputSystem = new List<string>(); public bool InputSystemUpdatedThisFrame; public bool Detected { get { if (Legacy.Count <= 0) { return InputSystem.Count > 0; } return true; } } public string Fingerprint => string.Join(";;", Legacy.ToArray()) + "##" + string.Join(";;", InputSystem.ToArray()); public static DeviceSnapshot Capture() { DeviceSnapshot deviceSnapshot = new DeviceSnapshot(); try { string[] array = Input.GetJoystickNames() ?? new string[0]; for (int i = 0; i < array.Length; i++) { string text = Safe(array[i], 160).Trim(); if (!string.IsNullOrEmpty(text) && !deviceSnapshot.Legacy.Contains(text)) { deviceSnapshot.Legacy.Add(text); } } } catch { } CaptureInputSystem(deviceSnapshot); deviceSnapshot.Legacy.Sort(StringComparer.OrdinalIgnoreCase); deviceSnapshot.InputSystem.Sort(StringComparer.OrdinalIgnoreCase); return deviceSnapshot; } private static void CaptureInputSystem(DeviceSnapshot snapshot) { Type type = FindType("UnityEngine.InputSystem.Gamepad"); if (type == null) { return; } try { PropertyInfo property = type.GetProperty("all", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); object obj = ((property == null) ? null : property.GetValue(null, null)); if (obj == null) { return; } foreach (object item2 in EnumerateReadOnlyArray(obj)) { if (item2 == null) { continue; } string value = Convert.ToString(ReadMember(item2, "displayName")) ?? ""; string value2 = Convert.ToString(ReadMember(item2, "name")) ?? ""; string value3 = Convert.ToString(ReadMember(item2, "layout")) ?? ""; string value4 = Convert.ToString(ReadMember(item2, "deviceId")) ?? ""; object instance = ReadMember(item2, "description"); string value5 = Convert.ToString(ReadMember(instance, "product")) ?? ""; string value6 = Convert.ToString(ReadMember(instance, "manufacturer")) ?? ""; string value7 = Convert.ToString(ReadMember(instance, "interfaceName")) ?? ""; string capabilities = Convert.ToString(ReadMember(instance, "capabilities")) ?? ""; string text = MatchId(VendorRegex, capabilities); string text2 = MatchId(ProductRegex, capabilities); bool flag = false; object obj2 = ReadMember(item2, "wasUpdatedThisFrame"); if (obj2 != null) { try { flag = Convert.ToBoolean(obj2); } catch { } } if (flag) { snapshot.InputSystemUpdatedThisFrame = true; } string item = "display=" + Safe(value, 120) + ";product=" + Safe(value5, 120) + ";manufacturer=" + Safe(value6, 100) + ";interface=" + Safe(value7, 80) + ";layout=" + Safe(value3, 80) + ";name=" + Safe(value2, 80) + ";deviceId=" + Safe(value4, 20) + ";vendorId=" + text + ";productId=" + text2; if (!snapshot.InputSystem.Contains(item)) { snapshot.InputSystem.Add(item); } } } catch { } } private static IEnumerable EnumerateReadOnlyArray(object value) { if (value is IEnumerable result) { return result; } List<object> list = new List<object>(); Type type = value.GetType(); object value2 = ReadMember(value, "Count"); int num = 0; try { num = Convert.ToInt32(value2); } catch { } PropertyInfo property = type.GetProperty("Item", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { for (int i = 0; i < num; i++) { try { list.Add(property.GetValue(value, new object[1] { i })); } catch { } } } return list; } private static string MatchId(Regex regex, string capabilities) { Match match = regex.Match(capabilities ?? ""); if (!match.Success) { return ""; } return match.Groups[1].Value; } public string ToPayload(bool activitySeen) { return "protocol=1|detected=" + (Detected ? "1" : "0") + "|activity=" + (activitySeen ? "1" : "0") + "|legacy=" + Safe(string.Join(" ;; ", Legacy.ToArray()), 800) + "|input=" + Safe(string.Join(" ;; ", InputSystem.ToArray()), 1800); } public string ToLocalSummary() { return "legacy=[" + Safe(string.Join(" ;; ", Legacy.ToArray()), 500) + "] input_system=[" + Safe(string.Join(" ;; ", InputSystem.ToArray()), 900) + "]"; } } private sealed class AuditReport { public bool Detected; public bool ActivitySeen; public string Legacy = ""; public string InputSystem = ""; public static AuditReport Parse(string payload) { AuditReport auditReport = new AuditReport(); string[] array = (payload ?? "").Split(new char[1] { '|' }); foreach (string text in array) { int num = text.IndexOf('='); if (num > 0) { string text2 = text.Substring(0, num); string text3 = text.Substring(num + 1); switch (text2) { case "detected": auditReport.Detected = text3 == "1"; break; case "activity": auditReport.ActivitySeen = text3 == "1"; break; case "legacy": auditReport.Legacy = text3; break; case "input": auditReport.InputSystem = text3; break; } } } return auditReport; } } public const string PluginGuid = "com.rustylasergaming.controlleraudit"; public const string PluginName = "Rusty Heathens Controller Audit"; public const string PluginVersion = "0.1.0"; private const string RpcName = "RHCA_ControllerAudit_v1"; private ConfigEntry<bool> _enabled; private ConfigEntry<float> _reportEverySeconds; private ConfigEntry<bool> _logClientChanges; private object _lastZNet; private bool _rpcRegistered; private float _nextRpcCheck; private float _nextReport; private string _lastFingerprint = ""; private bool _activitySeen; private static readonly Regex VendorRegex = new Regex("\\\"vendorId\\\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ProductRegex = new Regex("\\\"productId\\\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private void Awake() { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown _enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Audit", "Enabled", true, "Audit only. Detects controller/gamepad device names and reports them to the server. No controller input is blocked."); _reportEverySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Audit", "ReportEverySeconds", 15f, new ConfigDescription("How often a connected client sends a controller audit heartbeat. Device changes send immediately.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f), Array.Empty<object>())); _logClientChanges = ((BaseUnityPlugin)this).Config.Bind<bool>("Audit", "LogClientChanges", true, "Write a local log line when the detected controller list changes."); ((BaseUnityPlugin)this).Logger.LogInfo((object)"[RHCA] 0.1.0 loaded. AUDIT ONLY: no input is blocked and no serial numbers are collected."); } private void Update() { if (!_enabled.Value) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup >= _nextRpcCheck) { _nextRpcCheck = realtimeSinceStartup + 2f; EnsureRpc(); } if (!_rpcRegistered || IsServer()) { return; } if (LegacyControllerButtonPressed()) { _activitySeen = true; } DeviceSnapshot deviceSnapshot = DeviceSnapshot.Capture(); if (deviceSnapshot.InputSystemUpdatedThisFrame) { _activitySeen = true; } string fingerprint = deviceSnapshot.Fingerprint; if (!string.Equals(fingerprint, _lastFingerprint, StringComparison.Ordinal)) { _lastFingerprint = fingerprint; _nextReport = 0f; if (_logClientChanges.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHCA] Controller devices changed: " + deviceSnapshot.ToLocalSummary())); } } if (realtimeSinceStartup < _nextReport) { return; } long serverPeerUid = GetServerPeerUid(); if (serverPeerUid != 0L) { _nextReport = realtimeSinceStartup + Mathf.Clamp(_reportEverySeconds.Value, 5f, 120f); string payload = deviceSnapshot.ToPayload(_activitySeen); if (InvokeRoutedRpc(serverPeerUid, "RHCA_ControllerAudit_v1", payload)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHCA] Controller audit sent. detected=" + deviceSnapshot.Detected + " activity_seen=" + _activitySeen)); _activitySeen = false; } } } private void EnsureRpc() { object staticInstance = GetStaticInstance(FindType("ZNet")); if (staticInstance == null) { _rpcRegistered = false; _lastZNet = null; return; } if (staticInstance != _lastZNet) { _lastZNet = staticInstance; _rpcRegistered = false; } if (_rpcRegistered) { return; } Type type = FindType("ZRoutedRpc"); object staticInstance2 = GetStaticInstance(type); if (staticInstance2 == null || type == null) { return; } try { MethodInfo methodInfo = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "Register" && m.IsGenericMethodDefinition && m.GetParameters().Length == 2); if (!(methodInfo == null)) { MethodInfo methodInfo2 = methodInfo.MakeGenericMethod(typeof(string)); Action<long, string> action = OnAuditReport; methodInfo2.Invoke(staticInstance2, new object[2] { "RHCA_ControllerAudit_v1", action }); _rpcRegistered = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"[RHCA] Routed controller-audit RPC registered."); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHCA] Could not register audit RPC: " + ex.GetType().Name)); } } private void OnAuditReport(long sender, string payload) { if (!IsServer()) { return; } try { AuditReport auditReport = AuditReport.Parse(payload); string value = FindPeerName(sender); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHCA] CONTROLLER_AUDIT player=\\\"" + Safe(value, 80) + "\\\" uid=" + sender + " detected=" + auditReport.Detected + " activity_seen=" + auditReport.ActivitySeen + " legacy=[" + Safe(auditReport.Legacy, 700) + "] input_system=[" + Safe(auditReport.InputSystem, 1400) + "]")); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHCA] Controller audit from uid=" + sender + " could not be parsed: " + ex.GetType().Name)); } } private static bool LegacyControllerButtonPressed() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) try { for (int i = 0; i <= 19; i++) { if (Enum.TryParse<KeyCode>("JoystickButton" + i, out KeyCode result) && Input.GetKey(result)) { return true; } } } catch { } return false; } private bool IsServer() { object staticInstance = GetStaticInstance(FindType("ZNet")); if (staticInstance == null) { return false; } try { MethodInfo method = staticInstance.GetType().GetMethod("IsServer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return method != null && Convert.ToBoolean(method.Invoke(staticInstance, null)); } catch { return false; } } private long GetServerPeerUid() { object staticInstance = GetStaticInstance(FindType("ZNet")); if (staticInstance == null) { return 0L; } try { MethodInfo method = staticInstance.GetType().GetMethod("GetServerPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return ConvertToLong(ReadMember((method == null) ? null : method.Invoke(staticInstance, null), "m_uid")); } catch { return 0L; } } private string FindPeerName(long uid) { object staticInstance = GetStaticInstance(FindType("ZNet")); if (staticInstance == null) { return "unknown"; } try { MethodInfo method = staticInstance.GetType().GetMethod("GetPeers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(((method == null) ? ReadMember(staticInstance, "m_peers") : method.Invoke(staticInstance, null)) is IEnumerable enumerable)) { return "unknown"; } foreach (object item in enumerable) { if (ConvertToLong(ReadMember(item, "m_uid")) == uid) { string text = Convert.ToString(ReadMember(item, "m_playerName")) ?? Convert.ToString(ReadMember(item, "m_playerNameCached")); return string.IsNullOrWhiteSpace(text) ? "unknown" : text; } } } catch { } return "unknown"; } private bool InvokeRoutedRpc(long target, string methodName, string payload) { Type type = FindType("ZRoutedRpc"); object staticInstance = GetStaticInstance(type); if (type == null || staticInstance == null) { return false; } try { MethodInfo methodInfo = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(MethodInfo m) { if (m.Name != "InvokeRoutedRPC") { return false; } ParameterInfo[] parameters = m.GetParameters(); return parameters.Length == 3 && parameters[0].ParameterType == typeof(long) && parameters[1].ParameterType == typeof(string); }); if (methodInfo == null) { return false; } methodInfo.Invoke(staticInstance, new object[3] { target, methodName, new object[1] { payload } }); return true; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHCA] Audit send failed: " + ex.GetType().Name)); return false; } } private static Type FindType(string fullName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { Type type = assembly.GetType(fullName, throwOnError: false); if (type != null) { return type; } } catch { } } return null; } private static object GetStaticInstance(Type type) { if (type == null) { return null; } PropertyInfo property = type.GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property.GetValue(null, null); } FieldInfo field = type.GetField("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(null); } return null; } private static object ReadMember(object instance, string name) { if (instance == null) { return null; } Type type = instance.GetType(); PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { try { return property.GetValue(instance, null); } catch { } } FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { try { return field.GetValue(instance); } catch { } } return null; } private static long ConvertToLong(object value) { if (value == null) { return 0L; } try { return Convert.ToInt64(value); } catch { Match match = Regex.Match(Convert.ToString(value) ?? "", "(\\d{5,})"); long result; return (match.Success && long.TryParse(match.Groups[1].Value, out result)) ? result : 0; } } private static string Safe(string value, int max) { string text = (value ?? "").Replace("\\r", " ").Replace("\\n", " ").Replace("|", "/"); if (text.Length > max) { text = text.Substring(0, max); } return text; } } public static class MyPluginInfo { public const string PLUGIN_GUID = "RustyHeathensControllerAudit"; public const string PLUGIN_NAME = "RustyHeathensControllerAudit"; public const string PLUGIN_VERSION = "0.1.0"; } }