Merge remote-tracking branch 'origin/claude/add-local-storage-muted-01MeqMQ9SBBLZrfcAEudhk92' into claude/add-local-storage-muted-01MeqMQ9SBBLZrfcAEudhk92
# Conflicts: # Harmony/BotCommandPatch.cs # ModInfo.xml
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -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
|
||||
BIN
CHRANIBotTNG/CHRANIBotTNG.dll
Normal file
BIN
CHRANIBotTNG/CHRANIBotTNG.dll
Normal file
Binary file not shown.
9
CHRANIBotTNG/ModInfo.xml
Normal file
9
CHRANIBotTNG/ModInfo.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xml>
|
||||
<Name value="CHRANIBotTNG" />
|
||||
<DisplayName value="CHRANI-Bot-TNG" />
|
||||
<Description value="Companion Mod for the CHRANI-Bot-TNG" />
|
||||
<Author value="wwevo" />
|
||||
<Version value="2.0.0.0" />
|
||||
<Website value="https://code.notjustfor.me/wwevo/chrani-bot-tng-mod" />
|
||||
</xml>
|
||||
@@ -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<MutedPlayer> MutedPlayers { get; set; } = new List<MutedPlayer>();
|
||||
}
|
||||
|
||||
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<MutedPlayer> 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<AdminUser> admins = new List<AdminUser>();
|
||||
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<AdminUser> 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<NetPackageChat>().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 <player> [duration] [reason] | /bot unmute <player> | /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 <player> [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 <player>");
|
||||
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<NetPackageChat>().Setup(
|
||||
EChatType.Whisper, -1, message, "", false, null));
|
||||
}
|
||||
}
|
||||
374
Harmony/CHRANIBotTNG.cs
Normal file
374
Harmony/CHRANIBotTNG.cs
Normal file
@@ -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<string> MutedPlayers = new HashSet<string>();
|
||||
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<string> 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<string>();
|
||||
}
|
||||
|
||||
public static void SaveMutedPlayers(HashSet<string> 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<string> 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<string> ParseJsonArray(string json)
|
||||
{
|
||||
HashSet<string> result = new HashSet<string>();
|
||||
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<string> adminSteamIDs = new HashSet<string>();
|
||||
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<int> _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<int>();
|
||||
}
|
||||
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<int>();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
11
ModInfo.xml
11
ModInfo.xml
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ModInfo>
|
||||
<ModInfo>
|
||||
<Name value="BotCommandMod"/>
|
||||
<DisplayName value="BotCommandMod"/>
|
||||
<Version value="1.0.0"/>
|
||||
<Description value="Intercepts chat messages starting with /bot and logs them to telnet only"/>
|
||||
<Author value="Claude"/>
|
||||
<Website value=""/>
|
||||
</ModInfo>
|
||||
</ModInfo>
|
||||
8
build.sh
8
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
|
||||
|
||||
Reference in New Issue
Block a user