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<TimedBan> _bans = new List<TimedBan>();
internal static IReadOnlyList<TimedBan> 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<List<TimedBan>>(File.ReadAllText(FilePath)) ?? new List<TimedBan>()) : new List<TimedBan>());
}
catch (Exception ex)
{
MelonLogger.Error("Better Moderation could not read its ban file: " + ex.Message);
_bans = new List<TimedBan>();
}
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<ConnectionRequestData>();
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<NetworkLayer> <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<ulong, string> SessionBans = new Dictionary<ulong, string>();
internal static IReadOnlyDictionary<ulong, string> ActiveSessionBans => new Dictionary<ulong, string>(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<GroupElement>(BuildHeading(player.PlatformID));
if (!NetworkInfo.IsHost || player.IsHost)
{
MenuChaining.WithInteractability<ButtonElement>(MenuChaining.WithColor<ButtonElement>(val.AddElement<ButtonElement>("Available to the lobby host"), Color.yellow), false);
return;
}
MenuChaining.Do(MenuChaining.WithColor<FunctionElement>(val.AddElement<FunctionElement>("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<IntElement>("Minutes"), 0, 59), 1), 0).OnValueChanged = delegate(int value)
{
minutes = value;
};
MenuChaining.WithValue(MenuChaining.WithIncrement(MenuChaining.WithLimits(val.AddElement<IntElement>("Hours"), 0, 23), 1), 0).OnValueChanged = delegate(int value)
{
hours = value;
};
MenuChaining.WithValue(MenuChaining.WithIncrement(MenuChaining.WithLimits(val.AddElement<IntElement>("Days"), 0, 30), 1), 0).OnValueChanged = delegate(int value)
{
days = value;
};
MenuChaining.Do(MenuChaining.WithColor<FunctionElement>(val.AddElement<FunctionElement>("Apply timed ban"), Color.red), (Action)delegate
{
ApplyTimedBan(player, days, hours, minutes);
});
MenuChaining.Do(MenuChaining.WithColor<FunctionElement>(val.AddElement<FunctionElement>("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"
});
}
}
}