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 Splatform;
using UnityEngine;
using UnityEngine.Profiling;
using ValheimRcon.Commands;
using ValheimRcon.Core;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[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.0.1")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.1.0")]
[module: UnverifiableCode]
internal static class Log
{
private static ManualLogSource _instance;
internal 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}";
}
}
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_0029: Unknown result type (might be due to invalid IL or missing references)
//IL_002f: Expected O, but got Unknown
if ((Object)(object)_instance == (Object)null)
{
GameObject val = new GameObject("MainThreadDispatcher", new Type[1] { typeof(MainThreadDispatcher) });
Object.DontDestroyOnLoad((Object)(object)val);
_instance = val.GetComponent<MainThreadDispatcher>();
}
return _instance;
}
public void AddAction(Action action)
{
_queue.Enqueue(action);
}
public void AddCoroutine(IEnumerator coroutine)
{
_coroutinesQueue.Enqueue(coroutine);
}
private void Update()
{
Action result;
while (_queue.Count > 0 && _queue.TryDequeue(out result))
{
result?.Invoke();
}
IEnumerator result2;
while (_coroutinesQueue.Count > 0 && _coroutinesQueue.TryDequeue(out result2))
{
((MonoBehaviour)this).StartCoroutine(result2);
}
}
}
internal static IDisposable RunPeriodical(Action action, int periodMilliseconds)
{
return new Timer(delegate
{
action?.Invoke();
}, null, 0, periodMilliseconds);
}
internal static IDisposable RunPeriodicalInSingleThread(Action action, int periodMilliseconds)
{
Thread thread = new Thread((ParameterizedThreadStart)delegate
{
while (true)
{
action?.Invoke();
Thread.Sleep(periodMilliseconds);
}
});
thread.Start();
return new DisposableThread(thread);
}
internal static void RunInMainThread(Action action)
{
MainThreadDispatcher.GetInstante().AddAction(action);
}
internal static void RunCoroutine(IEnumerator coroutine)
{
MainThreadDispatcher.GetInstante().AddCoroutine(coroutine);
}
internal static void RunDelayed(float delay, Action action)
{
MainThreadDispatcher.GetInstante().AddCoroutine(DelayedActionCoroutine(delay, action));
}
internal static IDisposable RunThread(Action action)
{
Thread thread = new Thread(action.Invoke);
thread.Start();
return new DisposableThread(thread);
}
internal static IEnumerator DelayedActionCoroutine(float delay, Action action)
{
yield return (object)new WaitForSeconds(delay);
action?.Invoke();
}
}
namespace ValheimRcon
{
public interface IRconCommand
{
string Command { get; }
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);
StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream());
string result = streamReader.ReadToEnd();
httpWebResponse.Close();
fileStream.Dispose();
httpWebResponse.Dispose();
return result;
}
}
internal class DiscordService : IDisposable
{
private struct Message
{
public string text;
public string filePath;
}
private const string Name = "RCON";
private readonly IDisposable _thread;
private readonly string _webhook;
private readonly ConcurrentQueue<Message> _queue = new ConcurrentQueue<Message>();
public DiscordService(string webhook)
{
_webhook = webhook;
_thread = ThreadingUtil.RunPeriodicalInSingleThread(SendQueuedMessage, 333);
}
public void SendResult(string text, string filePath)
{
_queue.Enqueue(new Message
{
filePath = filePath,
text = text
});
}
private void SendQueuedMessage()
{
if (string.IsNullOrEmpty(_webhook) || !_queue.TryDequeue(out var result))
{
return;
}
try
{
string filePath = result.filePath;
string text = (string.IsNullOrEmpty(filePath) ? Discord.Send(result.text, "RCON", _webhook) : Discord.SendFile(result.text, Path.GetFileName(filePath), Path.GetExtension(filePath), filePath, "RCON", _webhook));
Log.Debug("Sent to discord " + result.text);
}
catch (Exception arg)
{
Log.Error($"Cannot send to discord {result.text}\n{arg}");
}
}
public void Dispose()
{
_thread.Dispose();
}
}
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_0014: 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();
}
}
[BepInProcess("valheim_server.exe")]
[BepInPlugin("org.tristan.rcon", "Valheim Rcon", "1.0.1")]
public class Plugin : BaseUnityPlugin
{
[HarmonyPatch]
private class Patches
{
[HarmonyFinalizer]
[HarmonyPatch(typeof(ZNet), "UpdatePlayerList")]
private static void ZNet_UpdatePlayerList(ZNet __instance)
{
//IL_0006: Unknown result type (might be due to invalid IL or missing references)
//IL_0038: Unknown result type (might be due to invalid IL or missing references)
//IL_004a: 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_0069: Unknown result type (might be due to invalid IL or missing references)
//IL_006b: Unknown result type (might be due to invalid IL or missing references)
//IL_0078: Unknown result type (might be due to invalid IL or missing references)
//IL_0079: Unknown result type (might be due to invalid IL or missing references)
//IL_0080: 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);
}
}
}
public const string Guid = "org.tristan.rcon";
public const string Name = "Valheim Rcon";
public const string Version = "1.0.1";
public static ConfigEntry<string> DiscordUrl;
public static ConfigEntry<string> Password;
public static ConfigEntry<int> Port;
public static ConfigEntry<string> ServerChatName;
private string _logsPath;
private DiscordService _discordService;
private StringBuilder _builder = new StringBuilder();
public static readonly UserInfo CommandsUserInfo = new UserInfo
{
Name = string.Empty,
UserId = new PlatformUserID("Bot", 0uL, false)
};
private void Awake()
{
//IL_0101: Unknown result type (might be due to invalid IL or missing references)
//IL_010b: Expected O, but got Unknown
Log.CreateInstance(((BaseUnityPlugin)this).Logger);
_logsPath = Path.Combine(Paths.BepInExRootPath, "rcon_logs.log");
Port = ((BaseUnityPlugin)this).Config.Bind<int>("1. Rcon", "Port", 2458, "Port to receive RCON commands");
Password = ((BaseUnityPlugin)this).Config.Bind<string>("1. Rcon", "Password", System.Guid.NewGuid().ToString(), "Password for RCON packages validation");
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");
CommandsUserInfo.Name = ServerChatName.Value;
_discordService = new DiscordService(DiscordUrl.Value);
Object.DontDestroyOnLoad((Object)new GameObject("RconProxy", new Type[1] { typeof(RconProxy) }));
RconProxy.Instance.OnCommandCompleted += OnCommandCompleted;
RconCommandsUtil.RegisterAllCommands(Assembly.GetExecutingAssembly());
Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), (string)null);
}
private void OnDestroy()
{
_discordService.Dispose();
}
private void OnCommandCompleted(string command, IReadOnlyList<string> args, CommandResult result)
{
_builder.Clear();
_builder.AppendFormat("Command {0} {1}", command, string.Join(", ", args));
_builder.AppendLine();
_builder.Append(result.Text);
File.AppendAllText(_logsPath, _builder.ToString());
if (!string.IsNullOrEmpty(DiscordUrl.Value))
{
_discordService.SendResult(_builder.ToString(), result.AttachedFilePath);
}
}
}
public static class RconCommandsUtil
{
public const int MaxMessageLength = 800;
public static string Truncate(string message)
{
return (message.Length > 800) ? message.Substring(0, 800) : message;
}
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);
IEnumerable<Type> enumerable = from t in assembly.GetTypes()
where t != null
where !t.IsAbstract && t.IsClass
where t.GetConstructor(Type.EmptyTypes) != null
where t.GetInterfaces().Contains(commandInterfaceType)
select t;
foreach (Type item in enumerable)
{
RconProxy.Instance.RegisterCommand(item);
}
}
}
public class RconProxy : MonoBehaviour
{
internal delegate void CompletedCommandDelegate(string command, IReadOnlyList<string> args, CommandResult result);
[HarmonyPatch]
private class Patches
{
[HarmonyFinalizer]
[HarmonyPatch(typeof(ZNet), "LoadWorld")]
private static void ZNet_LoadWorld()
{
Instance._receiver.StartListening();
}
[HarmonyPrefix]
[HarmonyPatch(typeof(Game), "Shutdown")]
private static void Game_Shutdown()
{
Instance._receiver.Dispose();
}
}
private RconCommandReceiver _receiver;
private Dictionary<string, IRconCommand> _commands = new Dictionary<string, IRconCommand>();
public static RconProxy Instance { get; private set; }
internal event CompletedCommandDelegate OnCommandCompleted;
private void Awake()
{
Instance = this;
_receiver = new RconCommandReceiver(Plugin.Port.Value, Plugin.Password.Value, HandleCommandAsync);
}
private void Update()
{
_receiver.Update();
}
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, Func<CommandArgs, CommandResult> commandFunc)
{
RegisterCommand(new ActionCommand(command, commandFunc));
}
private async Task<string> HandleCommandAsync(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 + " - " + result.Text);
this.OnCommandCompleted?.Invoke(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 command))
{
resultSource.TrySetResult(new CommandResult
{
Text = "Unknown command " + commandName
});
}
else
{
resultSource.TrySetResult(await command.HandleCommandAsync(new CommandArgs(args)));
}
}
catch (Exception ex)
{
Exception e = ex;
resultSource.TrySetResult(new CommandResult
{
Text = e.Message
});
}
}
}
}
namespace ValheimRcon.Core
{
internal class AsynchronousSocketListener
{
internal delegate void MessageReceived(RconPeer peer, RconPacket package);
private static readonly TimeSpan UnauthorizedClientLifetime = TimeSpan.FromSeconds(30.0);
private readonly IPAddress _address;
private readonly int _port;
private readonly Socket _listener;
private readonly HashSet<RconPeer> _clients = new HashSet<RconPeer>();
private readonly HashSet<RconPeer> _waitingForDisconnect = new HashSet<RconPeer>();
private IDisposable _acceptThread;
internal event MessageReceived OnMessage;
public AsynchronousSocketListener(IPAddress ipAddress, int port)
{
_address = ipAddress;
_port = port;
_listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
}
public void StartListening()
{
Log.Message("Start listening rcon commands");
try
{
IPEndPoint localEP = new IPEndPoint(_address, _port);
_listener.Bind(localEP);
_listener.Listen(100);
_acceptThread = ThreadingUtil.RunPeriodicalInSingleThread(TryAcceptClientThread, 100);
}
catch (Exception msg)
{
Log.Error(msg);
}
}
public async Task SendAsync(RconPeer peer, RconPacket packet)
{
try
{
Socket socket = peer.socket;
byte[] byteData = packet.Serialize();
Log.Debug($"Sent {await SocketTaskExtensions.SendAsync(socket, new ArraySegment<byte>(byteData), SocketFlags.None)} bytes to client [{peer.Endpoint}]");
}
catch (Exception ex)
{
Exception e = ex;
Log.Error(e);
}
}
public void Update()
{
foreach (RconPeer client in _clients)
{
if (!IsConnected(client))
{
Disconnect(client);
}
else if (IsUnauthorizedTimeout(client))
{
Log.Warning("Unauthorized timeout [" + client.Endpoint + "]");
Disconnect(client);
}
else
{
TryReceive(client);
}
}
foreach (RconPeer item in _waitingForDisconnect)
{
_clients.Remove(item);
DisconnectPeer(item);
}
_waitingForDisconnect.Clear();
}
public void Close()
{
_listener.Close();
_acceptThread?.Dispose();
foreach (RconPeer client in _clients)
{
client.Dispose();
}
_waitingForDisconnect.Clear();
_clients.Clear();
}
public void Disconnect(RconPeer peer)
{
_waitingForDisconnect.Add(peer);
}
private void TryAcceptClientThread()
{
try
{
if (_listener.Poll(0, SelectMode.SelectRead))
{
Socket socket = _listener.Accept();
OnClientConnected(socket);
}
}
catch (Exception msg)
{
Log.Error(msg);
}
}
private void TryReceive(RconPeer peer)
{
Socket socket = peer.socket;
if (socket.Poll(0, SelectMode.SelectRead) && socket.Available > 0)
{
int num = socket.Receive(peer.Buffer);
if (num != 0)
{
OnPackageReceived(peer, num);
}
}
}
private void OnClientConnected(Socket socket)
{
RconPeer state = new RconPeer(socket);
Log.Debug("Client connected [" + state.Endpoint + "]");
ThreadingUtil.RunInMainThread(delegate
{
_clients.Add(state);
});
}
private void OnPackageReceived(RconPeer peer, int readCount)
{
Socket socket = peer.socket;
Log.Debug($"Got package from client, {readCount} bytes [{peer.Endpoint}]");
RconPacket rconPacket = new RconPacket(peer.Buffer);
Log.Debug($"Received package {rconPacket}");
this.OnMessage?.Invoke(peer, rconPacket);
Array.Clear(peer.Buffer, 0, peer.Buffer.Length);
}
private void DisconnectPeer(RconPeer peer)
{
Socket socket = peer.socket;
Log.Debug("Client disconnected [" + peer.Endpoint + "]");
peer.Dispose();
}
private static bool IsConnected(RconPeer peer)
{
Socket socket = peer.socket;
return socket.Connected && (!socket.Poll(0, SelectMode.SelectRead) || socket.Available != 0);
}
private static bool IsUnauthorizedTimeout(RconPeer peer)
{
return !peer.Authentificated && DateTime.Now - peer.created > UnauthorizedClientLifetime;
}
}
internal enum PacketType
{
Error = 0,
Command = 2,
Login = 3
}
internal delegate Task<string> RconCommandHandler(string command, IReadOnlyList<string> data);
internal class RconCommandReceiver : IDisposable
{
private static readonly Regex MatchRegex = new Regex("(?<=[ ][\\\"]|^[\\\"])[^\\\"]+(?=[\\\"][ ]|[\\\"]$)|(?<=[ ]|^)[^\\\" ]+(?=[ ]|$)");
private readonly AsynchronousSocketListener _socketListener;
private readonly string _password;
private RconCommandHandler _commandHandler;
public RconCommandReceiver(int port, string password, RconCommandHandler commandHandler)
{
_password = password;
_socketListener = new AsynchronousSocketListener(IPAddress.Any, port);
_socketListener.OnMessage += SocketListener_OnMessage;
_commandHandler = commandHandler;
}
public void StartListening()
{
_socketListener.StartListening();
}
public void Update()
{
_socketListener.Update();
}
public void Dispose()
{
_socketListener.Close();
}
private async void SocketListener_OnMessage(RconPeer peer, RconPacket packet)
{
_ = peer.socket;
switch (packet.type)
{
case PacketType.Login:
{
if (peer.Authentificated)
{
Log.Error("Already authorized [" + peer.Endpoint + "]");
await _socketListener.SendAsync(peer, new RconPacket(packet.requestId, PacketType.Command, "Already authorized"));
_socketListener.Disconnect(peer);
break;
}
bool success = string.Equals(packet.payload.Trim(), _password);
RconPacket result2;
if (success)
{
peer.SetAuthentificated(authentificated: true);
result2 = new RconPacket(packet.requestId, PacketType.Command, "Logic success");
}
else
{
result2 = new RconPacket(-1, PacketType.Command, "Login failed");
}
Log.Debug($"Login result {result2}");
await _socketListener.SendAsync(peer, result2);
if (!success)
{
_socketListener.Disconnect(peer);
}
break;
}
case PacketType.Command:
{
if (!peer.Authentificated)
{
Log.Warning("Not authorized [" + peer.Endpoint + "]");
await _socketListener.SendAsync(peer, new RconPacket(packet.requestId, packet.type, "Unauthorized"));
_socketListener.Disconnect(peer);
break;
}
string payload = packet.payload.TrimStart(new char[1] { '/' });
List<string> data = (from Match m in MatchRegex.Matches(payload)
select m.Value).ToList();
string command = data[0];
data.RemoveAt(0);
RconPacket result = new RconPacket(payload: await _commandHandler(command, data), requestId: packet.requestId, type: packet.type);
Log.Debug($"Command result {command} - {result}");
await _socketListener.SendAsync(peer, result);
break;
}
default:
Log.Error($"Unknown packet type: {packet} [{peer.Endpoint}]");
await _socketListener.SendAsync(peer, new RconPacket(packet.requestId, PacketType.Error, "Cannot handle command"));
_socketListener.Disconnect(peer);
break;
}
}
}
internal readonly struct RconPacket
{
public readonly int requestId;
public readonly PacketType type;
public readonly string payload;
public RconPacket(byte[] bytes)
{
using MemoryStream input = new MemoryStream(bytes);
using BinaryReader binaryReader = new BinaryReader(input);
int num = binaryReader.ReadInt32();
requestId = binaryReader.ReadInt32();
type = (PacketType)binaryReader.ReadInt32();
byte[] bytes2 = binaryReader.ReadBytes(num - 8 - 2);
payload = Encoding.UTF8.GetString(bytes2);
}
public RconPacket(int requestId, PacketType type, string payload)
{
this.requestId = requestId;
this.type = type;
this.payload = payload;
}
public byte[] Serialize()
{
using MemoryStream memoryStream = new MemoryStream();
using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
binaryWriter.Write(requestId);
binaryWriter.Write((int)type);
byte[] bytes = Encoding.UTF8.GetBytes(payload);
binaryWriter.Write(bytes);
binaryWriter.Write((byte)0);
binaryWriter.Write((byte)0);
byte[] array = memoryStream.ToArray();
memoryStream.Position = 0L;
binaryWriter.Write(array.Length);
binaryWriter.Write(array);
return memoryStream.ToArray();
}
public override string ToString()
{
return $"[{requestId} t:{type} {payload}]";
}
}
internal class RconPeer : IDisposable
{
internal const int BufferSize = 4096;
internal readonly byte[] Buffer = new byte[4096];
public readonly Socket socket;
public readonly DateTime created;
public bool Authentificated { get; private set; }
public string Endpoint => socket.RemoteEndPoint?.ToString() ?? string.Empty;
public RconPeer(Socket workSocket)
{
socket = workSocket;
created = DateTime.Now;
}
public void SetAuthentificated(bool authentificated)
{
Authentificated = authentificated;
}
public void Dispose()
{
socket.Close();
socket.Dispose();
}
}
}
namespace ValheimRcon.Commands
{
internal class ActionCommand : IRconCommand
{
private readonly Func<CommandArgs, CommandResult> _execute;
public string Command { get; }
public ActionCommand(string command, Func<CommandArgs, CommandResult> execute)
{
Command = command;
_execute = execute;
}
public Task<CommandResult> HandleCommandAsync(CommandArgs args)
{
return Task.FromResult(_execute(args));
}
}
internal class AddAdmin : RconCommand
{
public override string Command => "addAdmin";
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";
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";
protected override string OnHandle(CommandArgs args)
{
string @string = args.GetString(0);
ZNet.instance.Ban(@string);
return "Banned " + @string;
}
}
internal class BanSteamId : RconCommand
{
public override string Command => "banSteamId";
protected override string OnHandle(CommandArgs args)
{
string @string = args.GetString(0);
ZNet.instance.m_bannedList.Add(@string);
return @string + " banned";
}
}
public class CommandArgs
{
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 string GetString(int index)
{
ValidateIndex(index);
return Arguments[index];
}
private void ValidateIndex(int index)
{
if (index >= 0 && index < Arguments.Count)
{
return;
}
throw new ArgumentException($"Cannot get argument at {index}");
}
public override string ToString()
{
return string.Join(" ", Arguments);
}
}
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 Damage : PlayerRconCommand
{
public override string Command => "damage";
protected override string OnHandle(ZNetPeer peer, ZDO zdo, CommandArgs args)
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0006: Unknown result type (might be due to invalid IL or missing references)
//IL_000d: Unknown result type (might be due to invalid IL or missing references)
//IL_0014: 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_001d: Unknown result type (might be due to invalid IL or missing references)
//IL_0022: Unknown result type (might be due to invalid IL or missing references)
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_003a: 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)
//IL_0041: Expected O, but got Unknown
HitData val = new HitData
{
m_blockable = false,
m_dodgeable = false,
m_ignorePVP = true,
m_hitType = (HitType)0,
m_damage = new DamageTypes
{
m_damage = args.GetInt(1)
}
};
peer.InvokeRoutedRpcToZdo("RPC_Damage", val);
return $"{peer.GetPlayerInfo()} damaged to {val.m_damage.m_damage}hp";
}
}
internal class GetServerStats : RconCommand
{
private StringBuilder builder = new StringBuilder();
public override string Command => "serverStats";
protected override string OnHandle(CommandArgs args)
{
builder.Clear();
string text = ZNet.World?.m_name ?? "INVALID WORLD";
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 GiveItem : PlayerRconCommand
{
public override string Command => "give";
protected override string OnHandle(ZNetPeer peer, ZDO zdo, CommandArgs args)
{
//IL_0070: 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)
//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
string @string = args.GetString(1);
int @int = args.GetInt(2);
int int2 = args.GetInt(3);
GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(@string);
if ((Object)(object)itemPrefab == (Object)null)
{
return "Cannot find prefab " + @string;
}
ZNetView.StartGhostInit();
ItemData val = itemPrefab.GetComponent<ItemDrop>().m_itemData.Clone();
val.m_dropPrefab = itemPrefab;
val.m_quality = @int;
ItemDrop val2 = ItemDrop.DropItem(val, int2, peer.GetRefPos(), Quaternion.identity);
ZNetView.FinishGhostInit();
Object.Destroy((Object)(object)((Component)val2).gameObject);
return $"Item {@string} x{int2} spawned on player {peer.GetPlayerInfo()} {peer.GetRefPos()}";
}
}
internal class Heal : PlayerRconCommand
{
public override string Command => "heal";
protected override string OnHandle(ZNetPeer peer, ZDO zdo, CommandArgs args)
{
int @int = args.GetInt(1);
peer.InvokeRoutedRpcToZdo("RPC_Heal", (float)@int, true);
return $"{peer.GetPlayerInfo()} healed to {@int}hp";
}
}
internal class Kick : RconCommand
{
public override string Command => "kick";
protected override string OnHandle(CommandArgs args)
{
string @string = args.GetString(0);
ZNet.instance.Kick(@string);
return "Kicked " + @string;
}
}
public abstract class PlayerRconCommand : RconCommand
{
protected override string OnHandle(CommandArgs args)
{
string @string = args.GetString(0);
ZNetPeer val = ZNet.instance.GetPeerByHostName(@string);
if (val == null)
{
val = ZNet.instance.GetPeerByPlayerName(@string);
}
if (val == null)
{
return "Cannot find user " + @string;
}
ZDO zDO = val.GetZDO();
if (zDO == null)
{
return "Cannot handle command for player " + val.GetPlayerInfo() + ". ZDO not found";
}
return OnHandle(val, zDO, args);
}
protected abstract string OnHandle(ZNetPeer peer, ZDO zdo, CommandArgs args);
}
internal class PrintAdminList : RconCommand
{
private StringBuilder stringBuilder = new StringBuilder();
public override string Command => "adminlist";
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";
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";
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
{
private static readonly string ResultFilePath = Path.Combine(Paths.CachePath, "result.txt");
public abstract string Command { get; }
public RconCommand()
{
FileHelpers.EnsureDirectoryExists(ResultFilePath);
}
public Task<CommandResult> HandleCommandAsync(CommandArgs args)
{
string text = OnHandle(args).Trim();
string attachedFilePath = string.Empty;
if (text.Length > 800)
{
File.WriteAllText(ResultFilePath, text);
attachedFilePath = ResultFilePath;
text = RconCommandsUtil.Truncate(text);
}
CommandResult result = default(CommandResult);
result.Text = OnHandle(args);
result.AttachedFilePath = attachedFilePath;
return Task.FromResult(result);
}
protected abstract string OnHandle(CommandArgs args);
}
internal class RemoveAdmin : RconCommand
{
public override string Command => "removeAdmin";
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";
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";
protected override string OnHandle(CommandArgs args)
{
//IL_0052: 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";
protected override string OnHandle(CommandArgs args)
{
//IL_0003: Unknown result type (might be due to invalid IL or missing references)
//IL_004d: Unknown result type (might be due to invalid IL or missing references)
//IL_0078: Unknown result type (might be due to invalid IL or missing references)
Vector3 val = default(Vector3);
val.x = args.GetInt(0);
val.y = args.GetInt(1);
val.z = args.GetInt(2);
ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[4]
{
val,
3,
Plugin.CommandsUserInfo,
""
});
return $"Ping sent to {val}";
}
}
internal class ServerLogs : IRconCommand
{
private readonly StringBuilder _builder = new StringBuilder();
public string Command => "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);
_builder.Clear();
for (int num = array.Length - 1; num >= 0; num--)
{
_builder.Insert(0, array[num]);
_builder.Insert(0, '\n');
if (_builder.Length > 800)
{
break;
}
}
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";
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 ShowPlayers : RconCommand
{
private StringBuilder _builder = new StringBuilder();
public override string Command => "players";
protected override string OnHandle(CommandArgs args)
{
//IL_0076: Unknown result type (might be due to invalid IL or missing references)
//IL_0084: Unknown result type (might be due to invalid IL or missing references)
//IL_0089: Unknown result type (might be due to invalid IL or missing references)
_builder.Clear();
int count = ZNet.instance.GetPeers().Count;
_builder.AppendFormat("Online {0}\n", count);
foreach (ZNetPeer peer in ZNet.instance.GetPeers())
{
_builder.AppendFormat("{0}:{1} - {2}({3})", peer.GetSteamId(), peer.m_playerName, peer.GetRefPos(), ZoneSystem.GetZone(peer.GetRefPos()));
_builder.AppendLine();
}
return _builder.ToString();
}
}
internal class SpawnObject : RconCommand
{
public override string Command => "spawn";
protected override string OnHandle(CommandArgs args)
{
//IL_001b: Unknown result type (might be due to invalid IL or missing references)
//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
//IL_0127: Unknown result type (might be due to invalid IL or missing references)
string @string = args.GetString(0);
int @int = args.GetInt(1);
int int2 = args.GetInt(2);
Vector3 val = default(Vector3);
val.x = args.GetInt(3);
val.y = args.GetInt(4);
val.z = args.GetInt(5);
GameObject prefab = ZNetScene.instance.GetPrefab(@string);
if ((Object)(object)prefab == (Object)null)
{
return "Prefab " + @string + " not found";
}
if (int2 <= 0)
{
return "Nothing to spawn";
}
Character val3 = default(Character);
ItemDrop val4 = default(ItemDrop);
for (int i = 0; i < int2; i++)
{
ZNetView.StartGhostInit();
GameObject val2 = Object.Instantiate<GameObject>(prefab, val, Quaternion.identity);
if (val2.TryGetComponent<Character>(ref val3))
{
val3.SetLevel(@int);
}
if (val2.TryGetComponent<ItemDrop>(ref val4))
{
val4.SetQuality(@int);
}
ZNetView.FinishGhostInit();
Object.Destroy((Object)(object)val2);
}
return $"{@string} x{int2} level:{@int} instantiated at {val}";
}
}
internal class TeleportPlayer : PlayerRconCommand
{
public override string Command => "teleport";
protected override string OnHandle(ZNetPeer peer, ZDO zdo, CommandArgs args)
{
//IL_0003: Unknown result type (might be due to invalid IL or missing references)
//IL_0044: Unknown result type (might be due to invalid IL or missing references)
//IL_004d: Unknown result type (might be due to invalid IL or missing references)
//IL_0072: Unknown result type (might be due to invalid IL or missing references)
Vector3 val = default(Vector3);
val.x = args.GetInt(1);
val.y = args.GetInt(2);
val.z = args.GetInt(3);
peer.InvokeRoutedRpcToZdo("RPC_TeleportTo", val, Quaternion.identity, true);
return $"Player {peer.GetPlayerInfo()} teleported to {val}";
}
}
internal class Unban : RconCommand
{
public override string Command => "unban";
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";
protected override string OnHandle(CommandArgs args)
{
ZNet.instance.Save(false, false, false);
return "World save started";
}
}
}