Decompiled source of ValheimRcon v1.5.1
ValheimRcon.dll
Decompiled 2 weeks 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)