Decompiled source of ValheimCliBridge v0.1.0

BepInEx/plugins/swear01-ValheimCliBridge/ValheimCliBridge.dll

Decompiled 2 weeks ago
using 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);
		}
	}
}