Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimRcon v1.6.2
ValheimRcon.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using LukeSkywalker.IPNetwork; using Splatform; using UnityEngine; using UnityEngine.Pool; using UnityEngine.Profiling; using ValheimRcon.Commands; using ValheimRcon.Commands.Modification; using ValheimRcon.Commands.Search; using ValheimRcon.Core; using ValheimRcon.ZDOInfo; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Valheim Rcon")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Valheim Rcon")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("43d6353e-ae3d-424e-8d9d-b274ab342a3e")] [assembly: AssemblyFileVersion("1.6.2")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.6.2.0")] [module: UnverifiableCode] internal class Helper { public static void WatchConfigFileChanges(ConfigFile config, Action onChanged = null) { WatchFileChanges(config.ConfigFilePath, (Action)config.Reload); config.SettingChanged += delegate { onChanged?.Invoke(); }; } public static void WatchFileChanges(string path, Action onChanged) { FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(); string directoryName = Path.GetDirectoryName(path); string fileName = Path.GetFileName(path); fileSystemWatcher.Path = directoryName; fileSystemWatcher.Filter = fileName; fileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite; fileSystemWatcher.Changed += delegate { onChanged?.Invoke(); }; fileSystemWatcher.Deleted += delegate { onChanged?.Invoke(); }; fileSystemWatcher.Created += delegate { onChanged?.Invoke(); }; fileSystemWatcher.Renamed += delegate { onChanged?.Invoke(); }; fileSystemWatcher.EnableRaisingEvents = true; } } internal static class ThreadingUtil { private class DisposableThread : IDisposable { private Thread _thread; internal DisposableThread(Thread thread) { _thread = thread; } public void Dispose() { _thread.Abort(); } } private class MainThreadDispatcher : MonoBehaviour { private static MainThreadDispatcher _instance; private ConcurrentQueue<Action> _queue = new ConcurrentQueue<Action>(); private ConcurrentQueue<IEnumerator> _coroutinesQueue = new ConcurrentQueue<IEnumerator>(); public static MainThreadDispatcher GetInstante() { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown if ((Object)(object)_instance == (Object)null) { GameObject val = new GameObject("MainThreadDispatcher", new Type[1] { typeof(MainThreadDispatcher) }); Object.DontDestroyOnLoad((Object)val); _instance = val.GetComponent<MainThreadDispatcher>(); } return _instance; } public void AddAction(Action action) { _queue.Enqueue(action); } public void AddCoroutine(IEnumerator coroutine) { _coroutinesQueue.Enqueue(coroutine); } private void Update() { Action result; while (_queue.Count > 0 && _queue.TryDequeue(out result)) { result?.Invoke(); } IEnumerator result2; while (_coroutinesQueue.Count > 0 && _coroutinesQueue.TryDequeue(out result2)) { ((MonoBehaviour)this).StartCoroutine(result2); } } } internal static IDisposable RunPeriodical(Action action, int periodMilliseconds) { return new Timer(delegate { action?.Invoke(); }, null, 0, periodMilliseconds); } internal static IDisposable RunPeriodicalInSingleThread(Action action, int periodMilliseconds) { Thread thread = new Thread((ParameterizedThreadStart)delegate { while (true) { action?.Invoke(); Thread.Sleep(periodMilliseconds); } }); thread.Start(); return new DisposableThread(thread); } internal static void RunInMainThread(Action action) { MainThreadDispatcher.GetInstante().AddAction(action); } internal static void RunCoroutine(IEnumerator coroutine) { MainThreadDispatcher.GetInstante().AddCoroutine(coroutine); } internal static void RunDelayed(float delay, Action action) { MainThreadDispatcher.GetInstante().AddCoroutine(DelayedActionCoroutine(delay, action)); } internal static IDisposable RunThread(Action action) { Thread thread = new Thread(action.Invoke); thread.Start(); return new DisposableThread(thread); } internal static IEnumerator DelayedActionCoroutine(float delay, Action action) { yield return (object)new WaitForSeconds(delay); action?.Invoke(); } } namespace ValheimRcon { public interface IRconCommand { string Command { get; } string Description { get; } Task<CommandResult> HandleCommandAsync(CommandArgs args); } internal static class Discord { private static class FormUpload { internal class FileParameter { public byte[] File; public string FileName; public string ContentType; public FileParameter(byte[] file, string filename, string contenttype) { File = file; FileName = filename; ContentType = contenttype; } } private static readonly Encoding Encoding = Encoding.UTF8; public static HttpWebResponse MultipartFormDataPost(string postUrl, Dictionary<string, object> postParameters) { string text = $"----------{Guid.NewGuid():N}"; string contentType = "multipart/form-data; boundary=" + text; byte[] multipartFormData = GetMultipartFormData(postParameters, text); return PostForm(postUrl, contentType, multipartFormData); } private static HttpWebResponse PostForm(string postUrl, string contentType, byte[] formData) { if (!(WebRequest.Create(postUrl) is HttpWebRequest httpWebRequest)) { throw new ArgumentException("request is not a http request"); } httpWebRequest.Method = "POST"; httpWebRequest.ContentType = contentType; httpWebRequest.CookieContainer = new CookieContainer(); httpWebRequest.ContentLength = formData.Length; using (Stream stream = httpWebRequest.GetRequestStream()) { stream.Write(formData, 0, formData.Length); stream.Close(); } return httpWebRequest.GetResponse() as HttpWebResponse; } private static byte[] GetMultipartFormData(Dictionary<string, object> postParameters, string boundary) { MemoryStream memoryStream = new MemoryStream(); bool flag = false; foreach (KeyValuePair<string, object> postParameter in postParameters) { if (flag) { memoryStream.Write(Encoding.GetBytes("\r\n"), 0, Encoding.GetByteCount("\r\n")); } flag = true; if (postParameter.Value is FileParameter) { FileParameter fileParameter = (FileParameter)postParameter.Value; string s = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n", boundary, postParameter.Key, fileParameter.FileName ?? postParameter.Key, fileParameter.ContentType ?? "application/octet-stream"); memoryStream.Write(Encoding.GetBytes(s), 0, Encoding.GetByteCount(s)); memoryStream.Write(fileParameter.File, 0, fileParameter.File.Length); } else { string s2 = $"--{boundary}\r\nContent-Disposition: form-data; name=\"{postParameter.Key}\"\r\n\r\n{postParameter.Value}"; memoryStream.Write(Encoding.GetBytes(s2), 0, Encoding.GetByteCount(s2)); } } string s3 = "\r\n--" + boundary + "--\r\n"; memoryStream.Write(Encoding.GetBytes(s3), 0, Encoding.GetByteCount(s3)); memoryStream.Position = 0L; byte[] array = new byte[memoryStream.Length]; memoryStream.Read(array, 0, array.Length); memoryStream.Close(); return array; } } internal static string Send(string mssgBody, string userName, string webhook) { Dictionary<string, object> postParameters = new Dictionary<string, object> { { "username", userName }, { "content", mssgBody } }; HttpWebResponse httpWebResponse = FormUpload.MultipartFormDataPost(webhook, postParameters); StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream()); string result = streamReader.ReadToEnd(); streamReader.Close(); httpWebResponse.Close(); streamReader.Dispose(); httpWebResponse.Dispose(); return result; } internal static string SendFile(string mssgBody, string filename, string fileformat, string filepath, string userName, string webhook) { FileStream fileStream = new FileStream(filepath, FileMode.Open, FileAccess.Read); byte[] array = new byte[fileStream.Length]; fileStream.Read(array, 0, array.Length); fileStream.Close(); Dictionary<string, object> postParameters = new Dictionary<string, object> { { "filename", filename }, { "fileformat", fileformat }, { "file", new FormUpload.FileParameter(array, filename, "application/msexcel") }, { "username", userName }, { "content", mssgBody } }; HttpWebResponse httpWebResponse = FormUpload.MultipartFormDataPost(webhook, postParameters); string result = new StreamReader(httpWebResponse.GetResponseStream()).ReadToEnd(); httpWebResponse.Close(); fileStream.Dispose(); httpWebResponse.Dispose(); return result; } } internal class DiscordService : IDisposable { private struct Message { public string url; public string text; public string filePath; } private const string Name = "RCON"; private readonly IDisposable _thread; private readonly ConcurrentQueue<Message> _queue = new ConcurrentQueue<Message>(); public DiscordService() { _thread = ThreadingUtil.RunPeriodicalInSingleThread(SendQueuedMessage, 333); } public void SendResult(string url, string text, string filePath) { if (!string.IsNullOrEmpty(url)) { _queue.Enqueue(new Message { url = url, filePath = filePath, text = text }); } } private void SendQueuedMessage() { if (!_queue.TryDequeue(out var result) || string.IsNullOrEmpty(result.url)) { return; } try { string filePath = result.filePath; if (string.IsNullOrEmpty(filePath)) { Discord.Send(result.text, "RCON", result.url); } else { string fileName = Path.GetFileName(filePath); string extension = Path.GetExtension(filePath); Discord.SendFile(result.text, fileName, extension, filePath, "RCON", result.url); } Log.Debug($"Sent to discord (symbols:{result.text.Length})"); } catch (Exception arg) { Log.Error($"Cannot send to discord (symbols:{result.text.Length})\n{arg}"); } } public void Dispose() { _thread.Dispose(); } } internal static class FormattingUtils { public static string ToDisplayFormat(this Vector3 vector) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) return $"({vector.x:0.##} {vector.y:0.##} {vector.z:0.##})"; } public static string ToDisplayFormat(this Quaternion quaternion) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return ((Quaternion)(ref quaternion)).eulerAngles.ToDisplayFormat(); } public static string ToDisplayFormat(this float value) { return $"{value:0.##}"; } public static string ToDisplayFormat(this Vector2i vector) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) return $"({vector.x} {vector.y})"; } public static string ToDisplayFormat(this Vector2s vector) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) return $"({vector.x} {vector.y})"; } } public static class Log { private static ManualLogSource _instance; public static void CreateInstance(ManualLogSource source) { _instance = source; } public static void Info(object msg) { _instance.LogInfo((object)FormatMessage(msg)); } public static void Message(object msg) { _instance.LogMessage((object)FormatMessage(msg)); } public static void Debug(object msg) { _instance.LogDebug((object)FormatMessage(msg)); } public static void Warning(object msg) { _instance.LogWarning((object)FormatMessage(msg)); } public static void Error(object msg) { _instance.LogError((object)FormatMessage(msg)); } public static void Fatal(object msg) { _instance.LogFatal((object)FormatMessage(msg)); } private static string FormatMessage(object msg) { return $"[{DateTime.UtcNow:G}] {msg}"; } } public static class PlayerUtils { public static string GetPlayerInfo(this ZNetPeer peer) { return peer.m_playerName + "(" + peer.GetSteamId() + ")"; } public static void InvokeRoutedRpcToZdo(this ZNetPeer peer, string rpc, params object[] args) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) ZDO zDO = peer.GetZDO(); ZRoutedRpc.instance.InvokeRoutedRPC(zDO.GetOwner(), zDO.m_uid, rpc, args); } public static ZDO GetZDO(this ZNetPeer peer) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return ZDOMan.instance.GetZDO(peer.m_characterID); } public static string GetSteamId(this ZNetPeer peer) { return peer.m_rpc.GetSocket().GetHostName(); } public static long GetPlayerId(this ZNetPeer peer) { ZDO zDO = peer.GetZDO(); if (zDO == null) { return 0L; } return zDO.GetLong(ZDOVars.s_playerID, 0L); } public static void WritePlayerInfo(this ZNetPeer peer, StringBuilder sb) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) sb.AppendFormat("{0} Steam ID:{1}", peer.m_playerName, peer.GetSteamId()); sb.AppendFormat(" Position: {0}", peer.GetRefPos().ToDisplayFormat()); sb.AppendFormat(" Zone: {0}", ZoneSystem.GetZone(peer.GetRefPos()).ToDisplayFormat()); ZDO zDO = peer.GetZDO(); if (zDO != null) { sb.AppendFormat(" Player ID:{0}", peer.GetPlayerId()); sb.AppendFormat(" HP:{0}/{1}", zDO.GetFloat(ZDOVars.s_health, 0f).ToDisplayFormat(), zDO.GetFloat(ZDOVars.s_maxHealth, 0f).ToDisplayFormat()); } sb.AppendFormat(" Public position: {0}", peer.m_publicRefPos); if (peer.m_serverSyncedPlayerData.TryGetValue("platformDisplayName", out var value)) { sb.AppendFormat(" Platform name: {0}", value); } } } [BepInProcess("valheim_server.exe")] [BepInPlugin("org.tristan.rcon", "Valheim Rcon", "1.6.2")] public class Plugin : BaseUnityPlugin { [HarmonyPatch] private class Patches { [HarmonyFinalizer] [HarmonyPatch(typeof(ZNet), "UpdatePlayerList")] private static void ZNet_UpdatePlayerList(ZNet __instance) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) PlayerInfo val = default(PlayerInfo); if (!ZNet.TryGetPlayerByPlatformUserID(CommandsUserInfo.UserId, ref val) && __instance.m_players.Count != 0) { string value = ServerChatName.Value; PlayerInfo item = new PlayerInfo { m_name = value, m_userInfo = new CrossNetworkUserInfo { m_displayName = value, m_id = CommandsUserInfo.UserId, m_serverAssignedDisplayName = value, m_playfabId = string.Empty } }; __instance.m_players.Add(item); } } } private const long MaxDiscordPayloadSize = 10485760L; public const string Guid = "org.tristan.rcon"; public const string Name = "Valheim Rcon"; public const string Version = "1.6.2"; private const int MaxDiscordMessageLength = 1900; private const int TruncatedMessageLength = 200; public static ConfigEntry<string> DiscordUrl; public static ConfigEntry<string> Password; public static ConfigEntry<int> Port; public static ConfigEntry<string> ServerChatName; private static ConfigEntry<string> WhiteListConfig; private static ConfigEntry<string> BlackListConfig; private static ConfigEntry<string> DiscordSecurityUrl; private static ConfigEntry<string> DiscordSecurityReportPrefix; private static ConfigEntry<Incident> LogIncidents; private DiscordService _discordService; private StringBuilder _builder = new StringBuilder(); private string _cacheFilesFolder; public static readonly UserInfo CommandsUserInfo = new UserInfo { Name = string.Empty, UserId = new PlatformUserID("Bot", 0uL, false) }; public static IpAddressFilter IpFilter = new IpAddressFilter(); private void Awake() { //IL_01c0: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Expected O, but got Unknown Log.CreateInstance(((BaseUnityPlugin)this).Logger); Port = ((BaseUnityPlugin)this).Config.Bind<int>("1. Rcon", "Port", 2458, "Port to receive RCON commands. [Server restart required for update]"); Password = ((BaseUnityPlugin)this).Config.Bind<string>("1. Rcon", "Password", "", "Password for RCON packages validation. Empty password means plugin will not work! [Server restart required for update]"); WhiteListConfig = ((BaseUnityPlugin)this).Config.Bind<string>("1. Rcon", "Whitelist IP mask", "", "Comma-separated list of IP addresses or masks (e.g., 192.168.1.0/24, 10.0.0.1)"); BlackListConfig = ((BaseUnityPlugin)this).Config.Bind<string>("1. Rcon", "Blacklist IP mask", "", "Comma-separated list of IP addresses or masks (e.g., 192.168.1.0/24, 10.0.0.1)"); DiscordUrl = ((BaseUnityPlugin)this).Config.Bind<string>("2. Discord", "Webhook url", "", "Discord webhook for sending command results"); ServerChatName = ((BaseUnityPlugin)this).Config.Bind<string>("3. Chat", "Server name", "Server", "Name of server to display messages sent with rcon command"); DiscordSecurityReportPrefix = ((BaseUnityPlugin)this).Config.Bind<string>("4. Security", "Message prefix", "@here Security alert", "Prefix attached to every security report"); DiscordSecurityUrl = ((BaseUnityPlugin)this).Config.Bind<string>("4. Security", "Webhook url", "", "Discord webhook for sending security reports"); LogIncidents = ((BaseUnityPlugin)this).Config.Bind<Incident>("4. Security", "Incidents", Incident.UnauthorizedAccess | Incident.UnexpectedBehaviour, "Incident types will be reported to discord"); CommandsUserInfo.Name = ServerChatName.Value; _cacheFilesFolder = Path.Combine(Paths.CachePath, "Valheim Rcon"); ClearCacheDirectory(_cacheFilesFolder); _discordService = new DiscordService(); RefreshIpFilter(); Helper.WatchConfigFileChanges(((BaseUnityPlugin)this).Config, RefreshIpFilter); Object.DontDestroyOnLoad((Object)new GameObject("RconProxy", new Type[1] { typeof(RconProxy) })); RconProxy.Instance.OnCommandCompleted += SendResultToDiscord; RconProxy.Instance.OnSecurityReport += SendReportToDiscord; Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), (string)null); if (string.IsNullOrWhiteSpace(Password.Value)) { Log.Error("Password is empty. Plugin will not work. Please configure a secure password and restart the server."); } else { RconCommandsUtil.RegisterAllCommands(Assembly.GetExecutingAssembly()); } } private void OnDestroy() { _discordService.Dispose(); } private void SendResultToDiscord(IRconPeer peer, string command, IReadOnlyList<string> args, CommandResult result) { string value = DiscordUrl.Value; if (string.IsNullOrEmpty(value)) { return; } string arg = command + " " + string.Join(" ", args); _builder.Clear(); _builder.AppendLine($"> {peer.Address} -> {arg}"); if (_builder.Length > 1900) { string value2 = RconCommandsUtil.TruncateMessage(_builder.ToString(), 200); _builder.Clear(); _builder.Append(value2); _builder.AppendLine("..."); } int num = 1900 - _builder.Length; if (result.Text.Length > num) { string value3 = RconCommandsUtil.TruncateMessage(result.Text, 200); _builder.AppendLine(value3); _builder.Append("*--- message truncated ---*"); _discordService.SendResult(value, _builder.ToString(), result.AttachedFilePath); string text = Path.Combine(_cacheFilesFolder, $"{DateTime.UtcNow.Ticks}.txt"); FileHelpers.EnsureDirectoryExists(text); File.WriteAllText(text, result.Text); FileInfo fileInfo = new FileInfo(text); Log.Debug($"Saved full result {text}. Size {(float)fileInfo.Length / 1024f}kb"); if (fileInfo.Length > 10485760) { string text2 = Path.Combine(Utils.GetSaveDataPath((FileSource)2), "Valheim Rcon", $"{command}_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss}.txt"); FileHelpers.EnsureDirectoryExists(text2); File.Copy(text, text2, overwrite: true); string text3 = "The result is too long to send to Discord. It has been saved on the server: `" + text2 + "`"; _discordService.SendResult(value, text3, ""); Log.Message(text3); } else { _discordService.SendResult(value, "**Full message**", text); } } else { _builder.Append(result.Text); _discordService.SendResult(value, _builder.ToString(), result.AttachedFilePath); } } private void SendReportToDiscord(object endPoint, Incident incident, string reason) { if (!LogIncidents.Value.HasFlag(incident)) { return; } string value = DiscordSecurityUrl.Value; if (!string.IsNullOrEmpty(value)) { _builder.Clear(); if (!string.IsNullOrEmpty(DiscordSecurityReportPrefix.Value)) { _builder.AppendLine(DiscordSecurityReportPrefix.Value); } _builder.AppendFormat("[`{0}`] Disconnected for security reasons", endPoint); _builder.AppendLine(); _builder.AppendFormat("**Reason**: {0}", reason); _discordService.SendResult(value, _builder.ToString(), null); } } private void RefreshIpFilter() { IEnumerable<string> blackList = ParseList(BlackListConfig.Value); IEnumerable<string> whiteList = ParseList(WhiteListConfig.Value); IpFilter.RefreshFilter(whiteList, blackList); Log.Debug($"IP filter updated {IpFilter}"); } private static IEnumerable<string> ParseList(string config) { return config.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); } private static void ClearCacheDirectory(string cacheDirectory) { if (Directory.Exists(cacheDirectory)) { DirectoryInfo directoryInfo = new DirectoryInfo(cacheDirectory); Log.Info($"Clearing cache. Files {directoryInfo.GetFiles().Length}, directories {directoryInfo.GetDirectories().Length}"); Directory.Delete(cacheDirectory, recursive: true); } } } public static class RconCommandsUtil { public static string TruncateMessage(string message, int maxLength) { if (message.Length <= maxLength) { return message; } return message.Substring(0, maxLength) + "..."; } public static string TruncateMessageByBytes(string message, int maxBytes) { if (string.IsNullOrEmpty(message)) { return message; } if (Encoding.UTF8.GetBytes(message).Length <= maxBytes) { return message; } StringBuilder stringBuilder = new StringBuilder(); int num = 0; for (int i = 0; i < message.Length; i++) { int byteCount = Encoding.UTF8.GetByteCount(message[i].ToString()); if (num + byteCount > maxBytes) { break; } stringBuilder.Append(message[i]); num += byteCount; } return stringBuilder.ToString(); } public static void RegisterAllCommands(Assembly assembly) { if ((Object)(object)RconProxy.Instance == (Object)null) { Log.Error("RconProxy not initialized"); return; } Log.Info("Registering rcon commands..."); Type commandInterfaceType = typeof(IRconCommand); foreach (Type item in from t in assembly.GetTypes() where t != null where !t.IsAbstract && t.IsClass where t.GetConstructor(Type.EmptyTypes) != null where t.GetInterfaces().Contains(commandInterfaceType) where t.GetCustomAttribute<ExcludeAttribute>() == null select t) { RconProxy.Instance.RegisterCommand(item); } } } public class RconProxy : MonoBehaviour { internal delegate void CompletedCommandDelegate(IRconPeer peer, string command, IReadOnlyList<string> args, CommandResult result); [HarmonyPatch] private class Patches { [HarmonyFinalizer] [HarmonyPatch(typeof(ZNet), "LoadWorld")] private static void ZNet_LoadWorld() { Instance.Startup(); } [HarmonyPrefix] [HarmonyPatch(typeof(Game), "Shutdown")] private static void Game_Shutdown() { Instance.ShutDown(); } } private class ListCommand : RconCommand { public override string Command => "list"; public override string Description => "Prints list of available commands."; protected override string OnHandle(CommandArgs args) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Available commands:"); foreach (IRconCommand value in Instance._commands.Values) { stringBuilder.AppendLine(value.Command + " - " + value.Description); } return stringBuilder.ToString().Trim(); } } private RconCommandReceiver _receiver; private Dictionary<string, IRconCommand> _commands = new Dictionary<string, IRconCommand>(); private IRconConnectionManager _connectionManager; public static RconProxy Instance { get; private set; } internal event CompletedCommandDelegate OnCommandCompleted; internal event SecurityReportHandler OnSecurityReport; private void Awake() { Instance = this; _connectionManager = new AsynchronousSocketListener(IPAddress.Any, Plugin.Port.Value, HandleSecurityReport, Plugin.IpFilter); _receiver = new RconCommandReceiver(_connectionManager, Plugin.Password.Value, HandleCommandAsync, HandleSecurityReport); } internal void Startup() { _connectionManager.StartListening(); } internal void ShutDown() { _receiver.Dispose(); _connectionManager.Dispose(); } private void Update() { _receiver.Update(); } private void HandleSecurityReport(object endPoint, Incident incident, string reason) { this.OnSecurityReport?.Invoke(endPoint, incident, reason); } public void RegisterCommand<T>() where T : IRconCommand { RegisterCommand(typeof(T)); } public void RegisterCommand(Type type) { if (Activator.CreateInstance(type) is IRconCommand rconCommand && !string.IsNullOrEmpty(rconCommand.Command)) { RegisterCommand(rconCommand); } } public void RegisterCommand(IRconCommand command) { if (_commands.TryGetValue(command.Command, out var _)) { Log.Error("Duplicated commands " + command.Command + "\n" + command.GetType().Name + "\n" + command.GetType().Name); } _commands[command.Command] = command; Log.Info("Registered command " + command.Command + " -> " + command.GetType().Name); } public void RegisterCommand(string command, string description, Func<CommandArgs, CommandResult> commandFunc) { RegisterCommand(new ActionCommand(command, description, commandFunc)); } private async Task<string> HandleCommandAsync(IRconPeer peer, string command, IReadOnlyList<string> args) { TaskCompletionSource<CommandResult> completionSource = new TaskCompletionSource<CommandResult>(); ThreadingUtil.RunInMainThread(delegate { RunCommand(command, args, completionSource); }); CommandResult result = await completionSource.Task; Log.Message("Command completed: " + command + "\n" + result.Text); this.OnCommandCompleted?.Invoke(peer, command, args, result); return result.Text; } private async void RunCommand(string commandName, IReadOnlyList<string> args, TaskCompletionSource<CommandResult> resultSource) { try { if (!_commands.TryGetValue(commandName, out var value)) { resultSource.TrySetResult(new CommandResult { Text = "Unknown command " + commandName }); } else { resultSource.TrySetResult(await value.HandleCommandAsync(new CommandArgs(args))); } } catch (Exception ex) { resultSource.TrySetResult(new CommandResult { Text = ex.Message }); } } } public static class ZdoUtils { private static readonly int TagZdoHash = StringExtensionMethods.GetStableHashCode("valheim_rcon_object_tag"); public static string GetTag(this ZDO zdo) { return zdo.GetString(TagZdoHash, ""); } public static void SetTag(this ZDO zdo, string tag) { zdo.Set(TagZdoHash, tag); } public static void SetZdoModified(this ZDO zdo) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) zdo.SetOwner(ZDOMan.GetSessionID()); zdo.DataRevision += 100; ZDOMan.instance.ForceSendZDO(zdo.m_uid); } public static string GetPrefabName(int prefabId) { GameObject prefab = ZNetScene.instance.GetPrefab(prefabId); if (!((Object)(object)prefab != (Object)null)) { return $"Unknown ({prefabId})"; } return ((Object)prefab).name; } public static string GetPrefabName(this ZDO zdo) { return GetPrefabName(zdo.GetPrefab()); } public static void DeleteZDO(ZDO zdo) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) if (zdo.Persistent) { zdo.SetOwner(ZDOMan.GetSessionID()); ZDOID connectionZDOID = zdo.GetConnectionZDOID((ConnectionType)3); if (connectionZDOID != ZDOID.None && ZDOMan.instance.m_objectsByID.TryGetValue(connectionZDOID, out var value) && value != zdo) { DeleteZDO(value); } ZDOMan.instance.DestroyZDO(zdo); } } public static bool CanModifyZdo(ZDO zdo) { if (!zdo.IsValid()) { return false; } if (ZNet.instance.m_peers.Any((ZNetPeer p) => p.m_characterID == zdo.m_uid)) { return false; } if (zdo.GetPrefabName().StartsWith("_")) { return false; } return true; } } } namespace ValheimRcon.ZDOInfo { internal class CommonZDOInfoProvider : IZDOInfoProvider { public void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) stringBuilder.AppendFormat("Prefab: {0}", zdo.GetPrefabName()); stringBuilder.AppendFormat(" Id: {0}:{1}", ((ZDOID)(ref zdo.m_uid)).ID, ((ZDOID)(ref zdo.m_uid)).UserID); stringBuilder.AppendFormat(" Position: {0}", zdo.GetPosition().ToDisplayFormat()); if (detailed) { stringBuilder.AppendFormat(" Zone: {0}", ZoneSystem.GetZone(zdo.GetPosition()).ToDisplayFormat()); stringBuilder.AppendFormat(" Rotation: {0}", zdo.GetRotation().ToDisplayFormat()); } string tag = zdo.GetTag(); if (!string.IsNullOrEmpty(tag)) { stringBuilder.Append(" Tag: " + tag); } } public bool IsAvailableTo(ZDO zdo) { return true; } } internal class BedZDOInfoProvider : ZDOInfoProviderBase<Bed> { public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { string text = zdo.GetString(ZDOVars.s_ownerName, ""); long num = zdo.GetLong(ZDOVars.s_owner, 0L); stringBuilder.Append("Owner: "); if (string.IsNullOrEmpty(text)) { stringBuilder.Append("<not claimed>"); } else { stringBuilder.AppendFormat("{0}({1})", text, num); } } } internal class BuildingZDOInfoProvider : IZDOInfoProvider { private readonly Dictionary<int, bool> _prefabs = new Dictionary<int, bool>(); private readonly Dictionary<int, float> _maxHealth = new Dictionary<int, float>(); private readonly Dictionary<int, float> _maxSupport = new Dictionary<int, float>(); public void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { stringBuilder.Append($"Creator: {zdo.GetLong(ZDOVars.s_creator, 0L)}"); float value; float num = (_maxHealth.TryGetValue(zdo.GetPrefab(), out value) ? value : 0f); stringBuilder.Append(" Health: " + zdo.GetFloat(ZDOVars.s_health, num).ToDisplayFormat()); if (detailed) { float value2; float num2 = (_maxSupport.TryGetValue(zdo.GetPrefab(), out value2) ? value2 : 0f); stringBuilder.Append(" Support: " + zdo.GetFloat(ZDOVars.s_support, num2).ToDisplayFormat()); } } public bool IsAvailableTo(ZDO zdo) { int prefab = zdo.GetPrefab(); if (_prefabs.TryGetValue(prefab, out var value)) { return value; } GameObject prefab2 = ZNetScene.instance.GetPrefab(prefab); WearNTear val = (((Object)(object)prefab2 != (Object)null) ? prefab2.GetComponentInChildren<WearNTear>() : null); value = (Object)(object)val != (Object)null; _prefabs[prefab] = value; if (!value) { return false; } _maxHealth[prefab] = val.m_health; _maxSupport[prefab] = val.m_support; return true; } } internal class CharacterZDOInfoProvider : ZDOInfoProviderBase<Character> { public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { stringBuilder.Append($"Level: {zdo.GetInt(ZDOVars.s_level, 0)}"); float num = zdo.GetFloat(ZDOVars.s_maxHealth, 0f); stringBuilder.Append(" Health: " + zdo.GetFloat(ZDOVars.s_health, num).ToDisplayFormat() + "/" + num.ToDisplayFormat()); stringBuilder.Append($" Tamed: {zdo.GetBool(ZDOVars.s_tamed, false)}"); string text = zdo.GetString(ZDOVars.s_tamedName, ""); string arg = zdo.GetString(ZDOVars.s_tamedNameAuthor, ""); if (!string.IsNullOrEmpty(text)) { stringBuilder.AppendFormat(" Name: {0} (author: {1})", text, arg); } } } internal class ContainerZDOInfoProvider : ZDOInfoProviderBase<Container> { private static readonly Inventory TempInventory = new Inventory("", (Sprite)null, 8, 5); public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown TempInventory.RemoveAll(); byte[] byteArray = zdo.GetByteArray(ZDOVars.s_items, (byte[])null); if (byteArray != null) { ZPackage val = new ZPackage(byteArray); TempInventory.Load(val); } List<ItemData> inventory = TempInventory.m_inventory; stringBuilder.Append("Container: "); int count = inventory.Count; if (count == 0) { stringBuilder.Append("<empty>"); } else { stringBuilder.AppendFormat("{0} items", count); } } } internal class CustomZDOInfoProvider : IZDOInfoProvider { private readonly HashSet<IZDOInfoProvider> _providers = new HashSet<IZDOInfoProvider>(); public CustomZDOInfoProvider(params IZDOInfoProvider[] providers) { _providers = providers.ToHashSet(); } public void AddProvider(IZDOInfoProvider provider) { _providers.Add(provider); } public void RemoveProvider(IZDOInfoProvider provider) { _providers.Remove(provider); } public void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { bool flag = false; foreach (IZDOInfoProvider provider in _providers) { if (provider.IsAvailableTo(zdo)) { if (flag) { stringBuilder.Append(' '); } provider.AppendInfo(zdo, stringBuilder, detailed); flag = true; } } } public bool IsAvailableTo(ZDO zdo) { if (_providers.Count == 0) { return false; } foreach (IZDOInfoProvider provider in _providers) { if (provider.IsAvailableTo(zdo)) { return true; } } return false; } } internal class GuardStoneZDOInfoProvider : ZDOInfoProviderBase<PrivateArea> { public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { stringBuilder.Append($"Enabled: {zdo.GetBool(ZDOVars.s_enabled, false)}"); stringBuilder.Append(" Owner: " + zdo.GetString(ZDOVars.s_creatorName, "")); stringBuilder.Append(" Permitted:"); IEnumerable<(long, string)> permittedPlayers = GetPermittedPlayers(zdo); if (!permittedPlayers.Any()) { stringBuilder.Append(" <empty>"); return; } foreach (var item in permittedPlayers) { stringBuilder.Append($" {item}"); } } private static IEnumerable<(long Id, string Name)> GetPermittedPlayers(ZDO zdo) { int count = zdo.GetInt(ZDOVars.s_permitted, 0); if (count > 0) { for (int i = 0; i < count; i++) { long item = zdo.GetLong($"pu_id{i}", 0L); string item2 = zdo.GetString($"pu_name{i}", ""); yield return (Id: item, Name: item2); } } } } internal class ItemDropZDOInfoProvider : ZDOInfoProviderBase<ItemDrop> { private readonly ItemData _tempData = new ItemData(); public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { ItemDrop.LoadFromZDO(_tempData, zdo, -1); ZDOInfoUtil.AppendItemInfo(_tempData, stringBuilder); } } internal class ArmorStandZDOInfoProvider : IZDOInfoProvider { private readonly Dictionary<int, int> _itemStandSlots = new Dictionary<int, int>(); private readonly ItemData _tempData = new ItemData(); public void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed = true) { stringBuilder.AppendFormat("Pose: {0}", zdo.GetInt(ZDOVars.s_pose, 0)); stringBuilder.AppendFormat(" Attached items: "); int num = _itemStandSlots[zdo.GetPrefab()]; bool flag = false; for (int i = 0; i < num; i++) { int num2 = zdo.GetInt(StringExtensionMethods.GetStableHashCode($"{i}_item"), 0); if (num2 != 0) { string prefabName = ZdoUtils.GetPrefabName(num2); if (flag) { stringBuilder.Append(','); } if (detailed) { ItemDrop.LoadFromZDO(_tempData, zdo, i); stringBuilder.AppendFormat("( {0} ", prefabName); ZDOInfoUtil.AppendItemInfo(_tempData, stringBuilder); stringBuilder.Append(')'); } else { stringBuilder.Append(prefabName); } flag = true; } } if (!flag) { stringBuilder.Append("<empty>"); } } public bool IsAvailableTo(ZDO zdo) { int prefab = zdo.GetPrefab(); if (_itemStandSlots.TryGetValue(prefab, out var value)) { return value > 0; } GameObject prefab2 = ZNetScene.instance.GetPrefab(prefab); ArmorStand val = default(ArmorStand); value = (((Object)(object)prefab2 != (Object)null && prefab2.TryGetComponent<ArmorStand>(ref val)) ? val.m_slots.Count : 0); _itemStandSlots[prefab] = value; return value > 0; } } internal class ItemStandZDOInfoProvider : ZDOInfoProviderBase<ItemStand> { private readonly ItemData _tempData = new ItemData(); public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { int num = zdo.GetInt(ZDOVars.s_item, 0); stringBuilder.AppendFormat(" Attached item: "); if (num == 0) { stringBuilder.Append("<empty>"); return; } stringBuilder.Append(ZdoUtils.GetPrefabName(num)); if (detailed) { stringBuilder.Append(' '); ItemDrop.LoadFromZDO(_tempData, zdo, -1); ZDOInfoUtil.AppendItemInfo(_tempData, stringBuilder); } } } public interface IZDOInfoProvider { bool IsAvailableTo(ZDO zdo); void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed); } internal class PortalZDOInfoProvider : ZDOInfoProviderBase<TeleportWorld> { public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { string arg = zdo.GetString(ZDOVars.s_tag, ""); string arg2 = zdo.GetString(ZDOVars.s_tagauthor, ""); stringBuilder.AppendFormat("Portal tag: {0} (author {1})", arg, arg2); } } internal class SignZDOInfoProvider : ZDOInfoProviderBase<Sign> { public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { string text = zdo.GetString(ZDOVars.s_text, ""); string arg = zdo.GetString(ZDOVars.s_author, ""); if (!string.IsNullOrEmpty(text)) { stringBuilder.AppendFormat("Text: {0} (author: {1})", text, arg); } } } internal class TombStoneZDOInfoProvider : ZDOInfoProviderBase<TombStone> { public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { string arg = zdo.GetString(ZDOVars.s_ownerName, ""); long num = zdo.GetLong(ZDOVars.s_owner, 0L); stringBuilder.AppendFormat("Tombstone: {0}({1})", arg, num); } } public abstract class ZDOInfoProviderBase<T> : IZDOInfoProvider where T : Component { private readonly Dictionary<int, bool> _prefabs = new Dictionary<int, bool>(); public virtual bool IsAvailableTo(ZDO zdo) { int prefab = zdo.GetPrefab(); if (_prefabs.TryGetValue(prefab, out var value)) { return value; } GameObject prefab2 = ZNetScene.instance.GetPrefab(prefab); value = (Object)(object)prefab2 != (Object)null && (Object)(object)prefab2.GetComponentInChildren<T>() != (Object)null; _prefabs[prefab] = value; return value; } public abstract void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed = true); } public static class ZDOInfoUtil { private static readonly CustomZDOInfoProvider _externalProvider = new CustomZDOInfoProvider(); private static readonly CustomZDOInfoProvider _globalProvider = new CustomZDOInfoProvider(new CommonZDOInfoProvider(), new BuildingZDOInfoProvider(), new ItemDropZDOInfoProvider(), new CharacterZDOInfoProvider(), new GuardStoneZDOInfoProvider(), new ArmorStandZDOInfoProvider(), new ItemStandZDOInfoProvider(), new ContainerZDOInfoProvider(), new BedZDOInfoProvider(), new TombStoneZDOInfoProvider(), new SignZDOInfoProvider(), new PortalZDOInfoProvider(), _externalProvider); public static void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed = true) { _globalProvider.AppendInfo(zdo, stringBuilder, detailed); if (!zdo.Persistent) { stringBuilder.Append(" [NOT PERSISTENT]"); } } public static void AppendItemInfo(ItemData item, StringBuilder stringBuilder) { stringBuilder.AppendFormat("Stack: {0}", item.m_stack); stringBuilder.AppendFormat(" Quality: {0}", item.m_quality); if (item.m_variant != 0) { stringBuilder.AppendFormat(" Variant: {0}", item.m_variant); } if (item.m_crafterID != 0L) { stringBuilder.AppendFormat(" Crafter: {0} ({1})", item.m_crafterName, item.m_crafterID); } if (item.m_customData.Count <= 0) { return; } stringBuilder.Append(" Data:"); foreach (KeyValuePair<string, string> customDatum in item.m_customData) { stringBuilder.Append(" '" + customDatum.Key + "'='" + customDatum.Value + "'"); } } public static void RegisterInfoProvider(IZDOInfoProvider provider) { _externalProvider.AddProvider(provider); } } } namespace ValheimRcon.Core { internal sealed class AsynchronousSocketListener : IRconConnectionManager, IDisposable { private static readonly TimeSpan UnauthorizedClientLifetime = TimeSpan.FromSeconds(30.0); private readonly IPAddress _address; private readonly int _port; private readonly Socket _listener; private readonly HashSet<IRconPeer> _clients = new HashSet<IRconPeer>(); private readonly HashSet<IRconPeer> _waitingForDisconnect = new HashSet<IRconPeer>(); private readonly SecurityReportHandler _securityReportHandler; private readonly IpAddressFilter _filter; private bool _checkNewConnections; public event Action<IRconPeer, RconPacket> OnMessage; public AsynchronousSocketListener(IPAddress ipAddress, int port, SecurityReportHandler securityReportHandler, IpAddressFilter filter) { if (ipAddress == null) { throw new ArgumentNullException("ipAddress"); } if (port < 1 || port > 65535) { throw new ArgumentOutOfRangeException("port", "Port must be between 1 and 65535"); } _address = ipAddress; _port = port; _filter = filter; _listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); _securityReportHandler = securityReportHandler; } public void StartListening() { Log.Message("Start listening rcon commands"); try { IPEndPoint localEP = new IPEndPoint(_address, _port); _listener.Bind(localEP); _listener.Listen(100); _checkNewConnections = true; } catch (Exception msg) { Log.Error(msg); } } public void Update() { TryAcceptNewClients(); DateTime now = DateTime.Now; foreach (IRconPeer client in _clients) { RconPacket packet; string error; if (!client.IsConnected()) { Disconnect(client); } else if (!client.Authentificated && now > client.Created + UnauthorizedClientLifetime) { Log.Warning($"Unauthorized timeout [{client.Address}]"); _securityReportHandler?.Invoke(client.Address, Incident.UnexpectedBehaviour, "Unauthorized timeout."); Disconnect(client); } else if (!_filter.IsAllowed(client.Address)) { Log.Warning($"Disconnected by IP filter [{client.Address}]"); _securityReportHandler?.Invoke(client.Address, Incident.IpFilter, "Disconnected by IP filter."); Disconnect(client); } else if (client.TryReceive(out packet, out error)) { this.OnMessage?.Invoke(client, packet); } else if (!string.IsNullOrEmpty(error)) { _securityReportHandler?.Invoke(client.Address, Incident.UnexpectedBehaviour, error); Disconnect(client); } } foreach (IRconPeer item in _waitingForDisconnect) { _clients.Remove(item); DisconnectPeer(item); } _waitingForDisconnect.Clear(); } public void Dispose() { _checkNewConnections = false; _listener.Close(); foreach (IRconPeer client in _clients) { client.Dispose(); } _clients.Clear(); _waitingForDisconnect.Clear(); } public void Disconnect(IRconPeer peer) { _waitingForDisconnect.Add(peer); } private void TryAcceptNewClients() { if (!_checkNewConnections) { return; } try { if (_listener.Poll(0, SelectMode.SelectRead)) { Socket socket = _listener.Accept(); OnClientConnected(socket); } } catch (Exception msg) { Log.Error(msg); } } private void OnClientConnected(Socket socket) { if (!(socket.RemoteEndPoint is IPEndPoint iPEndPoint)) { Log.Warning("Client connected with invalid endpoint"); _securityReportHandler?.Invoke(socket.RemoteEndPoint, Incident.IpFilter, "Rejected connection. Unknown endpoint."); socket.Close(); return; } IPAddress address = iPEndPoint.Address; if (!_filter.IsAllowed(address)) { Log.Warning($"Client connection rejected from [{iPEndPoint}] - IP not allowed"); _securityReportHandler?.Invoke(address, Incident.IpFilter, "Rejected connection by IP filter."); socket.Close(); } else { RconPeer rconPeer = new RconPeer(socket); Log.Debug($"Client connected [{rconPeer.Address}]"); _clients.Add(rconPeer); } } private void DisconnectPeer(IRconPeer peer) { Log.Debug($"Client disconnected [{peer.Address}]"); try { peer.Dispose(); } catch { Log.Debug("Warning: Could not dispose peer connection"); } } } public interface IRconConnectionManager : IDisposable { event Action<IRconPeer, RconPacket> OnMessage; void StartListening(); void Update(); void Disconnect(IRconPeer peer); } public interface IRconPeer : IDisposable { bool Authentificated { get; } IPAddress Address { get; } DateTime Created { get; } void SetAuthentificated(bool authentificated); Task SendAsync(RconPacket packet); bool IsConnected(); bool TryReceive(out RconPacket packet, out string error); } public enum PacketType { Error = 0, Command = 2, Login = 3 } public delegate Task<string> RconCommandHandler(IRconPeer peer, string command, IReadOnlyList<string> data); public class RconCommandReceiver : IDisposable { private static readonly Regex MatchRegex = new Regex("(?<=[ ][\\\"]|^[\\\"])[^\\\"]+(?=[\\\"][ ]|[\\\"]$)|(?<=[ ]|^)[^\\\" ]+(?=[ ]|$)", RegexOptions.Compiled | RegexOptions.CultureInvariant); private readonly IRconConnectionManager _manager; private readonly string _password; private readonly RconCommandHandler _commandHandler; private readonly SecurityReportHandler _securityReportHandler; public RconCommandReceiver(IRconConnectionManager connectionManager, string password, RconCommandHandler commandHandler, SecurityReportHandler securityReportHandler) { if (password == null) { throw new ArgumentException("Password cannot be null", "password"); } if (commandHandler == null) { throw new ArgumentNullException("commandHandler"); } _password = password; _manager = connectionManager; _manager.OnMessage += SocketListener_OnMessage; _commandHandler = commandHandler; _securityReportHandler = securityReportHandler; } public void Update() { _manager.Update(); } public void Dispose() { _manager.OnMessage -= SocketListener_OnMessage; } private async void SocketListener_OnMessage(IRconPeer peer, RconPacket packet) { switch (packet.type) { case PacketType.Login: { if (peer.Authentificated) { Log.Warning($"Already authorized [{peer.Address}]"); _securityReportHandler?.Invoke(peer.Address, Incident.UnexpectedBehaviour, "Already authorized."); await peer.SendAsync(new RconPacket(packet.requestId, PacketType.Command, "Already authorized")); _manager.Disconnect(peer); break; } bool success = !string.IsNullOrWhiteSpace(_password) && string.Equals(packet.payload ?? string.Empty, _password); RconPacket rconPacket; if (success) { peer.SetAuthentificated(authentificated: true); rconPacket = new RconPacket(packet.requestId, PacketType.Command, "Login success"); } else { rconPacket = new RconPacket(-1, PacketType.Command, "Login failed"); } Log.Debug($"Login result {rconPacket}"); await peer.SendAsync(rconPacket); if (!success) { _securityReportHandler?.Invoke(peer.Address, Incident.UnauthorizedAccess, "Login failed."); _manager.Disconnect(peer); } break; } case PacketType.Command: { if (!peer.Authentificated) { Log.Warning($"Unauthorized access attempt [{peer.Address}]"); _securityReportHandler?.Invoke(peer.Address, Incident.UnauthorizedAccess, "Unauthorized access attempt."); await peer.SendAsync(new RconPacket(packet.requestId, packet.type, "Unauthorized")); _manager.Disconnect(peer); break; } string input = packet.payload?.TrimStart(new char[1] { '/' }) ?? string.Empty; List<string> list = (from Match m in MatchRegex.Matches(input) select m.Value).ToList(); if (list.Count == 0) { Log.Warning($"Empty command from [{peer.Address}]"); await peer.SendAsync(new RconPacket(packet.requestId, packet.type, "Empty command")); break; } string command = list[0]; list.RemoveAt(0); string payload = ValidatePayloadLength(await _commandHandler(peer, command, list)); RconPacket rconPacket2 = new RconPacket(packet.requestId, packet.type, payload); Log.Debug($"Command result {command} - {rconPacket2}"); await peer.SendAsync(rconPacket2); break; } default: Log.Warning($"Unknown packet type: {packet} [{peer.Address}]"); _securityReportHandler?.Invoke(peer.Address, Incident.UnexpectedBehaviour, $"Unknown packet type {packet}."); await peer.SendAsync(new RconPacket(packet.requestId, PacketType.Error, "Cannot handle command")); _manager.Disconnect(peer); break; } } private string ValidatePayloadLength(string payload) { if (RconPacket.GetPayloadSize(payload) > 4050) { int maxBytes = 4050 - RconPacket.GetPayloadSize("\n--- message truncated ---"); return RconCommandsUtil.TruncateMessageByBytes(payload, maxBytes) + "\n--- message truncated ---"; } return payload; } } public readonly struct RconPacket { public const int MaxPayloadSize = 4050; private const int MaxPacketLength = 4096; public readonly int requestId; public readonly PacketType type; public readonly string payload; public RconPacket(byte[] bytes) { if (bytes == null) { throw new ArgumentNullException("bytes"); } if (bytes.Length < 14) { throw new ArgumentException($"Packet too small - {bytes.Length} bytes", "bytes"); } if (bytes.Length > 65536) { throw new ArgumentException($"Packet too large - {bytes.Length} bytes", "bytes"); } using MemoryStream input = new MemoryStream(bytes); using BinaryReader binaryReader = new BinaryReader(input); int num = binaryReader.ReadInt32(); if (num < 0) { throw new ArgumentException($"Invalid packet length - {num}", "bytes"); } if (num < 10) { throw new ArgumentException($"Packet data too small - {num}", "bytes"); } if (num > 2147483643 || num + 4 > bytes.Length) { throw new ArgumentException($"Packet length exceeds buffer size - {num}", "bytes"); } requestId = binaryReader.ReadInt32(); type = (PacketType)binaryReader.ReadInt32(); if (!Enum.IsDefined(typeof(PacketType), type)) { throw new ArgumentException($"Invalid packet type {type}", "bytes"); } int num2 = num - 10; if (num2 < 0) { throw new ArgumentException($"Invalid payload size - {num2} bytes", "bytes"); } if (num2 > 4050) { throw new ArgumentException($"Payload too large - {num2} bytes", "bytes"); } byte[] bytes2 = binaryReader.ReadBytes(num2); payload = Encoding.UTF8.GetString(bytes2); } public RconPacket(int requestId, PacketType type, string payload) { payload = payload ?? string.Empty; int payloadSize = GetPayloadSize(payload); if (payloadSize > 4050) { throw new ArgumentException($"Payload too large - {payloadSize} bytes", "payload"); } this.requestId = requestId; this.type = type; this.payload = payload; } public byte[] Serialize() { byte[] bytes = Encoding.UTF8.GetBytes(payload ?? string.Empty); if (bytes.Length > 4096) { throw new InvalidOperationException("Payload too large for serialization"); } using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream); long position = memoryStream.Position; binaryWriter.Write(0); binaryWriter.Write(requestId); binaryWriter.Write((int)type); binaryWriter.Write(bytes); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); long num = memoryStream.Position - position - 4; if (num > 4096) { throw new InvalidOperationException($"Packet too large for serialization - {num}"); } int value = (int)num; memoryStream.Position = position; binaryWriter.Write(value); return memoryStream.ToArray(); } public static int GetPayloadSize(string payload) { if (payload == null) { return 0; } return Encoding.UTF8.GetByteCount(payload); } public override string ToString() { if (type == PacketType.Login) { return $"[{requestId} t:{type} ****]"; } return $"[{requestId} t:{type} {payload}]"; } } public class RconPeer : IRconPeer, IDisposable { public const int BufferSize = 4096; private bool _disposed; private readonly byte[] _buffer = new byte[4096]; private readonly Socket _socket; private readonly IPAddress _address; public DateTime Created { get; } public bool Authentificated { get; private set; } public IPAddress Address { get { if (_disposed) { return null; } return _address; } } public RconPeer(Socket workSocket) { if (workSocket == null) { throw new ArgumentNullException("workSocket"); } _socket = workSocket; _address = (_socket.RemoteEndPoint as IPEndPoint).Address; Created = DateTime.Now; } public async Task SendAsync(RconPacket packet) { if (_disposed) { Log.Debug("Warning: Attempted to send to disposed peer"); return; } if (_socket == null || !_socket.Connected) { Log.Debug("Warning: Socket is null or not connected"); return; } byte[] array = packet.Serialize(); Log.Debug($"Sent {await SocketTaskExtensions.SendAsync(_socket, new ArraySegment<byte>(array), SocketFlags.None)} bytes to client [{Address}]"); } public bool IsConnected() { if (_disposed) { return false; } if (_socket.Connected) { if (_socket.Poll(0, SelectMode.SelectRead)) { return _socket.Available != 0; } return true; } return false; } public bool TryReceive(out RconPacket packet, out string error) { packet = default(RconPacket); error = null; if (_disposed) { return false; } if (_socket.Poll(0, SelectMode.SelectRead) && _socket.Available > 0) { int available = _socket.Available; if (available > _buffer.Length) { error = $"Available data exceeds buffer size: {available} > {_buffer.Length}"; Log.Warning($"{error} [{Address}]"); return false; } if (_socket.Receive(_buffer, 0, Math.Min(available, _buffer.Length), SocketFlags.None) == 0) { return false; } try { packet = new RconPacket(_buffer); Log.Debug($"Received package {packet} from [{Address}]"); return true; } catch (Exception ex) { Log.Warning($"Failed to parse packet from [{Address}]: {ex.Message}"); error = ex.Message; return false; } finally { Array.Clear(_buffer, 0, _buffer.Length); } } return false; } public void SetAuthentificated(bool authentificated) { Authentificated = authentificated; } public void Dispose() { if (!_disposed) { _disposed = true; try { _socket?.Shutdown(SocketShutdown.Both); } catch { } try { _socket?.Close(); } catch { } _socket.Dispose(); } } } public delegate void SecurityReportHandler(object endPoint, Incident incident, string reason); public class IpAddressFilter { private List<LukeSkywalker.IPNetwork.IPNetwork> _whiteList = new List<LukeSkywalker.IPNetwork.IPNetwork>(); private List<LukeSkywalker.IPNetwork.IPNetwork> _blackList = new List<LukeSkywalker.IPNetwork.IPNetwork>(); public void RefreshFilter(IEnumerable<string> whiteList, IEnumerable<string> blackList) { _whiteList.Clear(); _blackList.Clear(); if (ParseConfigs(whiteList, out var validNetworks)) { _whiteList.AddRange(validNetworks); } else { _whiteList.Add(LukeSkywalker.IPNetwork.IPNetwork.Parse("127.0.0.1")); } ParseConfigs(blackList, out var validNetworks2); _blackList.AddRange(validNetworks2); } public bool IsAllowed(IPAddress address) { if (address == null) { return false; } if (_blackList.Count > 0 && IsInList(address, _blackList)) { return false; } if (_whiteList.Count != 0) { return IsInList(address, _whiteList); } return true; } private static bool IsInList(IPAddress address, IReadOnlyCollection<LukeSkywalker.IPNetwork.IPNetwork> list) { foreach (LukeSkywalker.IPNetwork.IPNetwork item in list) { if (LukeSkywalker.IPNetwork.IPNetwork.Contains(item, address)) { return true; } } return false; } private static bool ParseConfigs(IEnumerable<string> configs, out IReadOnlyCollection<LukeSkywalker.IPNetwork.IPNetwork> validNetworks) { List<LukeSkywalker.IPNetwork.IPNetwork> list = (List<LukeSkywalker.IPNetwork.IPNetwork>)(validNetworks = new List<LukeSkywalker.IPNetwork.IPNetwork>()); if (configs == null) { return true; } bool result = true; foreach (string config in configs) { try { LukeSkywalker.IPNetwork.IPNetwork item = Parse(config.Trim()); list.Add(item); } catch (Exception arg) { Log.Error($"Cannot parse config {config} - {arg}"); } } return result; } private static LukeSkywalker.IPNetwork.IPNetwork Parse(string config) { string[] array = config.Split(new char[1] { '/' }); if (array.Length == 2) { string ipaddress = array[0]; byte cidr = byte.Parse(array[1]); return LukeSkywalker.IPNetwork.IPNetwork.Parse(ipaddress, cidr); } if (array.Length == 1) { return LukeSkywalker.IPNetwork.IPNetwork.Parse(config, 32); } throw new ArgumentException("Invalid network " + config); } public override string ToString() { return "IpAddressFilter - whitelist: " + string.Join(",", _whiteList) + " blacklist: " + string.Join(",", _blackList); } } [Flags] public enum Incident { IpFilter = 2, UnauthorizedAccess = 4, UnexpectedBehaviour = 8 } } namespace ValheimRcon.Commands { internal class ActionCommand : IRconCommand { private readonly Func<CommandArgs, CommandResult> _execute; public string Command { get; } public string Description { get; } public ActionCommand(string command, string description, Func<CommandArgs, CommandResult> execute) { Command = command; _execute = execute; Description = description; } public Task<CommandResult> HandleCommandAsync(CommandArgs args) { return Task.FromResult(_execute(args)); } } internal class AddAdmin : RconCommand { public override string Command => "addAdmin"; public override string Description => "Adds a player to the admin list. Usage: addAdmin <steamId>"; protected override string OnHandle(CommandArgs args) { string text = args.GetString(0); ZNet.instance.m_adminList.Add(text); return text + " is admin now"; } } internal class AddPermitted : RconCommand { public override string Command => "addPermitted"; public override string Description => "Adds a player to the permitted list. Usage: addPermitted <steamId>"; protected override string OnHandle(CommandArgs args) { string text = args.GetString(0); ZNet.instance.m_permittedList.Add(text); return text + " added to permitted"; } } internal class Ban : RconCommand { public override string Command => "ban"; public override string Description => "Ban a user from the server. Usage: ban <playername or steamid>"; protected override string OnHandle(CommandArgs args) { string text = args.GetString(0); ZNet.instance.Ban(text); return "Banned " + text; } } public class CommandArgs { private static readonly Regex OptionalArgumentRegex = new Regex("^-[A-Za-z]+$"); public IReadOnlyList<string> Arguments { get; } public CommandArgs(IReadOnlyList<string> args) { Arguments = args; } public int GetInt(int index) { ValidateIndex(index); if (!int.TryParse(Arguments[index], out var result)) { throw new ArgumentException($"Argument at {index} is invalid"); } return result; } public int TryGetInt(int index, int defaultValue = 0) { if (!HasArgument(index)) { return defaultValue; } return GetInt(index); } public long GetLong(int index) { ValidateIndex(index); if (!long.TryParse(Arguments[index], out var result)) { throw new ArgumentException($"Argument at {index} is invalid"); } return result; } public long TryGetLong(int index, long defaultValue) { if (!HasArgument(index)) { return defaultValue; } return GetLong(index); } public float GetFloat(int index) { ValidateIndex(index); if (!float.TryParse(Arguments[index], out var result)) { throw new ArgumentException($"Argument at {index} is invalid"); } return result; } public float TryGetFloat(int index, float defaultValue) { if (!HasArgument(index)) { return defaultValue; } return GetFloat(index); } public string GetString(int index) { ValidateIndex(index); return Arguments[index]; } public string TryGetString(int index, string defaultValue = "") { if (!HasArgument(index)) { return defaultValue; } return GetString(index); } public uint GetUInt(int index) { ValidateIndex(index); if (!uint.TryParse(Arguments[index], out var result)) { throw new ArgumentException($"Argument at {index} is invalid"); } return result; } public uint TryGetUInt(int index, uint defaultValue = 0u) { if (!HasArgument(index)) { return defaultValue; } return GetUInt(index); } private void ValidateIndex(int index) { if (HasArgument(index)) { return; } throw new ArgumentException($"Cannot get argument at {index}"); } private bool HasArgument(int index) { if (index >= 0) { return index < Arguments.Count; } return false; } public IEnumerable<(int Index, string Argument)> GetOptionalArguments() { for (int i = 0; i < Arguments.Count; i++) { if (OptionalArgumentRegex.IsMatch(Arguments[i])) { yield return (Index: i, Argument: Arguments[i]); } } } public override string ToString() { return string.Join(" ", Arguments); } } public static class CommandArgsExtensions { public static Vector3 GetVector3(this CommandArgs args, int index) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) return new Vector3(args.GetFloat(index), args.GetFloat(index + 1), args.GetFloat(index + 2)); } public static Vector2i GetVector2i(this CommandArgs args, int index) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) return new Vector2i(args.GetInt(index), args.GetInt(index + 1)); } public static ObjectId GetObjectId(this CommandArgs args, int index) { string text = args.GetString(index); string[] array = text.Split(new char[1] { ':' }); if (array.Length == 2 && uint.TryParse(array[0], out var result) && long.TryParse(array[1], out var result2)) { return new ObjectId(result, result2); } throw new ArgumentException("Cannot parse " + text + " as object id (expected format is ID:User)"); } } public struct CommandResult { public string Text; public string AttachedFilePath; public static CommandResult WithText(string text) { return new CommandResult { Text = text }; } } internal class DeleteObjects : RconCommand { private readonly List<ISearchCriteria> _criterias = new List<ISearchCriteria>(); public override string Command => "deleteObjects"; public override string Description => "Delete objects matching all search criteria. Usage (with optional arguments): deleteObjects -near <x> <y> <z> <radius> -zone <x> <y> -prefab <prefab> -creator <creator id> -id <id:userid> -tag <tag> -force (bypass security checks)"; protected override string OnHandle(CommandArgs args) { //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Unknown result type (might be due to invalid IL or missing references) bool flag = false; _criterias.Clear(); bool flag2 = false; string text = null; foreach (var (num, text2) in args.GetOptionalArguments()) { switch (text2.ToLower()) { case "-creator": _criterias.Add(new CreatorCriteria(args.GetLong(num + 1))); break; case "-id": _criterias.Add(new IdCriteria(args.GetObjectId(num + 1))); break; case "-prefab": text = args.GetString(num + 1); _criterias.Add(new PrefabCriteria(text)); break; case "-near": _criterias.Add(new NearCriteria(args.GetVector3(num + 1), args.GetFloat(num + 4))); flag2 = true; break; case "-zone": _criterias.Add(new ZoneCriteria(args.GetVector2i(num + 1))); flag2 = true; break; case "-tag": _criterias.Add(new TagCriteria(args.GetString(num + 1))); break; case "-force": flag = true; break; default: return "Unknown argument: " + text2; } } if (!_criterias.Any()) { return "At least one criteria must be provided."; } ZDO[] array = ZDOMan.instance.m_objectsByID.Values.Where((ZDO zdo) => _criterias.All((ISearchCriteria c) => c.IsMatch(zdo))).ToArray(); if (array.Length == 0) { return "No objects found matching the provided criteria."; } if (flag2 && _criterias.Count == 1) { return "Must provide at least 1 more criteria if use -near or -zone"; } if (!string.IsNullOrEmpty(text) && _criterias.Count == 1 && (Object)(object)ZNetScene.instance.GetPrefab(text) != (Object)null && !flag) { return $"You're about to delete all existing objects of prefab {text} ({array.Length}) in the world. Use -force if you really want to delete them."; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"Deleting {array.Length} objects:"); int num2 = 0; ZDO[] array2 = array; foreach (ZDO val in array2) { stringBuilder.Append('-'); ZDOInfoUtil.AppendInfo(val, stringBuilder); if (!val.Persistent) { stringBuilder.AppendLine(" [NOT ALLOWED TO DELETE]"); } else if (flag || ZdoUtils.CanModifyZdo(val)) { ZdoUtils.DeleteZDO(val); stringBuilder.AppendLine(" [DELETED]"); num2++; } else { stringBuilder.AppendLine(" [NOT ALLOWED TO DELETE]"); } stringBuilder.AppendLine(); } stringBuilder.AppendFormat("Deleted {0}/{1} objects", num2, array.Length); return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } } internal class ExcludeAttribute : Attribute { } internal class FindObjects : RconCommand { private readonly List<ISearchCriteria> _criterias = new List<ISearchCriteria>(); public override string Command => "findObjects"; public override string Description => "Find objects matching all search criteria. Usage (with optional arguments): findObjects -near <x> <y> <z> <radius> -zone <x> <y> -prefab <prefab> -creator <creator id> -id <id:userid> -tag <tag> -tag-old <tag> -detailed"; protected override string OnHandle(CommandArgs args) { //IL_01fa: Unknown result type (might be due to invalid IL or missing references) //IL_0225: Unknown result type (might be due to invalid IL or missing references) _criterias.Clear(); bool detailed = false; foreach (var (num, text) in args.GetOptionalArguments()) { switch (text.ToLower()) { case "-prefab": _criterias.Add(new PrefabCriteria(args.GetString(num + 1))); break; case "-creator": _criterias.Add(new CreatorCriteria(args.GetLong(num + 1))); break; case "-id": _criterias.Add(new IdCriteria(args.GetObjectId(num + 1))); break; case "-tag": _criterias.Add(new TagCriteria(args.GetString(num + 1))); break; case "-near": _criterias.Add(new NearCriteria(args.GetVector3(num + 1), args.GetFloat(num + 4))); break; case "-zone": _criterias.Add(new ZoneCriteria(args.GetVector2i(num + 1))); break; case "-tag-old": _criterias.Add(new OldTagCriteria(args.GetString(num + 1))); break; case "-detailed": detailed = true; break; default: return "Unknown argument: " + text; } } if (!_criterias.Any()) { return "At least one criteria must be provided."; } ZDO[] array = ZDOMan.instance.m_objectsByID.Values.Where((ZDO zdo2) => _criterias.All((ISearchCriteria c) => c.IsMatch(zdo2))).ToArray(); if (array.Length == 0) { return "No objects found matching the provided criteria."; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"Found {array.Length} objects:"); ZDO[] array2 = array; foreach (ZDO zdo in array2) { stringBuilder.Append('-'); ZDOInfoUtil.AppendInfo(zdo, stringBuilder, detailed); stringBuilder.AppendLine(); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } } internal class ModifyObject : RconCommand { private readonly List<IZdoModification> _modifications = new List<IZdoModification>(); private readonly StringBuilder builder = new StringBuilder(); public override string Command => "modifyObject"; public override string Description => "Modify properties of an object. Usage (with required and optional arguments): modifyObject <id:userid> -position <x> <y> <z> -rotation <x> <y> <z> -health <value> -tag <tag> -prefab <prefab name> -force (bypass security checks)"; protected override string OnHandle(CommandArgs args) { //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) ObjectId objectId = args.GetObjectId(0); ZDO val = Enumerable.FirstOrDefault(predicate: new IdCriteria(objectId).IsMatch, source: ZDOMan.instance.m_objectsByID.Values); if (val == null) { return "No object found"; } if (!val.Persistent) { return "Object is not persistent and cannot be modified."; } IEnumerable<(int Index, string Argument)> optionalArguments = args.GetOptionalArguments(); bool flag = false; string text = string.Empty; _modifications.Clear(); foreach (var item in optionalArguments) { var (num, _) = item; switch (item.Argument) { case "-position": _modifications.Add(new PositionModification(args.GetVector3(num + 1))); break; case "-rotation": _modifications.Add(new RotationModification(args.GetVector3(num + 1))); break; case "-health": _modifications.Add(new HealthModification(args.GetFloat(num + 1))); break; case "-tag": _modifications.Add(new TagModification(args.GetString(num + 1))); break; case "-prefab": text = args.GetString(num + 1); _modifications.Add(new PrefabModification(text)); break; case "-force": flag = true; break; default: return "Unknown argument: " + args.GetString(num); } } if (!_modifications.Any()) { return "At least one valid modification argument must be provided."; } if (!flag && !ZdoUtils.CanModifyZdo(val)) { return "Object cannot be modified."; } long owner = val.GetOwner(); ZNetPeer peer = ZNet.instance.GetPeer(owner); if (!flag && peer != null) { return "Object is owned by an online player " + peer.GetPlayerInfo() + " and cannot be modified."; } if (!flag && !string.IsNullOrEmpty(text) && !ZNetScene.instance.HasPrefab(StringExtensionMethods.GetStableHashCode(text))) { return "Cannot find prefab with name " + text + ". If you know what you are doing, use -force option."; } foreach (IZdoModification modification in _modifications) { modification.Apply(val); } val.SetZdoModified(); builder.Clear(); builder.AppendLine("Object modified successfully"); builder.AppendFormat("{0} ", val.GetPrefabName()); ZDOInfoUtil.AppendInfo(val, builder); return builder.ToString().TrimEnd(Array.Empty<char>()); } } internal class GetServerStats : RconCommand { private StringBuilder builder = new StringBuilder(); public override string Command => "serverStats"; public override string Description => "Prints server statistics including player count, FPS, memory usage, and world information."; protected override string OnHandle(CommandArgs args) { builder.Clear(); _ = ZNet.World?.m_name; int num = ZNet.instance?.m_peers.Count ?? (-1); float num2 = 1f / Time.deltaTime; int num3 = ZDOMan.instance?.m_objectsByID?.Count ?? (-1); EnvMan instance = EnvMan.instance; int num4; if (instance == null) { num4 = -1; } else { ZNet instance2 = ZNet.instance; num4 = instance.GetDay((instance2 != null) ? instance2.GetTimeSeconds() : 0.0); } int num5 = num4; int num6 = ZDOMan.instance?.m_deadZDOs.Count ?? 0; int num7 = ToMegabytes(Profiler.GetMonoUsedSizeLong()); int num8 = ToMegabytes(Profiler.GetMonoHeapSizeLong()); builder.AppendLine($"Stats - Online {num} FPS {num2:0.0}"); builder.AppendLine($"Memory - Mono {num7}MB, Heap {num8}MB"); builder.Append($"World - Day {num5}, Objects {num3}, Dead objects {num6}"); return builder.ToString(); } private int ToMegabytes(long bytes) { return Mathf.FloorToInt((float)bytes / 1048576f); } } internal class InvokeConsoleCommand : RconCommand { public override string Command => "consoleCommand"; public override string Description => "Executes a console command on the server. Usage: consoleCommand <command>"; protected override string OnHandle(CommandArgs args) { string text = string.Join(" ", args.Arguments); if (string.IsNullOrWhiteSpace(text)) { return "No command provided."; } ((Terminal)Console.instance).TryRunCommand(text, false, true); return "Command '" + text + "' executed."; } } internal class Kick : RconCommand { public override string Command => "kick"; public override string Description => "Kicks a player from the server. Usage: kick <playername or steamid>"; protected override string OnHandle(CommandArgs args) { string text = args.GetString(0); ZNet.instance.Kick(text); return "Kicked " + text; } } public readonly struct ObjectId { public readonly uint Id; public readonly long UserId; public ObjectId(uint id, long userId) { Id = id; UserId = userId; } } internal class PrintAdminList : RconCommand { private StringBuilder stringBuilder = new StringBuilder(); public override string Command => "adminlist"; public override string Description => "Prints the list of admins on the server."; protected override string OnHandle(CommandArgs args) { stringBuilder.Clear(); foreach (string item in ZNet.instance.m_adminList.GetList()) { stringBuilder.AppendLine(item); } return stringBuilder.ToString(); } } internal class PrintBanlist : RconCommand { private StringBuilder stringBuilder = new StringBuilder(); public override string Command => "banlist"; public override string Description => "Prints the list of banned players"; protected override string OnHandle(CommandArgs args) { stringBuilder.Clear(); foreach (string item in ZNet.instance.m_bannedList.GetList()) { stringBuilder.AppendLine(item); } return stringBuilder.ToString(); } } internal class PrintPermitlist : RconCommand { private StringBuilder stringBuilder = new StringBuilder(); public override string Command => "permitted"; public override string Description => "Prints the list of permitted players on the server."; protected override string OnHandle(CommandArgs args) { stringBuilder.Clear(); foreach (string item in ZNet.instance.m_permittedList.GetList()) { stringBuilder.AppendLine(item); } return stringBuilder.ToString(); } } public abstract class RconCommand : IRconCommand { public abstract string Command { get; } public abstract string Description { get; } public Task<CommandResult> HandleCommandAsync(CommandArgs args) { return Task.FromResult(new CommandResult { Text = OnHandle(args).Trim() }); } protected abstract string OnHandle(CommandArgs args); } internal class RemoveAdmin : RconCommand { public override string Command => "removeAdmin"; public override string Description => "Removes a player from the admin list. Usage: removeAdmin <steamId>"; protected override string OnHandle(CommandArgs args) { string text = args.GetString(0); ZNet.instance.m_adminList.Remove(text); return text + " removed from admins"; } } internal class RemovePermitted : RconCommand { public override string Command => "removePermitted"; public override string Description => "Removes a player from the permitted list. Usage: removePermitted <steamId>"; protected override string OnHandle(CommandArgs args) { string text = args.GetString(0); ZNet.instance.m_permittedList.Remove(text); return text + " removed from permitted"; } } internal class SayChat : RconCommand { public override string Command => "say"; public override string Description => "Sends a message to the chat as a shout. Usage: say <message>"; protected override string OnHandle(CommandArgs args) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) string text = args.ToString(); Vector3 val = default(Vector3); if (!ZoneSystem.instance.GetLocationIcon(Game.instance.m_StartLocation, ref val)) { ((Vector3)(ref val))..ctor(0f, 30f, 0f); } ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ChatMessage", new object[4] { val, 2, Plugin.CommandsUserInfo, text }); return "Sent to chat - " + text; } } internal class SayPing : RconCommand { public override string Command => "ping"; public override string Description => "Sends a ping message to all players at the specified coordinates. Usage: ping <x> <y> <z>"; protected override string OnHandle(CommandArgs args) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) Vector3 vector = args.GetVector3(0); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ChatMessage", new object[4] { vector, 3, Plugin.CommandsUserInfo, "" }); return "Ping sent to " + vector.ToDisplayFormat(); } } internal class ServerLogs : IRconCommand { private const int DefaultLinesToDisplay = 5; private readonly StringBuilder _builder = new StringBuilder(); public string Command => "logs"; public string Description => "Get the server logs <optional> -lines <number>"; public Task<CommandResult> HandleCommandAsync(CommandArgs args) { string text = Path.Combine(Paths.BepInExRootPath, "LogOutput.log"); if (!File.Exists(text)) { return Task.FromResult(CommandResult.WithText("No logs")); } int num = 5; foreach (var optionalArgument in args.GetOptionalArguments()) { if (optionalArgument.Argument == "lines") { num = args.GetInt(optionalArgument.Index + 1); } } string text2 = Path.Combine(Paths.CachePath, "LogOutput.log"); File.Copy(text, text2, overwrite: true); string[] array = File.ReadAllLines(text2); int num2 = Math.Max(array.Length - num - 1, 0); _builder.Clear(); for (int i = num2; i < array.Length; i++) { _builder.AppendLine(array[i]); } return Task.FromResult(new CommandResult { Text = _builder.ToString().Trim(new char[1] { '\n' }), AttachedFilePath = text2 }); } } internal class ShowMessage : RconCommand { public override string Command => "showMessage"; public override string Description => "Displays a message in the center of the screen for all players. Usage: showMessage <message>"; protected override string OnHandle(CommandArgs args) { string text = args.ToString(); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ShowMessage", new object[2] { 2, text }); return "Message sent - " + text; } } internal class SpawnObject : RconCommand { public override string Command => "spawn"; public override string Description => "Creates the specified number of objects at the given position. Usage (with optional arguments): spawn <prefabName> <x> <y> <z> -count(-c) <count> -radius(-rad) <radius> -level(-l) <level> -rotation(-rot) <x> <y> <z> -tag(-t) <tag> -tamed "; protected override string OnHandle(CommandArgs args) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Unknown result type (might be due to invalid IL or missing references) //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_020c: Unknown result type (might be due to invalid IL or missing references) //IL_0297: Unknown result type (might be due to invalid IL or missing references) //IL_029e: Unknown result type (might be due to invalid IL or missing references) //IL_02a3: Unknown result type (might be due to invalid IL or missing references) //IL_02b1: Unknown result type (might be due to invalid IL or missing references) //IL_02b2: Unknown result type (might be due to invalid IL or missing references) //IL_02b4: Unknown result type (might be due to invalid IL or missing references) //IL_02b9: Unknown result type (might be due to invalid IL or missing references) //IL_02bd: Unknown result type (might be due to invalid IL or missing references) //IL_02bf: Unknown result type (might be due to invalid IL or missing references) string text = args.GetString(0); Vector3 vector = args.GetVector3(1); int num = 1; int num2 = 0; string text2 = string.Empty; Quaternion val = Quaternion.identity; float num3 = 0f; bool flag = false; foreach (var (num4, text3) in args.GetOptionalArguments()) { switch (text3) { case "-level": case "-l": num2 = args.GetInt(num4 + 1); break; case "-count": case "-c": num = args.GetInt(num4 + 1); break; case "-t": case "-tag": text2 = args.GetString(num4 + 1); break; case "-rot": case "-rotation": val = Quaternion.Euler(args.GetVector3(num4 + 1)); break; case "-rad": case "-radius": num3 = args.GetFloat(num4 + 1); break; case "-tamed": flag = true; break; default: return "Unknown argument: " + text3; } } GameObject prefab = ZNetScene.instance.GetPrefab(text); if ((Object)(object)prefab == (Object)null) { return "Prefab " + text + " not found"; } if (num <= 0) { return "Nothing to spawn"; } List<ZDO> list = new List<ZDO>(num); Character val4 = default(Character); ItemDrop val5 = default(ItemDrop); for (int i = 0; i < num; i++) { ZNetView.StartGhostInit(); Vector3 val2 = Random.onUnitSphere * num3; val2.y = 0f; Vector3 val3 = vector + val2; GameObject obj = Object.Instantiate<GameObject>(prefab, val3, val); if (obj.TryGetComponent<Character>(ref val4)) { val4.SetLevel(num2); } ZDO zDO = obj.GetComponent<ZNetView>().GetZDO(); if (obj.TryGetComponent<ItemDrop>(ref val5) && num2 >= 1) { val5.SetQuality(num2); ItemDrop.SaveToZDO(val5.m_itemData, zDO, -1); } list.Add(zDO); if (!string.IsNullOrEmpty(text2)) { zDO.SetTag(text2); } if (flag) { zDO.Set(ZDOVars.s_tamed, true); } ZNetView.FinishGhostInit(); Object.Destroy((Object)(object)obj); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"Spawned {num} objects:"); foreach (ZDO item in list) { stringBuilder.Append('-'); ZDOInfoUtil.AppendInfo(item, stringBuilder); stringBuilder.AppendLine(); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } } internal class ServerTimeCommand : RconCommand { public override string Command => "time"; public override string Description => "Get the current server time and day."; protected override string OnHandle(CommandArgs args) { double timeSeconds = ZNet.instance.GetTimeSeconds(); int currentDay = EnvMan.instance.GetCurrentDay(); return $"Current server time: {timeSeconds} sec. Day: {currentDay}"; } } internal class Unban : RconCommand { public override string Command => "unban"; public override string Description => "Unban a user from the server. Usage: unban <playername or steamid>"; protected override string OnHandle(CommandArgs args) { string text = args.GetString(0); ZNet.instance.Unban(text); return text + " unbanned"; } } internal class WorldSave : RconCommand { public override string Command => "save"; public override string Description => "Save the current world state."; protected override string OnHandle(CommandArgs args) { ZNet.instance.Save(false, false, false); return "World save started"; } } } namespace ValheimRcon.Commands.Container { public abstract class ContainerRconCommand : RconCommand { protected override string OnHandle(CommandArgs args) { //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Expected O, but got Unknown //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Expected O, but got Unknown ObjectId objectId = args.GetObjectId(0); ZDO val = Enumerable.FirstOrDefault(predicate: new IdCriteria(objectId).IsMatch, source: ZDOMan.instance.m_objectsByID.Values); if (val == null) { return "No object found with the specified ID."; } int prefab = val.GetPrefab(); string prefabName = val.GetPrefabName(); GameObject prefab2 = ZNetScene.instance.GetPrefab(prefab); if ((Object)(object)prefab2 == (Object)null) { return "Failed to load prefab: " + prefabName; } Container componentInChildren = prefab2.GetComponentInChildren<Container>(); if ((Object)(object)componentInChildren == (Object)null) { return "Object " + prefabName + " is not a container."; } byte[] byteArray = val.GetByteArray(ZDOVars.s_items, (byte[])null); Inventory val2 = new Inventory(componentInChildren.m_name, componentInChildren.m_bkg, componentInChildren.m_width, componentInChildren.m_height); if (byteArray != null) { ZPackage val3 = new ZPackage(byteArray); val2.Load(val3); } return HandleInventory(args, val, val2, prefabName); } protected abstract string HandleInventory(CommandArgs args, ZDO zdo, Inventory inventory, string prefabName); protected void SaveInventory(ZDO zdo, Inventory inventory) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); inventory.Save(val); zdo.Set(ZDOVars.s_items, val.GetArray()); zdo.SetZdoModified(); } } internal class ShowContainerInventory : ContainerRconCommand { public override string Command => "showContainer"; public override string Description => "Shows inventory contents of a container by object ID. Usage: showContainer <id:userid>"; protected override string HandleInventory(CommandArgs args, ZDO zdo, Inventory inventory, string prefabName) { if (inventory.NrOfItems() == 0) { return "Container is empty."; } StringBuilder stringBuilder = new StringBuilder(); ZDOInfoUtil.AppendInfo(zdo, stringBuilder); stringBuilder.AppendLine(); stringBuilder.AppendFormat("Items ({0}):", inventory.NrOfItems()); List<ItemData> allItems = inventory.GetAllItems(); int num = 0; foreach (ItemData item in allItems) { stringBuilder.AppendLine(); GameObject dropPrefab = item.m_dropPrefab; string arg = ((dropPrefab != null) ? ((Object)dropPrefab).name : null) ?? item.m_shared.m_name; stringBuilder.AppendFormat("[{0}] {1} ", num, arg); ZDOInfoUtil.AppendItemInfo(item, stringBuilder); num++; } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } } internal class AddItemToContainer : ContainerRconCommand { public override string Command => "addItemToContainer"; public override string Description => "Adds an item to a container's inventory. Usage: addItemToContainer <id:userid> <item_name> -count <count> -quality <quality> -variant <variant> -durability <durability> -data <key> <value> -nocrafter -force"; protected override string HandleInventory(CommandArgs args, ZDO zdo, Inventory inventory, string prefabName) { string text = args.GetString(1); GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(text); if ((Object)(object)itemPrefab == (Object)null) { return "Cannot find item prefab: " + text; } ItemData itemData = itemPrefab.GetComponent<ItemDrop>().m_itemData; SharedData shared = itemData.m_shared; int num = 1; int num2 = 1; int num3 = 0; bool flag = true; string crafterName = Plugin.ServerChatName.Value; long crafterID = -1L; Dictionary<string, string> dictionary = new Dictionary<string, string>(); bool force = false; float? num4 = null; foreach (var (num5, text2) in args.GetOptionalArguments()) { switch (text2) { case "-count": num = args.GetInt(num5 + 1); if (num < 1) { return "Count must be at least 1"; } break; case "-quality": num2 = args.GetInt(num5 + 1); if (num2 < 0) { return "Quality must be at least 0"; } break; case "-variant": num3 = args.GetInt(num5 + 1); if (num3 < 0) { return "Variant must be at least 0"; } if (num3 > 0 && shared.m_variants == 0) { return "Item " + text + " does not have variants"; } if (num3 > shared.m_variants - 1) { return $"Item {text} has only {shared.m_variants} variants"; } break; case "-nocrafter": flag = false; crafterID = 0L; crafterName = string.Empty; break; case "-data": { string key = args.GetString(num5 + 1); string value = args.TryGetString(num5 + 2); dictionary[key] = value; break; } case "-durability": num4 = args.GetFloat(num5 + 1); if (num4.Value < 0f) { return "Durability must be at least 0"; } break; case "-force": force = true; break; default: return "Unknown argument: " + text2; } } if (!ContainerUtils.ValidateContainerModification(zdo, prefabName, force, out var error)) { return error; } ItemData val = itemData.Clone(); val.m_quality = num2; val.m_variant = num3; val.m_stack = num; if (flag) { val.m_crafterID = crafterID; val.m_crafterName = crafterName; } else { val.m_crafterID = 0L; val.m_crafterName = string.Empty; } val.m_customData = dictionary; if (shared.m_useDurability) { val.m_durability = (num4.HasValue ? num4.Value : val.GetMaxDurability()); } if (inventory.AddItem(val)) { SaveInventory(zdo, inventory); return $"Item {text} x{num} added to {prefabName}"; } return "Failed to add item to " + prefabName; } } internal class RemoveItemFromContainer : ContainerRconCommand { public override string Command => "removeItemFromContainer"; public override string Description => "Removes an item from a container's inventory. Usage: removeItemFromContainer <id:userid> -index <index> -item <name> -count <count> -force"; protected override string HandleInventory(CommandArgs args, ZDO zdo, Inventory inventory, string prefabName) { bool force = false; int num = 1; int? num2 = null; string text = null; foreach (var (num3, text2) in args.GetOptionalArguments()) { switch (text2) { case "-index": num2 = args.GetInt(num3 + 1); if (num2.Value < 0) { return "Index must be at least 0"; } break; case "-item": text = args.GetString(num3 + 1); break; case "-count": num = args.GetInt(num3 + 1); if (num < 1) { return "Count must be at least 1"; } break; case "-force": force = true; break; default: return "Unknown argument " + text2; } } if (!ContainerUtils.ValidateContainerModification(zdo, prefabName, force, out var error)) { return error; } if (!num2.HasValue && string.IsNullOrEmpty(text)) { return "Either -index or -item must be specified."; } if (num2.HasValue && !string.IsNullOrEmpty(text)) { return "Cannot specify both -index and -item. Use only one."; } if (num2.HasValue) { List<ItemData> allItems = inventory.GetAllItems(); if (num2.Value >= allItems.Count) { return $"Index {num2.Value} is out of range. Container has {allItems.Count} items (indices 0-{allItems.Count - 1})."; } ItemData val = allItems[num2.Value]; GameObject dropPrefab = val.m_dropPrefab; string text3 = ((dropPrefab != null) ? ((Object)dropPrefab).name : null) ?? val.m_shared.m_name; int num4 = Math.Min(num, val.m_stack); inventory.RemoveItem(val, num4); SaveInventory(zdo, inventory); return $"Removed {text3} x{num4} from {prefabName} (index {num2.Value})"; } GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(text); if ((Object)(object)itemPrefab == (Object)null) { return "Cannot find item prefab: " + text; } string name = itemPrefab.GetComponent<ItemDrop>().m_itemData.m_shared.m_name; if (!inventory.ContainsItemByName(name)) { return "Item " + text + " not found in " + prefabName; } inventory.RemoveItem(name, num, -1, true); SaveInventory(zdo, inventory); return $"Removed {name} x{num} from {prefabName}"; } } internal class ClearContainerInventory : ContainerRconCommand { public override string Command => "clearContainer"; public override string Description => "Clears all items from a container's inventory. Usage: clearContainer <id:userid> -force"; protected override string HandleInventory(CommandArgs args, ZDO zdo, Inventory inventory, string prefabName) { bool force = false; foreach (var optionalArgument in args.GetOptionalArguments()) { string item = optionalArgument.Argument; if (item == "-force") { force = true; continue; } return "Unknown argument " + item; } if (!ContainerUtils.ValidateContainerModification(zdo, prefabName, force, out var error)) { return error; } inventory.RemoveAll(); SaveInventory(zdo, inventory); return "Cleared inventory of " + prefabName; } } public static class ContainerUtils { public static bool ValidateContainerModification(ZDO zdo, string prefabName, bool force, out string error) { if (!zdo.Persistent) { error = "Object is not persistent and cannot be modified."; return false; } if (!ZdoUtils.CanModifyZdo(zdo)) { error = "Object cannot be modified."; return false; } if (zdo.GetInt(ZDOVars.s_inUse, 0) == 1) { error = "Container " + prefabName + " is currently in use and cannot be modified."; return false; } long owner = zdo.GetOwner(); ZNetPeer peer = ZNet.instance.GetPeer(owner); if (peer != null && !force) { error = "Container " + prefabName + " is owned by an online player " + peer.GetPlayerInfo() + " and cannot be modified. Use -force to override."; return false; } error = string.Empty; return true; } } } namespace ValheimRcon.Commands.Search { public interface ISearchCriteria { bool IsMatch(ZDO zdo); } public struct PrefabCriteria : ISearchCriteria { private readonly int _prefabHash; public PrefabCriteria(string prefab) { _prefabHash = StringExtensionMethods.GetStableHashCode(prefab); } public bool IsMatch(ZDO zdo) { return zdo.GetPrefab() == _prefabHash; } } public struct CreatorCriteria : ISearchCriteria { private readonly long _creatorId; public CreatorCriteria(long creatorId) { _creatorId = creatorId; } public bool IsMatch(ZDO zdo) { return zdo.GetLong(ZDOVars.s_creator, 0L) == _creatorId; } } public struct IdCriteria : ISearchCriteria { private readonly uint _id; private readonly long _userId; public IdCriteria(ObjectId objectId) { _id = objectId.Id; _userId = objectId.UserId; } public bool IsMatch(ZDO zdo) { if (((ZDOID)(ref zdo.m_uid)).ID == _id) { return