using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using BoneLib.BoneMenu; using BoneLib.Notifications; using HarmonyLib; using Il2CppSLZ.Marrow.Pool; using LabFusion.Marrow.Serialization; using LabFusion.Network; using LabFusion.Player; using LabFusion.Representation; using MelonLoader; using MelonLoader.Preferences; using SpawnGuard; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: MelonInfo(typeof(Main), "SpawnGuard", "1.0.0", "YourName", null)] [assembly: MelonGame("Stress Level Zero", "BONELAB")] [assembly: AssemblyTitle("SpawnWall")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("SpawnWall")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("0e80b46e-6f26-4e3b-9677-42beb4ac15fc")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace SpawnGuard; public static class SpawnGuardConfig { private static MelonPreferences_Category _category; public static MelonPreferences_Entry Enabled; public static MelonPreferences_Entry LogOnly; public static MelonPreferences_Entry ShowNotifications; public static MelonPreferences_Entry LogAllSpawns; public static MelonPreferences_Entry PreventOnHost; public static MelonPreferences_Entry AllowAvatarsThroughFirewall; public static MelonPreferences_Entry NotificationCooldown; public static MelonPreferences_Entry EnableRateLimit; public static MelonPreferences_Entry SpawnCooldown; public static MelonPreferences_Entry SpawnBurstCount; public static MelonPreferences_Entry RateLimitAppliesToHost; public static MelonPreferences_Entry RequiredPermissionRaw; public static PermissionLevel RequiredPermission => (PermissionLevel)(sbyte)RequiredPermissionRaw.Value; public static void Initialize() { _category = MelonPreferences.CreateCategory("SpawnGuard"); Enabled = _category.CreateEntry("Enabled", true, "Enabled", "Master toggle. When off, SpawnGuard does nothing.", false, false, (ValueValidator)null, (string)null); LogOnly = _category.CreateEntry("LogOnly", false, "Log Only (Debug)", "Logs what WOULD be blocked instead of actually blocking.", false, false, (ValueValidator)null, (string)null); ShowNotifications = _category.CreateEntry("ShowNotifications", true, "Show Notifications", "Show an in-headset warning whenever a spawn is blocked.", false, false, (ValueValidator)null, (string)null); LogAllSpawns = _category.CreateEntry("LogAllSpawns", false, "Log All Spawns", "Log every spawn request regardless of outcome.", false, false, (ValueValidator)null, (string)null); PreventOnHost = _category.CreateEntry("PreventOnHost", true, "Prevent At Source (Host Only)", "Blocks a disallowed spawn before SpawnResponseData is broadcast. Only active when you are the host.", false, false, (ValueValidator)null, (string)null); AllowAvatarsThroughFirewall = _category.CreateEntry("AllowAvatarsThroughFirewall", true, "Allow Avatars (Temporary Bypass)", "TEMPORARY: barcodes containing 'Avatar' skip SpawnGuard entirely.", false, false, (ValueValidator)null, (string)null); NotificationCooldown = _category.CreateEntry("NotificationCooldown", 2.5f, "Notification Cooldown (s)", "Minimum seconds between in-headset popups.", false, false, (ValueValidator)null, (string)null); EnableRateLimit = _category.CreateEntry("EnableRateLimit", false, "Enable Spawn Rate Limit", "Limits how many spawns a player can make within the cooldown window.", false, false, (ValueValidator)null, (string)null); SpawnCooldown = _category.CreateEntry("SpawnCooldown", 0.5f, "Spawn Cooldown (s)", "Length of the rate-limit window in seconds. Range 0.2-3.0.", false, false, (ValueValidator)null, (string)null); SpawnBurstCount = _category.CreateEntry("SpawnBurstCount", 1, "Spawns Before Rate Limit", "How many spawns are allowed within one cooldown window before throttling kicks in. 1 = one spawn per window, 2 = two spawns before the third is blocked, etc.", false, false, (ValueValidator)null, (string)null); RateLimitAppliesToHost = _category.CreateEntry("RateLimitAppliesToHost", false, "Rate Limit: Include Host", "When off (default), the host is exempt from the spawn cooldown. Enable if host PlayerID spoofing is a concern.", false, false, (ValueValidator)null, (string)null); RequiredPermissionRaw = _category.CreateEntry("RequiredPermission", 0, "Required Permission Level", "Minimum permission to spawn without being blocked. -1=GUEST, 0=DEFAULT, 1=OPERATOR, 2=OWNER.", false, false, (ValueValidator)null, (string)null); SpawnLogHistory.Initialize(_category); } public static void Save() { _category.SaveToFile(false); } } public static class BoneMenuHandler { private static Page mainPage; private static readonly Color MenuColor = Color.white; public static void Initialize() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) mainPage = Page.Root.CreatePage("SpawnGuard", MenuColor, 0, true); CreateElements(); } private static void CreateElements() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_01e0: Unknown result type (might be due to invalid IL or missing references) //IL_021e: Unknown result type (might be due to invalid IL or missing references) //IL_026b: Unknown result type (might be due to invalid IL or missing references) //IL_02ad: Unknown result type (might be due to invalid IL or missing references) //IL_02eb: Unknown result type (might be due to invalid IL or missing references) //IL_031f: Unknown result type (might be due to invalid IL or missing references) //IL_0324: Unknown result type (might be due to invalid IL or missing references) mainPage.CreateBool("Enabled", MenuColor, SpawnGuardConfig.Enabled.Value, (Action)delegate(bool value) { SpawnGuardConfig.Enabled.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateBool("Log Only (Debug)", Color.yellow, SpawnGuardConfig.LogOnly.Value, (Action)delegate(bool value) { SpawnGuardConfig.LogOnly.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateBool("Show Notifications", MenuColor, SpawnGuardConfig.ShowNotifications.Value, (Action)delegate(bool value) { SpawnGuardConfig.ShowNotifications.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateBool("Log All Spawns", Color.green, SpawnGuardConfig.LogAllSpawns.Value, (Action)delegate(bool value) { SpawnGuardConfig.LogAllSpawns.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateBool("Prevent At Source (Host Only)", new Color(1f, 0.5f, 0f, 1f), SpawnGuardConfig.PreventOnHost.Value, (Action)delegate(bool value) { SpawnGuardConfig.PreventOnHost.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateFloat("Notification Cooldown (s)", Color.white, SpawnGuardConfig.NotificationCooldown.Value, 0.5f, 0f, 10f, (Action)delegate(float value) { SpawnGuardConfig.NotificationCooldown.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateBool("Allow Avatars (Temporary Bypass)", Color.red, SpawnGuardConfig.AllowAvatarsThroughFirewall.Value, (Action)delegate(bool value) { SpawnGuardConfig.AllowAvatarsThroughFirewall.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateBool("Enable Spawn Rate Limit", Color.cyan, SpawnGuardConfig.EnableRateLimit.Value, (Action)delegate(bool value) { SpawnGuardConfig.EnableRateLimit.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateFloat("Spawn Cooldown (s)", Color.cyan, SpawnGuardConfig.SpawnCooldown.Value, 0.1f, 0.2f, 3f, (Action)delegate(float value) { SpawnGuardConfig.SpawnCooldown.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateInt("Spawns Before Rate Limit", Color.cyan, SpawnGuardConfig.SpawnBurstCount.Value, 1, 1, 10, (Action)delegate(int value) { SpawnGuardConfig.SpawnBurstCount.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateBool("Rate Limit: Include Host", Color.magenta, SpawnGuardConfig.RateLimitAppliesToHost.Value, (Action)delegate(bool value) { SpawnGuardConfig.RateLimitAppliesToHost.Value = value; SpawnGuardConfig.Save(); }); mainPage.CreateFunction("Print Last Session Log", Color.magenta, (Action)delegate { MelonLogger.Msg("[SpawnGuard] --- Last Session Log ---\n" + SpawnLogHistory.GetSavedLog()); }); mainPage.CreateEnum("Required Permission", Color.cyan, (Enum)(object)SpawnGuardConfig.RequiredPermission, (Action)delegate(Enum value) { SpawnGuardConfig.RequiredPermissionRaw.Value = Convert.ToInt32(value); SpawnGuardConfig.Save(); }); } } [HarmonyPatch(typeof(SpawnResponseMessage), "OnSpawnFinished")] public static class SpawnGuard_OnSpawnFinishedPatch { private static readonly Dictionary _recentlyHandled = new Dictionary(); private const float DedupeWindowSeconds = 1f; private const int DedupeMapPruneThreshold = 2000; [HarmonyPostfix] public static void Postfix(SpawnResponseData data, Poolee poolee) { if (!SpawnGuardConfig.Enabled.Value || !SpawnGuardConfig.LogAllSpawns.Value || NetworkInfo.IsHost) { return; } try { if (data == null) { return; } string text = "(unknown)"; if (data.SpawnData != null && !string.IsNullOrEmpty(data.SpawnData.Barcode)) { text = data.SpawnData.Barcode; } if (AvatarBypass.ShouldBypass(text)) { return; } float time = Time.time; if (_recentlyHandled.TryGetValue(data.EntityID, out var value) && time - value < 1f) { return; } _recentlyHandled[data.EntityID] = time; if (_recentlyHandled.Count > 2000) { List list = new List(); foreach (KeyValuePair item in _recentlyHandled) { if (time - item.Value > 1f) { list.Add(item.Key); } } for (int i = 0; i < list.Count; i++) { _recentlyHandled.Remove(list[i]); } } byte ownerID = data.OwnerID; PlayerID playerID = PlayerIDManager.GetPlayerID(ownerID); string text3 = default(string); string text2 = ((!PlayerID.IsNullOrInvalid(playerID)) ? (MetadataHelper.TryGetDisplayName(playerID, ref text3) ? text3 : ("Player " + ownerID)) : ("Unknown (ID " + ownerID + ")")); string line = "[SpawnGuard] " + text2 + " spawned " + text; SpawnLogAggregator.Enqueue(text2, text, line); } catch (Exception ex) { MelonLogger.Error("[SpawnGuard] Error in spawn observer postfix: " + ex); } } } public class Main : MelonMod { private Harmony _harmony; public override void OnInitializeMelon() { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Expected O, but got Unknown SpawnGuardConfig.Initialize(); _harmony = new Harmony("com.yourname.spawnguard"); _harmony.PatchAll(typeof(Main).Assembly); MelonLogger.Msg("[SpawnGuard] Initialized."); } public override void OnLateInitializeMelon() { BoneMenuHandler.Initialize(); } public override void OnUpdate() { SpawnLogAggregator.Tick(); } public override void OnDeinitializeMelon() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } public override void OnApplicationQuit() { SpawnLogHistory.Flush(); } } public static class AvatarBypass { public static bool ShouldBypass(string barcode) { if (!SpawnGuardConfig.AllowAvatarsThroughFirewall.Value) { return false; } if (string.IsNullOrEmpty(barcode)) { return false; } return barcode.IndexOf("Avatar", StringComparison.OrdinalIgnoreCase) >= 0; } } public static class SpawnLogAggregator { private struct AggregateKey { public string PlayerName; public string Barcode; public AggregateKey(string playerName, string barcode) { PlayerName = playerName; Barcode = barcode; } public override bool Equals(object obj) { if (!(obj is AggregateKey aggregateKey)) { return false; } return PlayerName == aggregateKey.PlayerName && Barcode == aggregateKey.Barcode; } public override int GetHashCode() { int num = ((PlayerName != null) ? PlayerName.GetHashCode() : 0); int num2 = ((Barcode != null) ? Barcode.GetHashCode() : 0); return num ^ (num2 << 5); } } private struct AggregateEntry { public string FirstLine; public int Count; } private const int MassSpawnThreshold = 50; private const double FlushWindowSeconds = 0.1; private static readonly Dictionary _pending = new Dictionary(); private static double _windowStartUtc = 0.0; private static bool _windowOpen = false; public static void Enqueue(string playerName, string barcode, string line) { double windowStartUtc = UtcSeconds(); AggregateKey key = new AggregateKey(playerName, barcode); if (_pending.TryGetValue(key, out var value)) { value.Count++; _pending[key] = value; } else { _pending[key] = new AggregateEntry { FirstLine = line, Count = 1 }; } _windowStartUtc = windowStartUtc; _windowOpen = true; } public static void Tick() { if (_windowOpen && _pending.Count != 0) { double num = UtcSeconds() - _windowStartUtc; if (!(num < 0.1)) { Flush(); } } } private static void Flush() { foreach (KeyValuePair item in _pending) { AggregateEntry value = item.Value; string text = ((value.Count != 1) ? ((value.Count <= 50) ? ("[SpawnGuard] " + value.Count + "x '" + item.Key.Barcode + "' spawned by " + item.Key.PlayerName + ".") : ("[SpawnGuard] MASS SPAWN ATTEMPT: " + value.Count + " spawn attempts of '" + item.Key.Barcode + "' from " + item.Key.PlayerName + " in 0.1s window.")) : value.FirstLine); MelonLogger.Msg(ConsoleColor.Yellow, text); SpawnLogHistory.Append(text); } _pending.Clear(); _windowOpen = false; } private static double UtcSeconds() { return (double)DateTime.UtcNow.Ticks / 10000000.0; } } public static class SpawnRateLimiter { private struct PlayerState { public long WindowStartTicks; public int SpawnsInWindow; } private static readonly Dictionary _states = new Dictionary(); private const long TicksPerSecond = 10000000L; private const long IdleEvictTicks = 3000000000L; private const int PruneThreshold = 512; public static bool TryAllowSpawn(byte playerID, bool isLocalHost) { if (!SpawnGuardConfig.EnableRateLimit.Value) { return true; } if (isLocalHost && !SpawnGuardConfig.RateLimitAppliesToHost.Value) { return true; } long ticks = DateTime.UtcNow.Ticks; long num = (long)(SpawnGuardConfig.SpawnCooldown.Value * 10000000f); int value = SpawnGuardConfig.SpawnBurstCount.Value; if (!_states.TryGetValue(playerID, out var value2)) { _states[playerID] = new PlayerState { WindowStartTicks = ticks, SpawnsInWindow = 1 }; MaybePrune(ticks); return true; } long num2 = ticks - value2.WindowStartTicks; if (num2 >= num) { _states[playerID] = new PlayerState { WindowStartTicks = ticks, SpawnsInWindow = 1 }; return true; } if (value2.SpawnsInWindow < value) { value2.SpawnsInWindow++; _states[playerID] = value2; return true; } _states[playerID] = new PlayerState { WindowStartTicks = ticks, SpawnsInWindow = 0 }; return false; } public static void ClearPlayer(byte playerID) { _states.Remove(playerID); } public static void ClearAll() { _states.Clear(); } private static void MaybePrune(long nowTicks) { if (_states.Count <= 512) { return; } List list = new List(); foreach (KeyValuePair state in _states) { if (nowTicks - state.Value.WindowStartTicks > 3000000000u) { list.Add(state.Key); } } for (int i = 0; i < list.Count; i++) { _states.Remove(list[i]); } } } public static class SpawnNotifier { private static float _lastShownTime = -999f; private static int _suppressedCount = 0; public static void Warn(string title, string message) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Expected O, but got Unknown if (!SpawnGuardConfig.ShowNotifications.Value) { return; } try { float time = Time.time; float value = SpawnGuardConfig.NotificationCooldown.Value; if (time - _lastShownTime < value) { _suppressedCount++; return; } string text = message; if (_suppressedCount > 0) { text = text + " (+" + _suppressedCount + " more)"; } Notifier.Send(new Notification { ShowTitleOnPopup = true, Title = NotificationText.op_Implicit(title), Message = NotificationText.op_Implicit(text), PopupLength = 4f, Type = (NotificationType)1 }); _lastShownTime = time; _suppressedCount = 0; } catch (Exception ex) { MelonLogger.Error("[SpawnGuard] Notification failed: " + ex); } } } public static class SpawnLogHistory { private const int MaxEntries = 5000; private static MelonPreferences_Category _category; private static MelonPreferences_Entry _savedLog; private static readonly List _entries = new List(); public static void Initialize(MelonPreferences_Category category) { _category = category; _savedLog = _category.CreateEntry("LastSessionLog", "", "Last Session Log", "The full SpawnGuard event log from the most recent game session, kept across scene changes and updated continuously so it survives even a crash. Overwritten starting with the first event of a new session.", false, false, (ValueValidator)null, (string)null); _entries.Clear(); } public static void Append(string line) { try { float time = Time.time; int num = (int)(time / 60f); int num2 = (int)time % 60; string item = "[" + num.ToString("D2") + ":" + num2.ToString("D2") + "] " + line; _entries.Add(item); if (_entries.Count > 5000) { _entries.RemoveAt(0); } Flush(); } catch (Exception ex) { MelonLogger.Error("[SpawnGuard] Failed to append to session log: " + ex); } } public static void Flush() { if (_savedLog == null || _category == null) { return; } try { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < _entries.Count; i++) { stringBuilder.Append(_entries[i]); stringBuilder.Append('\n'); } _savedLog.Value = stringBuilder.ToString(); _category.SaveToFile(false); } catch (Exception ex) { MelonLogger.Error("[SpawnGuard] Failed to flush session log: " + ex); } } public static string GetSavedLog() { return (_savedLog != null) ? _savedLog.Value : ""; } } [HarmonyPatch(typeof(SpawnRequestMessage), "OnHandleMessage")] public static class SpawnGuard_OnSpawnRequestPatch { [HarmonyPrefix] public static bool Prefix(ReceivedMessage received) { //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Unknown result type (might be due to invalid IL or missing references) if (!SpawnGuardConfig.Enabled.Value || !SpawnGuardConfig.PreventOnHost.Value) { return true; } if (!NetworkInfo.IsHost) { return true; } try { byte value = ((ReceivedMessage)(ref received)).Sender.Value; PlayerID playerID = PlayerIDManager.GetPlayerID(value); bool isLocalHost = false; string text; if (PlayerID.IsNullOrInvalid(playerID)) { text = "Unknown (ID " + value + ")"; } else { string text2 = default(string); text = (MetadataHelper.TryGetDisplayName(playerID, ref text2) ? text2 : ("Player " + value)); isLocalHost = playerID.IsMe; } string text3 = "(unknown)"; try { SerializedSpawnData val = ((ReceivedMessage)(ref received)).ReadData(); if (val != null) { text3 = val.Barcode; } } catch { } if (AvatarBypass.ShouldBypass(text3)) { return true; } if (!SpawnRateLimiter.TryAllowSpawn(value, isLocalHost)) { string text4 = "[SpawnGuard] Rate-limited spawn of '" + text3 + "' from " + text + " - cooldown not elapsed, timer reset."; if (SpawnGuardConfig.LogOnly.Value) { string line = "(log-only) " + text4; SpawnLogAggregator.Enqueue(text, text3, line); return true; } SpawnLogAggregator.Enqueue(text, text3, text4); SpawnNotifier.Warn("SpawnGuard", "Rate-limited '" + text3 + "' from " + text); return false; } bool flag; if (PlayerID.IsNullOrInvalid(playerID)) { flag = false; } else { PermissionLevel val2 = default(PermissionLevel); Color val3 = default(Color); FusionPermissions.FetchPermissionLevel(playerID.PlatformID, ref val2, ref val3); flag = FusionPermissions.HasSufficientPermissions(val2, SpawnGuardConfig.RequiredPermission); } if (SpawnGuardConfig.LogAllSpawns.Value && flag) { string line2 = "[SpawnGuard] " + text + " spawned " + text3; SpawnLogAggregator.Enqueue(text, text3, line2); return true; } if (flag) { return true; } if (SpawnGuardConfig.LogOnly.Value) { string line3 = "[SpawnGuard] (log-only) Would block spawn of '" + text3 + "' from " + text + " - no permission."; SpawnLogAggregator.Enqueue(text, text3, line3); return true; } string line4 = "[SpawnGuard] Blocked spawn of '" + text3 + "' from " + text + " - no permission. Never broadcast."; SpawnLogAggregator.Enqueue(text, text3, line4); SpawnNotifier.Warn("SpawnGuard", "Blocked '" + text3 + "' from " + text + " (no permission)"); return false; } catch (Exception ex) { MelonLogger.Error("[SpawnGuard] Error in host-prevention prefix, failing open: " + ex); return true; } } }