diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a731290
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+.idea/**/aws.xml
+.idea/**/contentModel.xml
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+.idea/**/gradle.xml
+.idea/**/libraries
+cmake-build-*/
+.idea/**/mongoSettings.xml
+*.iws
+out/
+.idea_modules/
+atlassian-ide-plugin.xml
+.idea/replstate.xml
+.idea/sonarlint/
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+.idea/httpRequests
+.idea/caches/build_file_checksums.ser
diff --git a/CHRANIBotTNG/CHRANIBotTNG.dll b/CHRANIBotTNG/CHRANIBotTNG.dll
new file mode 100644
index 0000000..5eddca0
Binary files /dev/null and b/CHRANIBotTNG/CHRANIBotTNG.dll differ
diff --git a/CHRANIBotTNG/ModInfo.xml b/CHRANIBotTNG/ModInfo.xml
new file mode 100644
index 0000000..68b39b7
--- /dev/null
+++ b/CHRANIBotTNG/ModInfo.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Harmony/BotCommandPatch.cs b/Harmony/BotCommandPatch.cs
deleted file mode 100644
index 3fe543c..0000000
--- a/Harmony/BotCommandPatch.cs
+++ /dev/null
@@ -1,664 +0,0 @@
-using HarmonyLib;
-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");
-
- // Initialize managers
- string modPath = _modInstance.Path;
- MuteManager.Initialize(modPath);
- AdminPermissionManager.Initialize();
-
- // 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/Harmony/CHRANIBotTNG.cs b/Harmony/CHRANIBotTNG.cs
new file mode 100644
index 0000000..d74793b
--- /dev/null
+++ b/Harmony/CHRANIBotTNG.cs
@@ -0,0 +1,374 @@
+using HarmonyLib;
+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 - {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();
+ }
+ }
+}
+
+[HarmonyPatch(typeof(GameManager), "ChatMessageServer")]
+public class ChatMessagePatch
+{
+ static void Prefix(ClientInfo _cInfo, string _msg, ref List _recipientEntityIds)
+ {
+ // Handle /bot commands
+ if (_msg != null && _msg.StartsWith("/bot "))
+ {
+ string[] parts = _msg.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+
+ if (parts.Length >= 3 && parts[1].ToLower() == "mute")
+ {
+ // Check if user is admin
+ if (!AdminManager.IsAdmin(_cInfo))
+ {
+ Console.WriteLine($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to mute - denied");
+ }
+ else
+ {
+ 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");
+ }
+ }
+
+ // Clear recipients so no players see /bot commands in-game
+ if (_recipientEntityIds == null)
+ {
+ _recipientEntityIds = new List();
+ }
+ else
+ {
+ _recipientEntityIds.Clear();
+ }
+ return;
+ }
+
+ // Check if sender is muted
+ if (_cInfo != null && IsPlayerMuted(_cInfo))
+ {
+ Console.WriteLine($"[CHRANIBotTNG] Blocked message from muted player: {_cInfo.playerName}");
+
+ // Clear recipients so no players see the message
+ if (_recipientEntityIds == null)
+ {
+ _recipientEntityIds = new List();
+ }
+ else
+ {
+ _recipientEntityIds.Clear();
+ }
+ }
+ }
+
+ static bool IsPlayerMuted(ClientInfo _cInfo)
+ {
+ // Check EntityID
+ if (CHRANIBotTNG.MutedPlayers.Contains(_cInfo.entityId.ToString()))
+ return true;
+
+ // Check player name
+ if (CHRANIBotTNG.MutedPlayers.Contains(_cInfo.playerName))
+ return true;
+
+ // Check SteamID (InternalId)
+ if (_cInfo.InternalId != null && CHRANIBotTNG.MutedPlayers.Contains(_cInfo.InternalId.ReadablePlatformUserIdentifier))
+ return true;
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/ModInfo.xml b/ModInfo.xml
deleted file mode 100644
index 6b26943..0000000
--- a/ModInfo.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/build.sh b/build.sh
index 3ff7db0..49c3519 100755
--- a/build.sh
+++ b/build.sh
@@ -1,11 +1,11 @@
#!/bin/bash
# Set your 7DTD installation path
-GAME_DIR="${GAME_DIR:-$HOME/.local/share/7DaysToDie}"
+GAME_DIR="${GAME_DIR:-$HOME/Software/SteamLibrary/steamapps/common/7 Days To Die}"
# Use csc (Roslyn compiler) instead of mcs
csc -target:library \
- -out:BotCommandMod.dll \
+ -out:CHRANIBotTNG/CHRANIBotTNG.dll \
-nostdlib \
-r:"$GAME_DIR/7DaysToDie_Data/Managed/mscorlib.dll" \
-r:"$GAME_DIR/7DaysToDie_Data/Managed/netstandard.dll" \
@@ -15,5 +15,5 @@ csc -target:library \
-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" \
- Harmony/BotCommandPatch.cs
+ -r:"/home/ecv/Software/SteamLibrary/steamapps/common/7 Days To Die/Mods/0_TFP_Harmony/0Harmony.dll" \
+ Harmony/CHRANIBotTNG.cs