Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimCliBridge v0.1.0
BepInEx/plugins/swear01-ValheimCliBridge/ValheimCliBridge.dll
Decompiled 2 weeks agousing System; 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.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Threading; using System.Xml; using BepInEx; using BepInEx.Configuration; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("ValheimCliBridge")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+8fe7b7c82c41527cf2dd95cbb16d43e62b0380a4")] [assembly: AssemblyProduct("ValheimCliBridge")] [assembly: AssemblyTitle("ValheimCliBridge")] [assembly: AssemblyVersion("0.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimCliBridge { public sealed class Dispatcher { private sealed class Job { public Request Request; public long Expires; public readonly object Gate = new object(); public bool Started; public bool Cancelled; public Response Result; } private readonly Queue<Job> queue = new Queue<Job>(); private readonly object gate = new object(); private bool stopped; private readonly Func<Request, Response> execute; public Dispatcher(Func<Request, Response> execute) { this.execute = execute; } public Response Run(Request request, int timeoutMs = 2000) { Job job = new Job { Request = request, Expires = Stopwatch.GetTimestamp() + timeoutMs * Stopwatch.Frequency / 1000 }; lock (gate) { if (stopped) { return Response.Error(request.id, "Bridge stopped", "cancelled"); } if (queue.Count >= 8) { return Response.Error(request.id, "Bridge busy", "cancelled"); } queue.Enqueue(job); } lock (job.Gate) { while (job.Result == null) { long num = (job.Expires - Stopwatch.GetTimestamp()) * 1000 / Stopwatch.Frequency; if (num <= 0) { break; } Monitor.Wait(job.Gate, (int)Math.Min(num, 2147483647L)); } if (job.Result != null) { return job.Result; } if (job.Started) { return Response.Error(request.id, "Execution started; outcome unknown. Do not retry automatically."); } job.Cancelled = true; return Response.Error(request.id, "Expired before execution; action cancelled", "cancelled"); } } public void Pump() { Job job; lock (gate) { if (stopped || queue.Count == 0) { return; } job = queue.Dequeue(); } lock (job.Gate) { if (job.Cancelled || Stopwatch.GetTimestamp() >= job.Expires) { job.Result = Response.Error(job.Request.id, "Expired before execution; action cancelled", "cancelled"); Monitor.PulseAll(job.Gate); return; } job.Started = true; } Response result; try { result = execute(job.Request); } catch (Exception ex) { result = Response.Error(job.Request.id, "Game operation failed: " + ex.GetType().Name); } lock (job.Gate) { job.Result = result; Monitor.PulseAll(job.Gate); } } public void Stop() { lock (gate) { stopped = true; while (queue.Count > 0) { Job job = queue.Dequeue(); lock (job.Gate) { job.Cancelled = true; job.Result = Response.Error(job.Request.id, "Bridge stopped", "cancelled"); Monitor.PulseAll(job.Gate); } } } } } [BepInPlugin("swear01.ValheimCliBridge", "Valheim CLI Bridge", "0.1.0")] public sealed class Plugin : BaseUnityPlugin { private Dispatcher dispatcher; private Server server; private ConfigEntry<bool> allowTeleport; private void Awake() { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown ConfigEntry<bool> obj = ((BaseUnityPlugin)this).Config.Bind<bool>("Bridge", "Enabled", true, "Enable local CLI bridge. Restart after changes."); ConfigEntry<int> val = ((BaseUnityPlugin)this).Config.Bind<int>("Bridge", "Port", 28761, new ConfigDescription("Local loopback port. Restart after changes.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1024, 65535), Array.Empty<object>())); allowTeleport = ((BaseUnityPlugin)this).Config.Bind<bool>("Permissions", "AllowTeleport", false, "Allow the local host to teleport their own character. Client writes are refused."); if (!obj.Value) { return; } try { string path = Path.Combine(Paths.ConfigPath, "swear01.ValheimCliBridge.token"); if (!File.Exists(path)) { using FileStream stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None); using StreamWriter streamWriter = new StreamWriter(stream); streamWriter.Write(Protocol.CreateToken()); } string text = File.ReadAllText(path).Trim(); if (text.Length != 64 || text.Any((char c) => !Enumerable.Contains("0123456789abcdef", c))) { throw new InvalidDataException("Invalid local token file"); } dispatcher = new Dispatcher(Execute); server = new Server(val.Value, text, dispatcher, delegate(Exception error) { ((BaseUnityPlugin)this).Logger.LogError((object)("CLI bridge connection failed: " + error.GetType().Name)); }); ((BaseUnityPlugin)this).Logger.LogInfo((object)("CLI bridge listening on 127.0.0.1:" + val.Value + "; token stored in local config folder. Never share it.")); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("CLI bridge did not start: " + ex.GetType().Name + ((ex is InvalidDataException) ? "; stop the game, delete swear01.ValheimCliBridge.token from the config folder, and restart." : "; check the config folder permissions and whether the configured port is already in use."))); } } private void Update() { dispatcher?.Pump(); } private void OnDestroy() { server?.Dispose(); } private Response Execute(Request request) { //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_021c: Unknown result type (might be due to invalid IL or missing references) //IL_0224: Unknown result type (might be due to invalid IL or missing references) //IL_0253: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; ZNet instance = ZNet.instance; Response response = new Response { id = request.id, ok = true, state = "observed", version = "0.1.0", inWorld = ((Object)(object)localPlayer != (Object)null && (Object)(object)instance != (Object)null), host = ((Object)(object)instance != (Object)null && instance.IsServer()), teleportAllowed = ((Object)(object)instance != (Object)null && instance.IsServer() && allowTeleport.Value), world = (((Object)(object)localPlayer != (Object)null && (Object)(object)instance != (Object)null) ? instance.GetWorldName() : null), player = (((Object)(object)localPlayer != (Object)null) ? localPlayer.GetPlayerName() : null), position = (((Object)(object)localPlayer != (Object)null) ? PositionOf(((Component)localPlayer).transform.position) : null), teleporting = ((Object)(object)localPlayer != (Object)null && ((Character)localPlayer).IsTeleporting()) }; if (request.operation == "players") { IEnumerable<Player> allPlayers = Player.GetAllPlayers(); response.players = (from p in allPlayers ?? Enumerable.Empty<Player>() where (Object)(object)p != (Object)null select p.GetPlayerName()).ToArray(); } if (request.operation == "teleport") { if (!response.inWorld) { return Response.Error(request.id, "No local player in world", "cancelled"); } if (!response.teleportAllowed) { return Response.Error(request.id, "Teleport requires local host and AllowTeleport=true", "cancelled"); } if (((Character)localPlayer).IsDead() || ((Character)localPlayer).IsTeleporting()) { return Response.Error(request.id, "Player dead or already teleporting", "cancelled"); } Vector3 val = default(Vector3); ((Vector3)(ref val))..ctor((float)request.x.Value, (float)request.y.Value, (float)request.z.Value); if (!((Character)localPlayer).TeleportTo(val, ((Component)localPlayer).transform.rotation, true)) { return Response.Error(request.id, "Game refused teleport", "cancelled"); } response.state = "started"; response.destination = PositionOf(val); response.teleporting = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)("CLI teleport started; request " + request.id)); } return response; } private static Position PositionOf(Vector3 position) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) return new Position { x = position.x, y = position.y, z = position.z }; } } [DataContract] public sealed class Request { [DataMember(IsRequired = true)] public string id; [DataMember(IsRequired = true)] public string token; [DataMember(IsRequired = true)] public string operation; [DataMember(EmitDefaultValue = false)] public double? x; [DataMember(EmitDefaultValue = false)] public double? y; [DataMember(EmitDefaultValue = false)] public double? z; public void Validate() { if (!Guid.TryParseExact(id, "D", out var _) || token == null || token.Length != 64) { throw new InvalidDataException("Invalid request schema"); } if (operation != "status" && operation != "players" && operation != "teleport") { throw new InvalidDataException("Unknown operation"); } if (operation == "teleport") { if (!Finite(x) || !Finite(y) || !Finite(z) || Math.Abs(x.Value) > 10500.0 || Math.Abs(z.Value) > 10500.0 || y < -1000.0 || y > 5000.0) { throw new InvalidDataException("Invalid coordinates"); } } else if (x.HasValue || y.HasValue || z.HasValue) { throw new InvalidDataException("Unexpected coordinates"); } } private static bool Finite(double? value) { if (value.HasValue && !double.IsNaN(value.Value)) { return !double.IsInfinity(value.Value); } return false; } } [DataContract] public sealed class Position { [DataMember] public float x; [DataMember] public float y; [DataMember] public float z; } [DataContract] public sealed class Response { [DataMember] public string id; [DataMember] public bool ok; [DataMember(EmitDefaultValue = false)] public string error; [DataMember(EmitDefaultValue = false)] public string state; [DataMember(EmitDefaultValue = false)] public string version; [DataMember] public bool inWorld; [DataMember] public bool host; [DataMember] public bool teleportAllowed; [DataMember] public bool teleporting; [DataMember(EmitDefaultValue = false)] public string world; [DataMember(EmitDefaultValue = false)] public string player; [DataMember(EmitDefaultValue = false)] public string[] players; [DataMember(EmitDefaultValue = false)] public Position position; [DataMember(EmitDefaultValue = false)] public Position destination; public static Response Error(string id, string message, string state = null) { return new Response { id = id, ok = false, error = message, state = state }; } } public static class Protocol { public const int MaxFrame = 65536; public static byte[] Encode<T>(T value) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) using MemoryStream memoryStream = new MemoryStream(); ((XmlObjectSerializer)new DataContractJsonSerializer(typeof(T))).WriteObject((Stream)memoryStream, (object)value); return memoryStream.ToArray(); } public static Request Decode(byte[] bytes) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Invalid comparison between Unknown and I4 //IL_0195: Unknown result type (might be due to invalid IL or missing references) HashSet<string> hashSet = new HashSet<string>(); XmlDictionaryReader val = JsonReaderWriterFactory.CreateJsonReader(bytes, new XmlDictionaryReaderQuotas { MaxDepth = 4, MaxStringContentLength = 512 }); try { ((XmlReader)val).MoveToContent(); if (((XmlReader)val).Name != "root" || ((XmlReader)val).GetAttribute("type") != "object" || ((XmlReader)val).GetAttribute("__type") != null) { throw new InvalidDataException("Expected plain request object"); } while (((XmlReader)val).Read()) { if ((int)((XmlReader)val).NodeType == 1) { if (((XmlReader)val).Depth != 1 || !hashSet.Add(((XmlReader)val).Name) || (((XmlReader)val).Name != "id" && ((XmlReader)val).Name != "token" && ((XmlReader)val).Name != "operation" && ((XmlReader)val).Name != "x" && ((XmlReader)val).Name != "y" && ((XmlReader)val).Name != "z")) { throw new InvalidDataException("Invalid request fields"); } if (((XmlReader)val).GetAttribute("type") != ((((XmlReader)val).Name == "x" || ((XmlReader)val).Name == "y" || ((XmlReader)val).Name == "z") ? "number" : "string")) { throw new InvalidDataException("Invalid request field type"); } } } } finally { ((IDisposable)val)?.Dispose(); } using MemoryStream memoryStream = new MemoryStream(bytes); Request obj = (Request)((XmlObjectSerializer)new DataContractJsonSerializer(typeof(Request))).ReadObject((Stream)memoryStream); if (memoryStream.Position != memoryStream.Length) { throw new InvalidDataException("Trailing data"); } obj.Validate(); return obj; } public static byte[] ReadFrame(Stream stream) { byte[] array = ReadExact(stream, 4); uint num = (uint)((array[0] << 24) | (array[1] << 16) | (array[2] << 8) | array[3]); if (num == 0 || num > 65536) { throw new InvalidDataException("Invalid frame length"); } return ReadExact(stream, (int)num); } private static byte[] ReadExact(Stream stream, int length) { byte[] array = new byte[length]; int num; for (int i = 0; i < length; i += num) { num = stream.Read(array, i, length - i); if (num == 0) { throw new EndOfStreamException(); } } return array; } public static void WriteFrame(Stream stream, Response response) { byte[] array = Encode(response); if (array.Length > 65536) { throw new InvalidDataException("Response too large"); } stream.Write(new byte[4] { (byte)(array.Length >> 24), (byte)(array.Length >> 16), (byte)(array.Length >> 8), (byte)array.Length }, 0, 4); stream.Write(array, 0, array.Length); } public static bool Authenticated(string expected, string actual) { if (expected == null || actual == null || expected.Length != 64 || actual.Length != 64) { return false; } int num = 0; for (int i = 0; i < 64; i++) { num |= expected[i] ^ actual[i]; } return num == 0; } public static string CreateToken() { byte[] array = new byte[32]; using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create()) { randomNumberGenerator.GetBytes(array); } return BitConverter.ToString(array).Replace("-", "").ToLowerInvariant(); } } public sealed class Server : IDisposable { private readonly TcpListener listener; private readonly Dispatcher dispatcher; private readonly string token; private readonly Action<Exception> logError; private readonly Thread thread; private volatile bool stopped; private TcpClient active; private readonly object gate = new object(); private readonly HashSet<string> writes = new HashSet<string>(StringComparer.OrdinalIgnoreCase); public int Port => ((IPEndPoint)listener.LocalEndpoint).Port; public Server(int port, string token, Dispatcher dispatcher, Action<Exception> logError) { if (token == null || token.Length != 64) { throw new ArgumentException("Invalid token"); } this.token = token; this.dispatcher = dispatcher; this.logError = logError; listener = new TcpListener(IPAddress.Loopback, port); listener.Start(8); thread = new Thread(Listen) { IsBackground = true, Name = "ValheimCliBridge" }; thread.Start(); } private void Listen() { while (!stopped) { try { TcpClient client = listener.AcceptTcpClient(); try { using (new Timer(delegate { client.Close(); }, null, 4000, -1)) { lock (gate) { if (stopped) { break; } active = client; } client.ReceiveTimeout = 1000; client.SendTimeout = 1000; using (NetworkStream stream = client.GetStream()) { Response response; try { Request request = Protocol.Decode(Protocol.ReadFrame(stream)); if (!Protocol.Authenticated(token, request.token)) { response = Response.Error(request.id, "Unauthorized"); } else if (request.operation == "teleport" && writes.Contains(request.id)) { response = Response.Error(request.id, "Write ID already used; inspect status, do not retry"); } else if (request.operation == "teleport" && writes.Count >= 256) { response = Response.Error(request.id, "Write limit reached; restart the bridge before a new session"); } else { if (request.operation == "teleport") { writes.Add(request.id); } response = dispatcher.Run(request); if (request.operation == "teleport" && response.state == "cancelled") { writes.Remove(request.id); } } } catch { response = Response.Error(null, "Invalid request"); } Protocol.WriteFrame(stream, response); } lock (gate) { active = null; } } } finally { if (client != null) { ((IDisposable)client).Dispose(); } } } catch (IOException) { } catch (InvalidDataException) { } catch (SerializationException) { } catch (SocketException) { if (stopped) { break; } } catch (ObjectDisposedException) { if (stopped) { break; } } catch (Exception obj2) { if (stopped) { break; } logError(obj2); } } } public void Dispose() { stopped = true; dispatcher.Stop(); listener.Stop(); lock (gate) { active?.Close(); } thread.Join(3000); } } }