using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BetterModeration; using HarmonyLib; using LabFusion.Marrow.Proxies; using LabFusion.Menu; using LabFusion.Network; using LabFusion.Player; using LabFusion.Senders; using LabFusion.UI.Popups; using LabFusion.Utilities; using MelonLoader; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Main), "Better Moderation", "1.0.0", "armyk_r3lv", null)] [assembly: MelonGame("Stress Level Zero", "BONELAB")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("BetterModeration")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("BetterModeration")] [assembly: AssemblyTitle("BetterModeration")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 BetterModeration { internal sealed class TimedBan { public ulong PlatformId { get; set; } public string LastKnownName { get; set; } = "Unknown"; public string Reason { get; set; } = "Host moderation"; public DateTime CreatedUtc { get; set; } public DateTime ExpiresUtc { get; set; } } internal static class BanStore { private static readonly object Sync = new object(); private static readonly string DirectoryPath = Path.Combine(MelonEnvironment.UserDataDirectory, "BetterModeration"); private static readonly string FilePath = Path.Combine(DirectoryPath, "timed-bans.json"); private static List _bans = new List(); internal static IReadOnlyList Active { get { lock (Sync) { PurgeExpired(save: true); return _bans.ToArray(); } } } internal static void Load() { lock (Sync) { Directory.CreateDirectory(DirectoryPath); try { _bans = (File.Exists(FilePath) ? (JsonConvert.DeserializeObject>(File.ReadAllText(FilePath)) ?? new List()) : new List()); } catch (Exception ex) { MelonLogger.Error("Better Moderation could not read its ban file: " + ex.Message); _bans = new List(); } PurgeExpired(save: true); } } internal static bool TryGet(ulong platformId, out TimedBan? ban) { lock (Sync) { PurgeExpired(save: true); ban = _bans.FirstOrDefault((TimedBan item) => item.PlatformId == platformId); return ban != null; } } internal static void Add(ulong platformId, string name, TimeSpan duration, string reason) { if (duration <= TimeSpan.Zero || duration > TimeSpan.FromDays(30.0)) { throw new ArgumentOutOfRangeException("duration", "Timed bans must be between one minute and 30 days."); } lock (Sync) { DateTime utcNow = DateTime.UtcNow; _bans.RemoveAll((TimedBan item) => item.PlatformId == platformId); _bans.Add(new TimedBan { PlatformId = platformId, LastKnownName = (string.IsNullOrWhiteSpace(name) ? "Unknown" : name), Reason = (string.IsNullOrWhiteSpace(reason) ? "Host moderation" : reason), CreatedUtc = utcNow, ExpiresUtc = utcNow.Add(duration) }); Save(); } } internal static bool Remove(ulong platformId) { lock (Sync) { bool num = _bans.RemoveAll((TimedBan item) => item.PlatformId == platformId) != 0; if (num) { Save(); } return num; } } private static void PurgeExpired(bool save) { int num = _bans.RemoveAll((TimedBan item) => item.ExpiresUtc <= DateTime.UtcNow); if (save && num != 0) { Save(); } } private static void Save() { Directory.CreateDirectory(DirectoryPath); string text = FilePath + ".tmp"; File.WriteAllText(text, JsonConvert.SerializeObject((object)_bans, (Formatting)1)); File.Copy(text, FilePath, overwrite: true); File.Delete(text); } } internal sealed class Main : MelonMod { [HarmonyPatch(typeof(ConnectionRequestMessage), "OnHandleMessage")] [HarmonyPriority(800)] private static class ConnectionRequestGuard { private static bool Prefix(ReceivedMessage received) { if (!NetworkInfo.IsHost) { return true; } ConnectionRequestData val = ((ReceivedMessage)(ref received)).ReadData(); ulong? platformID = ((ReceivedMessage)(ref received)).PlatformID; if (!platformID.HasValue || !ModerationService.ShouldDeny(platformID.Value, val.BackupPlatformID, out string reason)) { return true; } ConnectionSender.SendConnectionDeny(platformID.Value, reason); MelonLogger.Warning($"Rejected connection from authenticated platform ID {platformID.Value}: {reason}."); return false; } } [CompilerGenerated] private static class <>O { public static Action <0>__PatchNativeUiAfterFusionLogin; public static ServerEvent <1>__StartNewSession; } private static bool _nativeUiPatched; public override void OnInitializeMelon() { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Expected O, but got Unknown BanStore.Load(); NetworkLayer.OnLoggedInEvent += PatchNativeUiAfterFusionLogin; object obj = <>O.<1>__StartNewSession; if (obj == null) { ServerEvent val = ModerationService.StartNewSession; <>O.<1>__StartNewSession = val; obj = (object)val; } MultiplayerHooking.OnJoinedServer += (ServerEvent)obj; object obj2 = <>O.<1>__StartNewSession; if (obj2 == null) { ServerEvent val2 = ModerationService.StartNewSession; <>O.<1>__StartNewSession = val2; obj2 = (object)val2; } MultiplayerHooking.OnDisconnected += (ServerEvent)obj2; MelonLogger.Msg("Better Moderation initialized; waiting for Fusion login before extending its Actions menu."); } private static void PatchNativeUiAfterFusionLogin(NetworkLayer _) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Expected O, but got Unknown if (!_nativeUiPatched) { MethodInfo methodInfo = AccessTools.Method(typeof(MenuLocation), "ApplyPlayerToElement", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(NativeFusionMenu), "Postfix", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { MelonLogger.Error("Better Moderation could not locate Fusion's player Actions UI methods."); return; } new Harmony("BetterModeration.NativeFusionUi").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _nativeUiPatched = true; MelonLogger.Msg("Better Moderation added to Fusion's native player Actions menu after login."); } } } internal static class ModerationService { private static readonly Dictionary SessionBans = new Dictionary(); internal static IReadOnlyDictionary ActiveSessionBans => new Dictionary(SessionBans); internal static void StartNewSession() { SessionBans.Clear(); MelonLogger.Msg("Better Moderation: session-ban list cleared for the new server session."); } internal static bool IsSessionBanned(ulong platformId) { return SessionBans.ContainsKey(platformId); } internal static void Kick(PlayerID player) { if (CanModerate(player)) { NetworkHelper.KickUser(player); MelonLogger.Warning($"Kicked {SafeName(player)} ({player.PlatformID})."); } } internal static void SessionBan(PlayerID player) { if (CanModerate(player)) { SessionBans[player.PlatformID] = SafeName(player); NetworkHelper.KickUser(player); MelonLogger.Warning($"Session-banned {SafeName(player)} ({player.PlatformID})."); } } internal static void TimedBan(PlayerID player, TimeSpan duration) { if (CanModerate(player)) { BanStore.Add(player.PlatformID, SafeName(player), duration, "Host moderation"); NetworkHelper.KickUser(player); MelonLogger.Warning($"Timed-banned {SafeName(player)} ({player.PlatformID}) until {DateTime.UtcNow.Add(duration):O}."); } } internal static bool ShouldDeny(ulong authenticatedPlatformId, ulong claimedPlatformId, out string reason) { if (authenticatedPlatformId == 0L || authenticatedPlatformId != claimedPlatformId) { reason = "Identity verification failed"; return true; } if (SessionBans.ContainsKey(authenticatedPlatformId)) { reason = "Banned for this server session"; return true; } if (BanStore.TryGet(authenticatedPlatformId, out TimedBan ban)) { reason = $"Banned until {ban.ExpiresUtc:u}"; return true; } reason = string.Empty; return false; } internal static bool ClearActions(ulong platformId) { bool num = SessionBans.Remove(platformId); bool flag = BanStore.Remove(platformId); if (num || flag) { MelonLogger.Msg($"Cleared moderation actions for platform ID {platformId}."); } return num || flag; } internal static string SafeName(PlayerID player) { try { string value = player.Metadata.Nickname.GetValue(); string value2 = player.Metadata.Username.GetValue(); string text = ((string.IsNullOrWhiteSpace(value) || value == "?") ? value2 : value); return string.IsNullOrWhiteSpace(text) ? "Unknown" : text; } catch { return "Unknown"; } } private static bool CanModerate(PlayerID player) { if (!NetworkInfo.IsHost || player == null || !player.IsValid || player.IsHost) { MelonLogger.Warning("Better Moderation action ignored: only the host can moderate a valid non-host player."); return false; } return true; } } internal static class NativeFusionMenu { internal static void Postfix(PlayerElement element, PlayerID player) { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Unknown result type (might be due to invalid IL or missing references) //IL_01af: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)element == (Object)null || player == null || !player.IsValid || player.IsMe) { return; } GroupElement val = ((GroupElement)((element.ActionsElement.Pages.Count > 0) ? element.ActionsElement.Pages[0] : element.ActionsElement.AddPage())).AddElement(BuildHeading(player.PlatformID)); if (!NetworkInfo.IsHost || player.IsHost) { MenuChaining.WithInteractability(MenuChaining.WithColor(val.AddElement("Available to the lobby host"), Color.yellow), false); return; } MenuChaining.Do(MenuChaining.WithColor(val.AddElement("Ban for this session"), Color.red), (Action)delegate { ModerationService.SessionBan(player); }); int minutes = 0; int hours = 0; int days = 0; MenuChaining.WithValue(MenuChaining.WithIncrement(MenuChaining.WithLimits(val.AddElement("Minutes"), 0, 59), 1), 0).OnValueChanged = delegate(int value) { minutes = value; }; MenuChaining.WithValue(MenuChaining.WithIncrement(MenuChaining.WithLimits(val.AddElement("Hours"), 0, 23), 1), 0).OnValueChanged = delegate(int value) { hours = value; }; MenuChaining.WithValue(MenuChaining.WithIncrement(MenuChaining.WithLimits(val.AddElement("Days"), 0, 30), 1), 0).OnValueChanged = delegate(int value) { days = value; }; MenuChaining.Do(MenuChaining.WithColor(val.AddElement("Apply timed ban"), Color.red), (Action)delegate { ApplyTimedBan(player, days, hours, minutes); }); MenuChaining.Do(MenuChaining.WithColor(val.AddElement("Clear this player's moderation"), Color.green), (Action)delegate { bool flag = ModerationService.ClearActions(player.PlatformID); Notify(flag ? "Moderation cleared" : "Nothing to clear", flag ? "This player's session and timed bans were removed." : "This player has no BetterModeration bans.", (NotificationType)(flag ? 3 : 0)); }); } private static void ApplyTimedBan(PlayerID player, int days, int hours, int minutes) { TimeSpan timeSpan = TimeSpan.FromDays(days) + TimeSpan.FromHours(hours) + TimeSpan.FromMinutes(minutes); if (timeSpan <= TimeSpan.Zero || timeSpan > TimeSpan.FromDays(30.0)) { Notify("Invalid ban duration", "Choose at least one minute and no more than 30 days.", (NotificationType)2); return; } ModerationService.TimedBan(player, timeSpan); Notify("Timed ban applied", $"{ModerationService.SafeName(player)} was banned until {DateTime.Now.Add(timeSpan):g}.", (NotificationType)3); } private static string BuildHeading(ulong id) { bool flag = ModerationService.IsSessionBanned(id); TimedBan ban; bool flag2 = BanStore.TryGet(id, out ban); if (flag && flag2) { return $"BetterModeration (Session + until {ban.ExpiresUtc.ToLocalTime():g})"; } if (flag) { return "BetterModeration (Session banned)"; } if (flag2) { return $"BetterModeration (Banned until {ban.ExpiresUtc.ToLocalTime():g})"; } return "BetterModeration"; } private static void Notify(string title, string message, NotificationType type) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown Notifier.Send(new Notification { Title = NotificationText.op_Implicit(title), Message = NotificationText.op_Implicit(message), Type = type, PopupLength = 3f, SaveToMenu = true, ShowPopup = true, Tag = "BetterModeration" }); } } }