From c4b8e4395c11cc7ff249ff2b443dcdba47092651 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Nov 2025 07:04:40 +0000 Subject: [PATCH] Add local storage for muted players and serveradmin.xml integration **MuteStorage (JSON Persistence)**: - Added persistent storage in CHRANIBotTNG/Data/muted_players.json - Auto-loads muted players on mod init - Auto-saves on mute/unmute operations - Thread-safe file operations - Simple JSON serialization without external dependencies **AdminManager (serveradmin.xml Integration)**: - Reads serveradmin.xml to load admin SteamIDs (permission_level < 1000) - Auto-discovers serveradmin.xml location in common paths - Caches admin list in memory for fast permission checks - Supports /bot reload command to refresh admin list **Updated Commands (Admin-only)**: - /bot mute - Now requires admin permission and persists - /bot unmute - Now requires admin permission and persists - /bot mutelist - List all muted players (admin-only) - /bot reload - Reload serveradmin.xml (admin-only) **Build System**: - Added System.Xml.dll and System.Xml.Linq.dll references All features integrate seamlessly with existing codebase structure. --- Harmony/CHRANIBotTNG.cs | 302 ++++++++++++++++++++++++++++++++++++++-- build.sh | 2 + 2 files changed, 293 insertions(+), 11 deletions(-) diff --git a/Harmony/CHRANIBotTNG.cs b/Harmony/CHRANIBotTNG.cs index 65e717b..d74793b 100644 --- a/Harmony/CHRANIBotTNG.cs +++ b/Harmony/CHRANIBotTNG.cs @@ -3,17 +3,250 @@ using System.Reflection; using System; using System.Collections.Generic; using System.Linq; +using System.IO; +using System.Text; +using System.Xml.Linq; public class CHRANIBotTNG : IModApi { public static HashSet MutedPlayers = new HashSet(); + private static string modPath; public void InitMod(Mod _modInstance) { Console.WriteLine("[CHRANIBotTNG] Loading"); + + modPath = _modInstance.Path; + + // Load persisted mute list + MuteStorage.Initialize(modPath); + MutedPlayers = MuteStorage.LoadMutedPlayers(); + + // Load admin list from serveradmin.xml + AdminManager.Initialize(); + var harmony = new Harmony("com.chranibottng.mod"); harmony.PatchAll(Assembly.GetExecutingAssembly()); - Console.WriteLine("[CHRANIBotTNG] Loaded"); + + Console.WriteLine($"[CHRANIBotTNG] Loaded - {MutedPlayers.Count} muted players, {AdminManager.GetAdminCount()} admins"); + } +} + +// ==================== MUTE STORAGE (JSON Persistence) ==================== +public static class MuteStorage +{ + private static string muteFilePath; + private static readonly object fileLock = new object(); + + public static void Initialize(string modPath) + { + string dataDir = Path.Combine(modPath, "Data"); + if (!Directory.Exists(dataDir)) + { + Directory.CreateDirectory(dataDir); + } + muteFilePath = Path.Combine(dataDir, "muted_players.json"); + Console.WriteLine($"[MuteStorage] Initialized: {muteFilePath}"); + } + + public static HashSet LoadMutedPlayers() + { + lock (fileLock) + { + try + { + if (File.Exists(muteFilePath)) + { + string json = File.ReadAllText(muteFilePath); + var players = ParseJsonArray(json); + Console.WriteLine($"[MuteStorage] Loaded {players.Count} muted players"); + return players; + } + } + catch (Exception e) + { + Console.WriteLine($"[MuteStorage] Error loading: {e.Message}"); + } + } + return new HashSet(); + } + + public static void SaveMutedPlayers(HashSet players) + { + lock (fileLock) + { + try + { + string json = ToJsonArray(players); + File.WriteAllText(muteFilePath, json); + Console.WriteLine($"[MuteStorage] Saved {players.Count} muted players"); + } + catch (Exception e) + { + Console.WriteLine($"[MuteStorage] Error saving: {e.Message}"); + } + } + } + + private static string ToJsonArray(HashSet players) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("{"); + sb.AppendLine(" \"MutedPlayers\": ["); + + var list = players.ToList(); + for (int i = 0; i < list.Count; i++) + { + sb.Append($" \"{EscapeJson(list[i])}\""); + if (i < list.Count - 1) sb.Append(","); + sb.AppendLine(); + } + + sb.AppendLine(" ]"); + sb.AppendLine("}"); + return sb.ToString(); + } + + private static HashSet ParseJsonArray(string json) + { + HashSet result = new HashSet(); + try + { + // Simple parser: extract strings between quotes in the array + bool inArray = false; + for (int i = 0; i < json.Length; i++) + { + if (json[i] == '[') inArray = true; + if (json[i] == ']') break; + + if (inArray && json[i] == '"') + { + int start = i + 1; + int end = json.IndexOf('"', start); + if (end > start) + { + string value = json.Substring(start, end - start); + if (!string.IsNullOrWhiteSpace(value)) + { + result.Add(value); + } + i = end; + } + } + } + } + catch (Exception e) + { + Console.WriteLine($"[MuteStorage] Parse error: {e.Message}"); + } + return result; + } + + private static string EscapeJson(string str) + { + if (string.IsNullOrEmpty(str)) return ""; + return str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"); + } +} + +// ==================== ADMIN MANAGER (serveradmin.xml) ==================== +public static class AdminManager +{ + private static HashSet adminSteamIDs = new HashSet(); + private static string serverAdminPath; + + public static void Initialize() + { + serverAdminPath = FindServerAdminXml(); + if (serverAdminPath != null) + { + LoadAdmins(); + } + else + { + Console.WriteLine("[AdminManager] serveradmin.xml not found - admin checks disabled"); + } + } + + private static string FindServerAdminXml() + { + try + { + // Try common locations + 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($"[AdminManager] Found serveradmin.xml: {fullPath}"); + return fullPath; + } + } + catch { } + } + } + catch (Exception e) + { + Console.WriteLine($"[AdminManager] Error finding serveradmin.xml: {e.Message}"); + } + return null; + } + + private static void LoadAdmins() + { + try + { + XDocument doc = XDocument.Load(serverAdminPath); + + adminSteamIDs = doc.Descendants("user") + .Where(u => + { + var permStr = u.Attribute("permission_level")?.Value; + return int.TryParse(permStr, out int perm) && perm < 1000; + }) + .Select(u => u.Attribute("userid")?.Value) + .Where(id => !string.IsNullOrEmpty(id)) + .ToHashSet(); + + Console.WriteLine($"[AdminManager] Loaded {adminSteamIDs.Count} admins (permission < 1000)"); + foreach (var id in adminSteamIDs) + { + Console.WriteLine($" Admin: {id}"); + } + } + catch (Exception e) + { + Console.WriteLine($"[AdminManager] Error loading serveradmin.xml: {e.Message}"); + } + } + + public static bool IsAdmin(ClientInfo _cInfo) + { + if (_cInfo == null || _cInfo.InternalId == null) return false; + return adminSteamIDs.Contains(_cInfo.InternalId.ReadablePlatformUserIdentifier); + } + + public static int GetAdminCount() + { + return adminSteamIDs.Count; + } + + public static void Reload() + { + if (serverAdminPath != null) + { + Console.WriteLine("[AdminManager] Reloading serveradmin.xml"); + LoadAdmins(); + } } } @@ -29,20 +262,67 @@ public class ChatMessagePatch if (parts.Length >= 3 && parts[1].ToLower() == "mute") { - string targetId = parts[2]; - CHRANIBotTNG.MutedPlayers.Add(targetId); - Console.WriteLine($"[CHRANIBotTNG] Muted player: {targetId}"); - } - else if (parts.Length >= 3 && parts[1].ToLower() == "unmute") - { - string targetId = parts[2]; - if (CHRANIBotTNG.MutedPlayers.Remove(targetId)) + // Check if user is admin + if (!AdminManager.IsAdmin(_cInfo)) { - Console.WriteLine($"[CHRANIBotTNG] Unmuted player: {targetId}"); + Console.WriteLine($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to mute - denied"); } else { - Console.WriteLine($"[CHRANIBotTNG] Player was not muted: {targetId}"); + string targetId = parts[2]; + CHRANIBotTNG.MutedPlayers.Add(targetId); + MuteStorage.SaveMutedPlayers(CHRANIBotTNG.MutedPlayers); + Console.WriteLine($"[CHRANIBotTNG] Admin {_cInfo?.playerName} muted: {targetId}"); + } + } + else if (parts.Length >= 3 && parts[1].ToLower() == "unmute") + { + // Check if user is admin + if (!AdminManager.IsAdmin(_cInfo)) + { + Console.WriteLine($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to unmute - denied"); + } + else + { + string targetId = parts[2]; + if (CHRANIBotTNG.MutedPlayers.Remove(targetId)) + { + MuteStorage.SaveMutedPlayers(CHRANIBotTNG.MutedPlayers); + Console.WriteLine($"[CHRANIBotTNG] Admin {_cInfo?.playerName} unmuted: {targetId}"); + } + else + { + Console.WriteLine($"[CHRANIBotTNG] Player was not muted: {targetId}"); + } + } + } + else if (parts.Length >= 2 && parts[1].ToLower() == "mutelist") + { + // Show muted players list + if (!AdminManager.IsAdmin(_cInfo)) + { + Console.WriteLine($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to view mutelist - denied"); + } + else + { + Console.WriteLine($"[CHRANIBotTNG] Muted players ({CHRANIBotTNG.MutedPlayers.Count}):"); + foreach (var muted in CHRANIBotTNG.MutedPlayers) + { + Console.WriteLine($" - {muted}"); + } + } + } + else if (parts.Length >= 2 && parts[1].ToLower() == "reload") + { + // Reload serveradmin.xml + if (!AdminManager.IsAdmin(_cInfo)) + { + Console.WriteLine($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to reload - denied"); + } + else + { + AdminManager.Reload(); + Console.WriteLine($"[CHRANIBotTNG] Admin {_cInfo?.playerName} reloaded serveradmin.xml"); } } diff --git a/build.sh b/build.sh index cef7d2d..49c3519 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:"/home/ecv/Software/SteamLibrary/steamapps/common/7 Days To Die/Mods/0_TFP_Harmony/0Harmony.dll" \