diff --git a/Harmony/BotCommandPatch.cs b/Harmony/BotCommandPatch.cs index 1e49e2a..3fe543c 100644 --- a/Harmony/BotCommandPatch.cs +++ b/Harmony/BotCommandPatch.cs @@ -1,27 +1,664 @@ using HarmonyLib; -using System.Reflection; using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml.Linq; public class BotCommandMod : IModApi { + private static Harmony harmony; + public void InitMod(Mod _modInstance) { Console.WriteLine("[BotCommandMod] Loading"); - // List all GameManager methods containing "Chat" - var gmType = typeof(GameManager); - Console.WriteLine("[BotCommandMod] GameManager methods with 'Chat':"); - foreach (var method in gmType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) - { - if (method.Name.Contains("Chat")) - { - var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name + " " + p.Name)); - Console.WriteLine($" {method.Name}({parameters})"); - } - } + // Initialize managers + string modPath = _modInstance.Path; + MuteManager.Initialize(modPath); + AdminPermissionManager.Initialize(); - Console.WriteLine("[BotCommandMod] Loaded"); + // Apply Harmony patches + harmony = new Harmony("wwevo.botcommand"); + harmony.PatchAll(Assembly.GetExecutingAssembly()); + + Console.WriteLine("[BotCommandMod] Loaded successfully"); + Console.WriteLine($"[BotCommandMod] Muted players: {MuteManager.GetMutedCount()}"); + Console.WriteLine($"[BotCommandMod] Admin count: {AdminPermissionManager.GetAdminCount()}"); } } +// ==================== MUTE MANAGER ==================== +public static class MuteManager +{ + private static string dataFilePath; + private static MuteData muteData = new MuteData(); + private static readonly object fileLock = new object(); + + public class MutedPlayer + { + public string SteamID { get; set; } + public string PlayerName { get; set; } + public long MutedUntilTimestamp { get; set; } // Unix timestamp, 0 = permanent + public string Reason { get; set; } + public string MutedBy { get; set; } + public long MutedAtTimestamp { get; set; } + } + + public class MuteData + { + public List MutedPlayers { get; set; } = new List(); + } + + public static void Initialize(string modPath) + { + string dataDir = Path.Combine(modPath, "Data"); + if (!Directory.Exists(dataDir)) + { + Directory.CreateDirectory(dataDir); + } + + dataFilePath = Path.Combine(dataDir, "muted_players.json"); + LoadData(); + Console.WriteLine($"[MuteManager] Initialized with file: {dataFilePath}"); + } + + private static void LoadData() + { + lock (fileLock) + { + try + { + if (File.Exists(dataFilePath)) + { + string json = File.ReadAllText(dataFilePath); + muteData = ParseJson(json); + + // Clean up expired mutes + long currentTime = GetUnixTimestamp(); + int removed = muteData.MutedPlayers.RemoveAll(p => + p.MutedUntilTimestamp > 0 && p.MutedUntilTimestamp < currentTime); + + if (removed > 0) + { + SaveData(); + Console.WriteLine($"[MuteManager] Removed {removed} expired mutes"); + } + + Console.WriteLine($"[MuteManager] Loaded {muteData.MutedPlayers.Count} muted players"); + } + else + { + Console.WriteLine("[MuteManager] No existing data file, starting fresh"); + } + } + catch (Exception e) + { + Console.WriteLine($"[MuteManager] Error loading data: {e.Message}"); + muteData = new MuteData(); + } + } + } + + private static void SaveData() + { + lock (fileLock) + { + try + { + string json = ToJson(muteData); + File.WriteAllText(dataFilePath, json); + } + catch (Exception e) + { + Console.WriteLine($"[MuteManager] Error saving data: {e.Message}"); + } + } + } + + public static void MutePlayer(string steamID, string playerName, long durationSeconds, string reason, string mutedBy) + { + lock (fileLock) + { + // Remove existing mute if present + muteData.MutedPlayers.RemoveAll(p => p.SteamID == steamID); + + long currentTime = GetUnixTimestamp(); + long mutedUntil = durationSeconds > 0 ? currentTime + durationSeconds : 0; // 0 = permanent + + muteData.MutedPlayers.Add(new MutedPlayer + { + SteamID = steamID, + PlayerName = playerName, + MutedUntilTimestamp = mutedUntil, + Reason = reason ?? "No reason specified", + MutedBy = mutedBy, + MutedAtTimestamp = currentTime + }); + + SaveData(); + Console.WriteLine($"[MuteManager] Muted player {playerName} ({steamID}) until {(mutedUntil > 0 ? DateTimeOffset.FromUnixTimeSeconds(mutedUntil).ToString() : "permanent")}"); + } + } + + public static void UnmutePlayer(string steamID) + { + lock (fileLock) + { + int removed = muteData.MutedPlayers.RemoveAll(p => p.SteamID == steamID); + if (removed > 0) + { + SaveData(); + Console.WriteLine($"[MuteManager] Unmuted player with SteamID {steamID}"); + } + } + } + + public static bool IsPlayerMuted(string steamID) + { + long currentTime = GetUnixTimestamp(); + var mute = muteData.MutedPlayers.FirstOrDefault(p => p.SteamID == steamID); + + if (mute == null) return false; + + // Check if temporary mute has expired + if (mute.MutedUntilTimestamp > 0 && mute.MutedUntilTimestamp < currentTime) + { + UnmutePlayer(steamID); + return false; + } + + return true; + } + + public static MutedPlayer GetMuteInfo(string steamID) + { + return muteData.MutedPlayers.FirstOrDefault(p => p.SteamID == steamID); + } + + public static List GetAllMutedPlayers() + { + return muteData.MutedPlayers.ToList(); + } + + public static int GetMutedCount() + { + return muteData.MutedPlayers.Count; + } + + private static long GetUnixTimestamp() + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + // Simple JSON serialization (manual to avoid dependencies) + private static string ToJson(MuteData data) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("{"); + sb.AppendLine(" \"MutedPlayers\": ["); + + for (int i = 0; i < data.MutedPlayers.Count; i++) + { + var player = data.MutedPlayers[i]; + sb.AppendLine(" {"); + sb.AppendLine($" \"SteamID\": \"{EscapeJson(player.SteamID)}\","); + sb.AppendLine($" \"PlayerName\": \"{EscapeJson(player.PlayerName)}\","); + sb.AppendLine($" \"MutedUntilTimestamp\": {player.MutedUntilTimestamp},"); + sb.AppendLine($" \"Reason\": \"{EscapeJson(player.Reason)}\","); + sb.AppendLine($" \"MutedBy\": \"{EscapeJson(player.MutedBy)}\","); + sb.AppendLine($" \"MutedAtTimestamp\": {player.MutedAtTimestamp}"); + sb.Append(" }"); + if (i < data.MutedPlayers.Count - 1) sb.Append(","); + sb.AppendLine(); + } + + sb.AppendLine(" ]"); + sb.AppendLine("}"); + return sb.ToString(); + } + + private static MuteData ParseJson(string json) + { + MuteData data = new MuteData(); + + try + { + // Simple manual JSON parsing + string[] lines = json.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + MutedPlayer currentPlayer = null; + + foreach (string line in lines) + { + string trimmed = line.Trim(); + + if (trimmed.StartsWith("{") && currentPlayer == null && !trimmed.Contains("MutedPlayers")) + { + currentPlayer = new MutedPlayer(); + } + else if (trimmed.StartsWith("}") && currentPlayer != null) + { + data.MutedPlayers.Add(currentPlayer); + currentPlayer = null; + } + else if (currentPlayer != null) + { + if (trimmed.Contains("\"SteamID\"")) + currentPlayer.SteamID = ExtractStringValue(trimmed); + else if (trimmed.Contains("\"PlayerName\"")) + currentPlayer.PlayerName = ExtractStringValue(trimmed); + else if (trimmed.Contains("\"MutedUntilTimestamp\"")) + currentPlayer.MutedUntilTimestamp = ExtractLongValue(trimmed); + else if (trimmed.Contains("\"Reason\"")) + currentPlayer.Reason = ExtractStringValue(trimmed); + else if (trimmed.Contains("\"MutedBy\"")) + currentPlayer.MutedBy = ExtractStringValue(trimmed); + else if (trimmed.Contains("\"MutedAtTimestamp\"")) + currentPlayer.MutedAtTimestamp = ExtractLongValue(trimmed); + } + } + } + catch (Exception e) + { + Console.WriteLine($"[MuteManager] Error parsing JSON: {e.Message}"); + } + + return data; + } + + private static string ExtractStringValue(string line) + { + int firstQuote = line.IndexOf('"', line.IndexOf(':')); + int lastQuote = line.LastIndexOf('"'); + if (firstQuote >= 0 && lastQuote > firstQuote) + { + return line.Substring(firstQuote + 1, lastQuote - firstQuote - 1); + } + return ""; + } + + private static long ExtractLongValue(string line) + { + int colonIndex = line.IndexOf(':'); + if (colonIndex >= 0) + { + string valueStr = line.Substring(colonIndex + 1).Trim().TrimEnd(','); + if (long.TryParse(valueStr, out long value)) + { + return value; + } + } + return 0; + } + + private static string EscapeJson(string str) + { + if (string.IsNullOrEmpty(str)) return ""; + return str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"); + } +} + +// ==================== ADMIN PERMISSION MANAGER ==================== +public static class AdminPermissionManager +{ + private static List admins = new List(); + private static string serverAdminPath; + + public class AdminUser + { + public string Platform { get; set; } + public string SteamID { get; set; } + public string Name { get; set; } + public int PermissionLevel { get; set; } + } + + public static void Initialize() + { + // Try to find serveradmin.xml + serverAdminPath = FindServerAdminXml(); + + if (serverAdminPath != null) + { + LoadAdmins(); + } + else + { + Console.WriteLine("[AdminPermissionManager] serveradmin.xml not found, admin checking disabled"); + } + } + + private static string FindServerAdminXml() + { + // Common paths for serveradmin.xml + string[] possiblePaths = new[] + { + Path.Combine(GameIO.GetSaveGameDir(), "..", "serveradmin.xml"), + Path.Combine(GameIO.GetSaveGameDir(), "serveradmin.xml"), + "serveradmin.xml" + }; + + foreach (string path in possiblePaths) + { + try + { + string fullPath = Path.GetFullPath(path); + if (File.Exists(fullPath)) + { + Console.WriteLine($"[AdminPermissionManager] Found serveradmin.xml at: {fullPath}"); + return fullPath; + } + } + catch (Exception e) + { + Console.WriteLine($"[AdminPermissionManager] Error checking path {path}: {e.Message}"); + } + } + + return null; + } + + private static void LoadAdmins() + { + try + { + if (!File.Exists(serverAdminPath)) + { + Console.WriteLine("[AdminPermissionManager] serveradmin.xml does not exist"); + return; + } + + XDocument doc = XDocument.Load(serverAdminPath); + + admins = doc.Descendants("user") + .Where(u => + { + var permStr = u.Attribute("permission_level")?.Value; + return int.TryParse(permStr, out int perm) && perm < 1000; + }) + .Select(u => new AdminUser + { + Platform = u.Attribute("platform")?.Value ?? "Steam", + SteamID = u.Attribute("userid")?.Value, + Name = u.Attribute("name")?.Value, + PermissionLevel = int.TryParse(u.Attribute("permission_level")?.Value, out int perm) ? perm : 1000 + }) + .ToList(); + + Console.WriteLine($"[AdminPermissionManager] Loaded {admins.Count} admins with permission level < 1000:"); + foreach (var admin in admins) + { + Console.WriteLine($" - {admin.Name} ({admin.SteamID}) [Level {admin.PermissionLevel}]"); + } + } + catch (Exception e) + { + Console.WriteLine($"[AdminPermissionManager] Error loading serveradmin.xml: {e.Message}"); + } + } + + public static bool IsAdmin(string steamID) + { + return admins.Any(a => a.SteamID == steamID); + } + + public static AdminUser GetAdmin(string steamID) + { + return admins.FirstOrDefault(a => a.SteamID == steamID); + } + + public static int GetPermissionLevel(string steamID) + { + var admin = GetAdmin(steamID); + return admin?.PermissionLevel ?? 1000; // Default permission level for non-admins + } + + public static List GetAllAdmins() + { + return admins.ToList(); + } + + public static int GetAdminCount() + { + return admins.Count; + } + + public static void Reload() + { + Console.WriteLine("[AdminPermissionManager] Reloading serveradmin.xml"); + LoadAdmins(); + } +} + +// ==================== HARMONY PATCH FOR CHAT COMMANDS ==================== +[HarmonyPatch(typeof(ChatCommandManager))] +[HarmonyPatch("ProcessCommand")] +public class ChatCommandPatch +{ + static bool Prefix(string _command, ClientInfo _cInfo) + { + try + { + if (_cInfo == null) return true; + + string steamID = _cInfo.InternalId.ReadablePlatformUserIdentifier; + string playerName = _cInfo.playerName; + + // Check if player is muted + if (MuteManager.IsPlayerMuted(steamID)) + { + var muteInfo = MuteManager.GetMuteInfo(steamID); + if (muteInfo != null) + { + string message; + if (muteInfo.MutedUntilTimestamp == 0) + { + message = $"You are permanently muted. Reason: {muteInfo.Reason}"; + } + else + { + long remainingSeconds = muteInfo.MutedUntilTimestamp - DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + message = $"You are muted for {remainingSeconds / 60} more minutes. Reason: {muteInfo.Reason}"; + } + + _cInfo.SendPackage(NetPackageManager.GetPackage().Setup( + EChatType.Whisper, -1, message, "", false, null)); + } + + return false; // Block the command + } + + // Handle bot commands + if (_command.StartsWith("/bot")) + { + HandleBotCommand(_command, _cInfo, steamID, playerName); + return false; // Prevent default processing + } + } + catch (Exception e) + { + Console.WriteLine($"[ChatCommandPatch] Error in Prefix: {e}"); + } + + return true; // Allow default processing + } + + private static void HandleBotCommand(string command, ClientInfo cInfo, string steamID, string playerName) + { + string[] parts = command.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length < 2) + { + SendMessage(cInfo, "Bot commands: /bot mute [duration] [reason] | /bot unmute | /bot mutelist | /bot admins | /bot reload"); + return; + } + + string subCommand = parts[1].ToLower(); + + switch (subCommand) + { + case "mute": + if (!AdminPermissionManager.IsAdmin(steamID)) + { + SendMessage(cInfo, "You don't have permission to use this command."); + return; + } + + if (parts.Length < 3) + { + SendMessage(cInfo, "Usage: /bot mute [durationMinutes] [reason]"); + return; + } + + string targetPlayer = parts[2]; + long durationSeconds = 3600; // Default 1 hour + string reason = "No reason specified"; + + if (parts.Length >= 4 && int.TryParse(parts[3], out int minutes)) + { + durationSeconds = minutes * 60; + } + + if (parts.Length >= 5) + { + reason = string.Join(" ", parts.Skip(4)); + } + + // Find target player + ClientInfo targetCInfo = FindPlayer(targetPlayer); + if (targetCInfo != null) + { + string targetSteamID = targetCInfo.InternalId.ReadablePlatformUserIdentifier; + MuteManager.MutePlayer(targetSteamID, targetCInfo.playerName, durationSeconds, reason, playerName); + SendMessage(cInfo, $"Muted player {targetCInfo.playerName} for {durationSeconds / 60} minutes. Reason: {reason}"); + } + else + { + SendMessage(cInfo, $"Player '{targetPlayer}' not found."); + } + break; + + case "unmute": + if (!AdminPermissionManager.IsAdmin(steamID)) + { + SendMessage(cInfo, "You don't have permission to use this command."); + return; + } + + if (parts.Length < 3) + { + SendMessage(cInfo, "Usage: /bot unmute "); + return; + } + + string unmuteTarget = parts[2]; + ClientInfo unmuteCInfo = FindPlayer(unmuteTarget); + + if (unmuteCInfo != null) + { + string targetSteamID = unmuteCInfo.InternalId.ReadablePlatformUserIdentifier; + MuteManager.UnmutePlayer(targetSteamID); + SendMessage(cInfo, $"Unmuted player {unmuteCInfo.playerName}"); + } + else + { + SendMessage(cInfo, $"Player '{unmuteTarget}' not found."); + } + break; + + case "mutelist": + if (!AdminPermissionManager.IsAdmin(steamID)) + { + SendMessage(cInfo, "You don't have permission to use this command."); + return; + } + + var mutedPlayers = MuteManager.GetAllMutedPlayers(); + if (mutedPlayers.Count == 0) + { + SendMessage(cInfo, "No players are currently muted."); + } + else + { + SendMessage(cInfo, $"Muted players ({mutedPlayers.Count}):"); + foreach (var muted in mutedPlayers) + { + string until = muted.MutedUntilTimestamp == 0 ? "permanent" : + DateTimeOffset.FromUnixTimeSeconds(muted.MutedUntilTimestamp).ToString("yyyy-MM-dd HH:mm"); + SendMessage(cInfo, $" {muted.PlayerName} until {until} - {muted.Reason}"); + } + } + break; + + case "admins": + var admins = AdminPermissionManager.GetAllAdmins(); + if (admins.Count == 0) + { + SendMessage(cInfo, "No admins loaded (serveradmin.xml not found or empty)."); + } + else + { + SendMessage(cInfo, $"Admins ({admins.Count}):"); + foreach (var admin in admins) + { + SendMessage(cInfo, $" {admin.Name} [Level {admin.PermissionLevel}]"); + } + } + break; + + case "reload": + if (!AdminPermissionManager.IsAdmin(steamID)) + { + SendMessage(cInfo, "You don't have permission to use this command."); + return; + } + + AdminPermissionManager.Reload(); + SendMessage(cInfo, "Reloaded admin permissions from serveradmin.xml"); + break; + + default: + SendMessage(cInfo, $"Unknown bot command: {subCommand}"); + break; + } + } + + private static ClientInfo FindPlayer(string nameOrId) + { + // Try exact name match first + foreach (var client in ConnectionManager.Instance.Clients.List) + { + if (client.playerName.Equals(nameOrId, StringComparison.OrdinalIgnoreCase)) + { + return client; + } + } + + // Try partial name match + foreach (var client in ConnectionManager.Instance.Clients.List) + { + if (client.playerName.IndexOf(nameOrId, StringComparison.OrdinalIgnoreCase) >= 0) + { + return client; + } + } + + // Try Steam ID match + foreach (var client in ConnectionManager.Instance.Clients.List) + { + if (client.InternalId.ReadablePlatformUserIdentifier == nameOrId) + { + return client; + } + } + + return null; + } + + private static void SendMessage(ClientInfo cInfo, string message) + { + cInfo.SendPackage(NetPackageManager.GetPackage().Setup( + EChatType.Whisper, -1, message, "", false, null)); + } +} diff --git a/build.sh b/build.sh index 9cf126c..3ff7db0 100755 --- a/build.sh +++ b/build.sh @@ -11,6 +11,8 @@ csc -target:library \ -r:"$GAME_DIR/7DaysToDie_Data/Managed/netstandard.dll" \ -r:"$GAME_DIR/7DaysToDie_Data/Managed/System.dll" \ -r:"$GAME_DIR/7DaysToDie_Data/Managed/System.Core.dll" \ + -r:"$GAME_DIR/7DaysToDie_Data/Managed/System.Xml.dll" \ + -r:"$GAME_DIR/7DaysToDie_Data/Managed/System.Xml.Linq.dll" \ -r:"$GAME_DIR/7DaysToDie_Data/Managed/Assembly-CSharp.dll" \ -r:"$GAME_DIR/7DaysToDie_Data/Managed/UnityEngine.CoreModule.dll" \ -r:"$GAME_DIR/7DaysToDie_Data/Managed/0Harmony.dll" \