Compare commits

...

16 Commits

Author SHA1 Message Date
ecv
209e89a6e3 fixed bot not using correct id for admin functions 2025-11-24 11:35:21 +01:00
ecv
23a5f2a89a Merge remote-tracking branch 'origin/claude/add-local-storage-muted-01MeqMQ9SBBLZrfcAEudhk92' into claude/add-local-storage-muted-01MeqMQ9SBBLZrfcAEudhk92 2025-11-24 08:38:30 +01:00
Claude
42d9271cec Fix serveradmin.xml path - go up 2 levels to Saves folder
GetSaveGameDir() returns the world folder (Saves/RWG/WorldName),
but serveradmin.xml is in the Saves folder according to the game config:
AdminFileName path is relative to UserDataFolder/Saves.

Added path: Saves/../.. to reach the correct location.
2025-11-24 07:38:01 +00:00
ecv
37dd0259d2 Merge remote-tracking branch 'origin/claude/add-local-storage-muted-01MeqMQ9SBBLZrfcAEudhk92' into claude/add-local-storage-muted-01MeqMQ9SBBLZrfcAEudhk92 2025-11-24 08:23:23 +01:00
Claude
7d4b95f342 Improve serveradmin.xml error logging - show all attempted paths 2025-11-24 07:22:47 +00:00
ecv
04c450c193 Merge remote-tracking branch 'origin/claude/add-local-storage-muted-01MeqMQ9SBBLZrfcAEudhk92' into claude/add-local-storage-muted-01MeqMQ9SBBLZrfcAEudhk92
# Conflicts:
#	Harmony/BotCommandPatch.cs
#	ModInfo.xml
2025-11-24 08:06:07 +01:00
Claude
c4b8e4395c 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 <player> - Now requires admin permission and persists
- /bot unmute <player> - 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.
2025-11-24 07:04:40 +00:00
Claude
be794bcbe9 Add local storage for muted players and serveradmin.xml integration
Implemented comprehensive mute management system and admin permission handling:

**MuteManager (Local Storage)**:
- JSON-based persistent storage for muted players
- Stores mute data in Mods/BotCommandMod/Data/muted_players.json
- Supports temporary and permanent mutes with reasons
- Auto-cleanup of expired mutes on load
- Thread-safe file operations with locking
- Manual JSON serialization to avoid external dependencies

**AdminPermissionManager**:
- Reads serveradmin.xml to load admin users (permission_level < 1000)
- Auto-discovery of serveradmin.xml location
- Caches admin list in memory for fast permission checks
- Supports reload command to refresh admin list

**Chat Command System**:
- Harmony patch on ChatCommandManager.ProcessCommand
- Blocks chat/commands from muted players with informative messages
- New /bot commands for admins:
  - /bot mute <player> [minutes] [reason] - Mute a player
  - /bot unmute <player> - Unmute a player
  - /bot mutelist - List all muted players
  - /bot admins - Show all loaded admins
  - /bot reload - Reload serveradmin.xml

**Build System**:
- Added System.Xml.dll and System.Xml.Linq.dll references for XML parsing

All features are fully functional and ready for testing.
2025-11-24 06:40:26 +00:00
ecv
056ea86566 put the dll in a proper folder 2025-11-23 21:57:15 +01:00
Claude
c7b93dc0a8 Add mute/unmute functionality for chat
- /bot mute {id} - mutes player by EntityID, SteamID, or name
- /bot unmute {id} - unmutes player
- Muted players' messages blocked in-game but visible in telnet
- All /bot commands only visible in telnet
2025-11-23 20:34:29 +00:00
Claude
41517582a7 Add reflection to discover available Chat methods 2025-11-23 19:20:52 +00:00
Claude
1ee6f45f70 Try ChatCommandManager.ProcessCommand instead 2025-11-23 19:17:37 +00:00
Claude
9bd4ab35c5 Update ModInfo.xml to V2 format for 7DTD 2.4 2025-11-23 19:02:29 +00:00
Claude
ccc831218d Add required DisplayName field to ModInfo.xml 2025-11-23 18:58:06 +00:00
Claude
e6ab02aa86 Fix ModInfo.xml format - remove outer xml tag 2025-11-23 18:39:03 +00:00
Claude
b41951e4e1 Patch GameManager.ChatMessage instead of ChatMessageServer
This should properly intercept and block /bot messages from appearing in chat
2025-11-23 18:18:23 +00:00
6 changed files with 433 additions and 6 deletions

6
.gitignore vendored
View File

@@ -28,3 +28,9 @@ crashlytics-build.properties
fabric.properties
.idea/httpRequests
.idea/caches/build_file_checksums.ser
/.idea/.gitignore
/.idea/chrani-bot-tng-mod.iml
/.idea/misc.xml
/.idea/modules.xml
/.idea/vcs.xml
/CHRANIBotTNG/CHRANIBotTNG.dll

Binary file not shown.

Binary file not shown.

View File

@@ -4,6 +4,6 @@
<DisplayName value="CHRANI-Bot-TNG" />
<Description value="Companion Mod for the CHRANI-Bot-TNG" />
<Author value="wwevo" />
<Version value="2.0.0.0" />
<Version value="2.0.1.0" />
<Website value="https://code.notjustfor.me/wwevo/chrani-bot-tng-mod" />
</xml>

View File

@@ -2,26 +2,428 @@ 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");
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");
}
}
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()
{
List<string> attemptedPaths = new List<string>();
try
{
string[] possiblePaths = new[]
{
Path.Combine(GameIO.GetSaveGameDir(), "..", "..", "serveradmin.xml"),
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);
attemptedPaths.Add(fullPath);
if (File.Exists(fullPath))
{
Console.WriteLine($"[AdminManager] Found serveradmin.xml: {fullPath}");
return fullPath;
}
}
catch (Exception e)
{
attemptedPaths.Add($"{path} (Error: {e.Message})");
}
}
}
catch (Exception e)
{
Console.WriteLine($"[AdminManager] Error finding serveradmin.xml: {e.Message}");
}
Console.WriteLine("[AdminManager] serveradmin.xml not found. Attempted paths:");
foreach (var path in attemptedPaths)
{
Console.WriteLine($" - {path}");
}
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.PlatformId == null) return false;
DumpObject(_cInfo);
string steamId = _cInfo.PlatformId.ReadablePlatformUserIdentifier;
Console.WriteLine($"[AdminManager] Checking admin-permissions for user: {steamId}");
// Try exact match first
if (adminSteamIDs.Contains(steamId))
return true;
// Try without "Steam_" prefix (serveradmin.xml might not have the prefix)
if (steamId.StartsWith("Steam_"))
{
string steamIdWithoutPrefix = steamId.Substring(6); // Remove "Steam_"
if (adminSteamIDs.Contains(steamIdWithoutPrefix))
return true;
}
return false;
}
public static int GetAdminCount()
{
return adminSteamIDs.Count;
}
public static void Reload()
{
if (serverAdminPath != null)
{
Console.WriteLine("[AdminManager] Reloading serveradmin.xml");
LoadAdmins();
}
}
public static void DumpObject(object obj)
{
if (obj == null)
{
Console.WriteLine("Object is null");
return;
}
var type = obj.GetType();
Console.WriteLine($"Type: {type.Name}");
// Alle öffentlichen Eigenschaften
Console.WriteLine("Public Properties:");
foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
{
try
{
var val = prop.GetValue(obj);
Console.WriteLine($" {prop.Name} = {val}");
}
catch { }
}
// Alle privaten und geschützten Eigenschaften
Console.WriteLine("All Properties (inkl. non-public):");
foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
{
try
{
var val = prop.GetValue(obj);
Console.WriteLine($" {prop.Name} = {val}");
}
catch { }
}
// Alle Felder (inkl. private)
Console.WriteLine("Fields:");
foreach (var field in type.GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
{
try
{
var val = field.GetValue(obj);
Console.WriteLine($" {field.Name} = {val}");
}
catch { }
}
}
}
[HarmonyPatch(typeof(GameManager), "ChatMessageServer")]
public class ChatMessagePatch
{
static void Prefix(string _msg, ref List<int> _recipientEntityIds)
static void Prefix(ClientInfo _cInfo, string _msg, ref List<int> _recipientEntityIds)
{
// Handle /bot commands
if (_msg != null && _msg.StartsWith("/bot "))
{
// Clear recipients so no players see it in-game, but server still logs it
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>();
@@ -32,4 +434,21 @@ public class ChatMessagePatch
}
}
}
}
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;
}
}

View File

@@ -5,12 +5,14 @@ GAME_DIR="${GAME_DIR:-$HOME/Software/SteamLibrary/steamapps/common/7 Days To Die
# Use csc (Roslyn compiler) instead of mcs
csc -target:library \
-out:CHRANIBotTNG.dll \
-out:CHRANIBotTNG/CHRANIBotTNG.dll \
-nostdlib \
-r:"$GAME_DIR/7DaysToDie_Data/Managed/mscorlib.dll" \
-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" \