commit 76175f943fa4125b4f81fb1cd50917ad29451b54 Author: wwevo Date: Tue Apr 14 17:32:30 2026 +0200 Release 2.0.2.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f451f81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +.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 +/.idea/.gitignore +/.idea/chrani-bot-tng-mod.iml +/.idea/misc.xml +/.idea/modules.xml +/.idea/vcs.xml +/CHRANIBotTNG/CHRANIBotTNG.dll diff --git a/CHRANIBotTNG/CHRANIBotTNG.dll b/CHRANIBotTNG/CHRANIBotTNG.dll new file mode 100644 index 0000000..f5b2cc3 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..baa8dbb --- /dev/null +++ b/CHRANIBotTNG/ModInfo.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Harmony/CHRANIBotTNG.cs b/Harmony/CHRANIBotTNG.cs new file mode 100644 index 0000000..fd06229 --- /dev/null +++ b/Harmony/CHRANIBotTNG.cs @@ -0,0 +1,454 @@ +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) + { + Log.Out("[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()); + + Log.Out($"[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"); + } + + public static HashSet LoadMutedPlayers() + { + lock (fileLock) + { + try + { + if (File.Exists(muteFilePath)) + { + string json = File.ReadAllText(muteFilePath); + var players = ParseJsonArray(json); + Log.Out($"[CHRANIBotTNG] 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); + Log.Out($"[CHRANIBotTNG] 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"); + } +} + +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() + { + List attemptedPaths = new List(); + + 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 + { + var permStr = ""; + XDocument doc = XDocument.Load(serverAdminPath); + + adminSteamIDs = doc.Descendants("user") + .Where(u => + { + 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(); + + Log.Out($"[CHRANIBotTNG] Loaded {adminSteamIDs.Count} admins (permission < 1000)"); + foreach (var id in adminSteamIDs) + { + Log.Out($"[CHRANIBotTNG] Admin: {id} ({permStr})"); + } + } + 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; + + Log.Out($"[CHRANIBotTNG] 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) + { + Log.Out($"[CHRANIBotTNG] 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(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)) + { + Log.Out($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to mute - denied"); + } + else + { + string targetId = parts[2]; + CHRANIBotTNG.MutedPlayers.Add(targetId); + MuteStorage.SaveMutedPlayers(CHRANIBotTNG.MutedPlayers); + Log.Out($"[CHRANIBotTNG] Admin {_cInfo?.playerName} muted: {targetId}"); + } + } + else if (parts.Length >= 3 && parts[1].ToLower() == "unmute") + { + // Check if user is admin + if (!AdminManager.IsAdmin(_cInfo)) + { + Log.Out($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to unmute - denied"); + } + else + { + string targetId = parts[2]; + if (CHRANIBotTNG.MutedPlayers.Remove(targetId)) + { + MuteStorage.SaveMutedPlayers(CHRANIBotTNG.MutedPlayers); + Log.Out($"[CHRANIBotTNG] Admin {_cInfo?.playerName} unmuted: {targetId}"); + } + else + { + Log.Out($"[CHRANIBotTNG] Player was not muted: {targetId}"); + } + } + } + else if (parts.Length >= 2 && parts[1].ToLower() == "mutelist") + { + // Show muted players list + if (!AdminManager.IsAdmin(_cInfo)) + { + Log.Out($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to view mutelist - denied"); + } + else + { + Log.Out($"[CHRANIBotTNG] Muted players ({CHRANIBotTNG.MutedPlayers.Count}):"); + foreach (var muted in CHRANIBotTNG.MutedPlayers) + { + Log.Out($"[CHRANIBotTNG] {muted}"); + } + } + } + else if (parts.Length >= 2 && parts[1].ToLower() == "reload") + { + // Reload serveradmin.xml + if (!AdminManager.IsAdmin(_cInfo)) + { + Log.Out($"[CHRANIBotTNG] Non-admin {_cInfo?.playerName} tried to reload - denied"); + } + else + { + AdminManager.Reload(); + Log.Out($"[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; + } +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..9d12bdf --- /dev/null +++ b/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Set your 7DTD installation path +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/CHRANIBotTNG.dll \ + -nostdlib \ + -r:"$GAME_DIR/7DaysToDie_Data/Managed/mscorlib.dll" \ + -r:"$GAME_DIR/7DaysToDie_Data/Managed/LogLibrary.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" \ + Harmony/CHRANIBotTNG.cs diff --git a/release_bot.sh b/release_bot.sh new file mode 100755 index 0000000..9bbf6f4 --- /dev/null +++ b/release_bot.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# release-to-gitea.sh + +WORK_REMOTE="https://github.com/wwevo/chrani-bot-tng-mod.git" +GITEA_REMOTE="https://code.notjustfor.me/wwevo/chrani-bot-tng-mod.git" +RELEASE_TAG=$(grep -oP '(?<=