Please disclose if your mod was created primarily 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.5.1
ValheimRcon.dll
Decompiled 3 months 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.5.1")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.5.1.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); } } } [CompilerGenerated] private sealed class <DelayedActionCoroutine>d__6 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public float delay; public Action action; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <DelayedActionCoroutine>d__6(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(delay); <>1__state = 1; return true; case 1: <>1__state = -1; action?.Invoke(); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } 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); } [IteratorStateMachine(typeof(<DelayedActionCoroutine>d__6))] internal static IEnumerator DelayedActionCoroutine(float delay, Action action) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <DelayedActionCoroutine>d__6(0) { delay = delay, action = action }; } } 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 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.5.1")] 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_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0075: 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; val = default(PlayerInfo); val.m_name = value; val.m_userInfo = new CrossNetworkUserInfo { m_displayName = value, m_id = CommandsUserInfo.UserId }; val.m_serverAssignedDisplayName = value; PlayerInfo item = val; __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.5.1"; 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)1), "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 @string = zdo.GetString(ZDOVars.s_ownerName, ""); long @long = zdo.GetLong(ZDOVars.s_owner, 0L); stringBuilder.Append("Owner: "); if (string.IsNullOrEmpty(@string)) { stringBuilder.Append("<not claimed>"); } else { stringBuilder.AppendFormat("{0}({1})", @string, @long); } } } 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 @float = zdo.GetFloat(ZDOVars.s_maxHealth, 0f); stringBuilder.Append(" Health: " + zdo.GetFloat(ZDOVars.s_health, @float).ToDisplayFormat() + "/" + @float.ToDisplayFormat()); stringBuilder.Append($" Tamed: {zdo.GetBool(ZDOVars.s_tamed, false)}"); string @string = zdo.GetString(ZDOVars.s_tamedName, ""); string string2 = zdo.GetString(ZDOVars.s_tamedNameAuthor, ""); if (!string.IsNullOrEmpty(@string)) { stringBuilder.AppendFormat(" Name: {0} (author: {1})", @string, string2); } } } 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_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown TempInventory.RemoveAll(); string @string = zdo.GetString(ZDOVars.s_items, ""); if (!string.IsNullOrEmpty(@string)) { ZPackage val = new ZPackage(@string); 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> { [CompilerGenerated] private sealed class <GetPermittedPlayers>d__1 : IEnumerable<(long Id, string Name)>, IEnumerable, IEnumerator<(long Id, string Name)>, IDisposable, IEnumerator { private int <>1__state; private (long Id, string Name) <>2__current; private int <>l__initialThreadId; private ZDO zdo; public ZDO <>3__zdo; private int <count>5__2; private int <i>5__3; (long, string) IEnumerator<(long, string)>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <GetPermittedPlayers>d__1(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <count>5__2 = zdo.GetInt(ZDOVars.s_permitted, 0); if (<count>5__2 <= 0) { return false; } <i>5__3 = 0; break; case 1: <>1__state = -1; <i>5__3++; break; } if (<i>5__3 < <count>5__2) { long @long = zdo.GetLong($"pu_id{<i>5__3}", 0L); string @string = zdo.GetString($"pu_name{<i>5__3}", ""); <>2__current = (@long, @string); <>1__state = 1; return true; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator<(long Id, string Name)> IEnumerable<(long, string)>.GetEnumerator() { <GetPermittedPlayers>d__1 <GetPermittedPlayers>d__; if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; <GetPermittedPlayers>d__ = this; } else { <GetPermittedPlayers>d__ = new <GetPermittedPlayers>d__1(0); } <GetPermittedPlayers>d__.zdo = <>3__zdo; return <GetPermittedPlayers>d__; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<(long, string)>)this).GetEnumerator(); } } 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}"); } } [IteratorStateMachine(typeof(<GetPermittedPlayers>d__1))] private static IEnumerable<(long Id, string Name)> GetPermittedPlayers(ZDO zdo) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <GetPermittedPlayers>d__1(-2) { <>3__zdo = zdo }; } } 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); 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++) { string @string = zdo.GetString($"{i}_item", ""); if (!string.IsNullOrEmpty(@string)) { if (flag) { stringBuilder.Append(','); } if (detailed) { ItemDrop.LoadFromZDO(i, _tempData, zdo); stringBuilder.AppendFormat("( {0} ", @string); ZDOInfoUtil.AppendItemInfo(_tempData, stringBuilder); stringBuilder.Append(')'); } else { stringBuilder.Append(@string); } 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) { string @string = zdo.GetString(ZDOVars.s_item, ""); stringBuilder.AppendFormat(" Attached item: "); if (string.IsNullOrEmpty(@string)) { stringBuilder.Append("<empty>"); return; } stringBuilder.Append(@string); if (detailed) { stringBuilder.Append(' '); ItemDrop.LoadFromZDO(_tempData, zdo); 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 @string = zdo.GetString(ZDOVars.s_tag, ""); string string2 = zdo.GetString(ZDOVars.s_tagauthor, ""); stringBuilder.AppendFormat("Portal tag: {0} (author {1})", @string, string2); } } internal class SignZDOInfoProvider : ZDOInfoProviderBase<Sign> { public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { string @string = zdo.GetString(ZDOVars.s_text, ""); string string2 = zdo.GetString(ZDOVars.s_author, ""); if (!string.IsNullOrEmpty(@string)) { stringBuilder.AppendFormat("Text: {0} (author: {1})", @string, string2); } } } internal class TombStoneZDOInfoProvider : ZDOInfoProviderBase<TombStone> { public override void AppendInfo(ZDO zdo, StringBuilder stringBuilder, bool detailed) { string @string = zdo.GetString(ZDOVars.s_ownerName, ""); long @long = zdo.GetLong(ZDOVars.s_owner, 0L); stringBuilder.AppendFormat("Tombstone: {0}({1})", @string, @long); } } 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<IPNetwork> _whiteList = new List<IPNetwork>(); private List<IPNetwork> _blackList = new List<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(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<IPNetwork> list) { foreach (IPNetwork item in list) { if (IPNetwork.Contains(item, address)) { return true; } } return false; } private static bool ParseConfigs(IEnumerable<string> configs, out IReadOnlyCollection<IPNetwork> validNetworks) { List<IPNetwork> list = (List<IPNetwork>)(validNetworks = new List<IPNetwork>()); if (configs == null) { return true; } bool result = true; foreach (string config in configs) { try { IPNetwork item = Parse(config.Trim()); list.Add(item); } catch (Exception arg) { Log.Error($"Cannot parse config {config} - {arg}"); } } return result; } private static 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 IPNetwork.Parse(ipaddress, cidr); } if (array.Length == 1) { return 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 @string = args.GetString(0); ZNet.instance.m_adminList.Add(@string); return @string + " 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 @string = args.GetString(0); ZNet.instance.m_permittedList.Add(@string); return @string + " 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 @string = args.GetString(0); ZNet.instance.Ban(@string); return "Banned " + @string; } } public class CommandArgs { [CompilerGenerated] private sealed class <GetOptionalArguments>d__17 : IEnumerable<(int Index, string Argument)>, IEnumerable, IEnumerator<(int Index, string Argument)>, IDisposable, IEnumerator { private int <>1__state; private (int Index, string Argument) <>2__current; private int <>l__initialThreadId; public CommandArgs <>4__this; private int <i>5__2; (int, string) IEnumerator<(int, string)>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <GetOptionalArguments>d__17(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { int num = <>1__state; CommandArgs commandArgs = <>4__this; if (num != 0) { if (num != 1) { return false; } <>1__state = -1; goto IL_0076; } <>1__state = -1; <i>5__2 = 0; goto IL_0086; IL_0076: <i>5__2++; goto IL_0086; IL_0086: if (<i>5__2 < commandArgs.Arguments.Count) { if (OptionalArgumentRegex.IsMatch(commandArgs.Arguments[<i>5__2])) { <>2__current = (<i>5__2, commandArgs.Arguments[<i>5__2]); <>1__state = 1; return true; } goto IL_0076; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator<(int Index, string Argument)> IEnumerable<(int, string)>.GetEnumerator() { <GetOptionalArguments>d__17 result; if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; result = this; } else { result = new <GetOptionalArguments>d__17(0) { <>4__this = <>4__this }; } return result; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<(int, string)>)this).GetEnumerator(); } } 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; } [IteratorStateMachine(typeof(<GetOptionalArguments>d__17))] public IEnumerable<(int Index, string Argument)> GetOptionalArguments() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <GetOptionalArguments>d__17(-2) { <>4__this = this }; } 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 @string = args.GetString(index); string[] array = @string.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 " + @string + " as object id (expected format is ID:User)"); } } public struct CommandResult { public string Text; public string AttachedFilePath; public static CommandResult WithText(string text) { CommandResult result = default(CommandResult); result.Text = text; return result; } } 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 zdo) => _criterias.All((ISearchCriteria c) => c.IsMatch(zdo))).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 zdo2 in array2) { stringBuilder.Append('-'); ZDOInfoUtil.AppendInfo(zdo2, 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 valueOrDefault = (ZDOMan.instance?.m_objectsByID?.Count).GetValueOrDefault(-1); EnvMan instance = EnvMan.instance; int num3; if (instance == null) { num3 = -1; } else { ZNet instance2 = ZNet.instance; num3 = instance.GetDay((instance2 != null) ? instance2.GetTimeSeconds() : 0.0); } int num4 = num3; int num5 = ZDOMan.instance?.m_deadZDOs.Count ?? 0; int num6 = ToMegabytes(Profiler.GetMonoUsedSizeLong()); int num7 = ToMegabytes(Profiler.GetMonoHeapSizeLong()); builder.AppendLine($"Stats - Online {num} FPS {num2:0.0}"); builder.AppendLine($"Memory - Mono {num6}MB, Heap {num7}MB"); builder.Append($"World - Day {num4}, Objects {valueOrDefault}, Dead objects {num5}"); 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 @string = args.GetString(0); ZNet.instance.Kick(@string); return "Kicked " + @string; } } 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) { CommandResult result = default(CommandResult); result.Text = OnHandle(args).Trim(); return Task.FromResult(result); } 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 @string = args.GetString(0); ZNet.instance.m_adminList.Remove(@string); return @string + " 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 @string = args.GetString(0); ZNet.instance.m_permittedList.Remove(@string); return @string + " 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_004c: 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(ZRoutedRpc.Everybody, "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_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) Vector3 vector = args.GetVector3(0); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[4] { vector, 3, Plugin.CommandsUserInfo, "" }); return "Ping sent to " + vector.ToDisplayFormat(); } } internal class ServerLogs : IRconCommand { private const int MaxLinesToDisplay = 5; private readonly StringBuilder _builder = new StringBuilder(); public string Command => "logs"; public string Description => "Get the server logs"; 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")); } string text2 = Path.Combine(Paths.CachePath, "LogOutput.log"); File.Copy(text, text2, overwrite: true); string[] array = File.ReadAllLines(text2); int num = Math.Max(array.Length - 5 - 1, 0); _builder.Clear(); for (int i = num; i < array.Length; i++) { _builder.AppendLine(array[i]); } CommandResult result = default(CommandResult); result.Text = _builder.ToString().Trim(new char[1] { '\n' }); result.AttachedFilePath = text2; return Task.FromResult(result); } } 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(ZRoutedRpc.Everybody, "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 @string = args.GetString(0); Vector3 vector = args.GetVector3(1); int num = 1; int num2 = 0; string text = string.Empty; Quaternion val = Quaternion.identity; float num3 = 0f; bool flag = false; foreach (var (num4, text2) in args.GetOptionalArguments()) { switch (text2) { case "-level": case "-l": num2 = args.GetInt(num4 + 1); break; case "-count": case "-c": num = args.GetInt(num4 + 1); break; case "-t": case "-tag": text = 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: " + text2; } } GameObject prefab = ZNetScene.instance.GetPrefab(@string); if ((Object)(object)prefab == (Object)null) { return "Prefab " + @string + " 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); } if (obj.TryGetComponent<ItemDrop>(ref val5)) { val5.SetQuality(num2); } ZDO zDO = obj.GetComponent<ZNetView>().GetZDO(); list.Add(zDO); if (!string.IsNullOrEmpty(text)) { zDO.SetTag(text); } 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 @string = args.GetString(0); ZNet.instance.Unban(@string); return @string + " 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_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Expected O, but got Unknown //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00df: 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."; } string @string = val.GetString(ZDOVars.s_items, ""); Inventory val2 = new Inventory(componentInChildren.m_name, componentInChildren.m_bkg, componentInChildren.m_width, componentInChildren.m_height); if (!string.IsNullOrEmpty(@string)) { ZPackage val3 = new ZPackage(@string); 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.GetBase64()); 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 @string = args.GetString(1); GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(@string); if ((Object)(object)itemPrefab == (Object)null) { return "Cannot find item prefab: " + @string; } 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, text) in args.GetOptionalArguments()) { switch (text) { 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 " + @string + " does not have variants"; } if (num3 > shared.m_variants - 1) { return $"Item {@string} has only {shared.m_variants} variants"; } break; case "-nocrafter": flag = false; crafterID = 0L; crafterName = string.Empty; break; case "-data": { string string2 = args.GetString(num5 + 1); string value = args.TryGetString(num5 + 2); dictionary[string2] = 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: " + text; } } 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)