Decompiled source of ValheimMCP v0.2.0

plugins/ValheimMCP/ValheimMCP.dll

Decompiled 2 months ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("ValheimMCP")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+1966c462ed64ecc3aadb8d32adb981e2b6033030")]
[assembly: AssemblyProduct("ValheimMCP")]
[assembly: AssemblyTitle("ValheimMCP")]
[assembly: AssemblyVersion("0.2.0.0")]
namespace ValheimMCP;

internal sealed class RenderResult
{
	public byte[] Png;

	public string Error;
}
internal static class CameraRenderer
{
	private const string CamName = "valheimmcp_render_cam";

	private static Camera _sCam;

	public static RenderResult Render(float x, float z, float? y, float yaw, float pitch, float dist, int size)
	{
		//IL_0068: 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_0077: Unknown result type (might be due to invalid IL or missing references)
		//IL_007c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0081: Unknown result type (might be due to invalid IL or missing references)
		//IL_0091: Unknown result type (might be due to invalid IL or missing references)
		//IL_009f: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a2: 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_00ac: Unknown result type (might be due to invalid IL or missing references)
		//IL_0106: Unknown result type (might be due to invalid IL or missing references)
		//IL_010d: Expected O, but got Unknown
		//IL_011f: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			size = ModConfig.ClampRenderSize(size);
			float num = y ?? SampleGround(x, z);
			Vector3 val = default(Vector3);
			((Vector3)(ref val))..ctor(x, num, z);
			float num2 = pitch * ((float)Math.PI / 180f);
			float num3 = yaw * ((float)Math.PI / 180f);
			Vector3 val2 = default(Vector3);
			((Vector3)(ref val2))..ctor(Mathf.Cos(num2) * Mathf.Sin(num3), Mathf.Sin(num2), Mathf.Cos(num2) * Mathf.Cos(num3));
			Vector3 val3 = val + val2 * Mathf.Max(1f, dist);
			Camera val4 = EnsureCamera();
			((Component)val4).transform.position = val3;
			((Component)val4).transform.rotation = Quaternion.LookRotation(val - val3, Vector3.up);
			val4.nearClipPlane = 0.1f;
			val4.farClipPlane = dist + 1000f;
			RenderTexture temporary = RenderTexture.GetTemporary(size, size, 24, (RenderTextureFormat)0);
			RenderTexture active = RenderTexture.active;
			Texture2D val5 = null;
			try
			{
				val4.targetTexture = temporary;
				val4.Render();
				RenderTexture.active = temporary;
				val5 = new Texture2D(size, size, (TextureFormat)3, false);
				val5.ReadPixels(new Rect(0f, 0f, (float)size, (float)size), 0, 0);
				val5.Apply();
				byte[] png = ImageConversion.EncodeToPNG(val5);
				return new RenderResult
				{
					Png = png
				};
			}
			finally
			{
				val4.targetTexture = null;
				RenderTexture.active = active;
				RenderTexture.ReleaseTemporary(temporary);
				if ((Object)(object)val5 != (Object)null)
				{
					Object.Destroy((Object)(object)val5);
				}
			}
		}
		catch (Exception ex)
		{
			return new RenderResult
			{
				Error = ex.Message
			};
		}
	}

	private static Camera EnsureCamera()
	{
		//IL_0018: 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_0025: Unknown result type (might be due to invalid IL or missing references)
		//IL_002b: Expected O, but got Unknown
		if ((Object)(object)_sCam != (Object)null)
		{
			return _sCam;
		}
		GameObject val = new GameObject("valheimmcp_render_cam")
		{
			hideFlags = (HideFlags)61
		};
		Object.DontDestroyOnLoad((Object)val);
		_sCam = val.AddComponent<Camera>();
		((Behaviour)_sCam).enabled = false;
		_sCam.clearFlags = (CameraClearFlags)1;
		_sCam.cullingMask = -1;
		_sCam.fieldOfView = 60f;
		return _sCam;
	}

	private static float SampleGround(float x, float z)
	{
		//IL_0017: Unknown result type (might be due to invalid IL or missing references)
		ZoneSystem instance = ZoneSystem.instance;
		float result = default(float);
		if ((Object)(object)instance != (Object)null && instance.GetGroundHeight(new Vector3(x, 5000f, z), ref result))
		{
			return result;
		}
		return 0f;
	}
}
internal sealed class CommandResult
{
	public bool Ok;

	public string Error;

	public List<string> Output = new List<string>();
}
internal static class ConsoleBridge
{
	private static readonly FieldInfo CommandsField = typeof(Terminal).GetField("commands", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

	public static bool IsReady => (Object)(object)Console.instance != (Object)null;

	public static List<KeyValuePair<string, string>> ListCommands()
	{
		List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();
		if (CommandsField?.GetValue(null) is IDictionary dictionary)
		{
			foreach (DictionaryEntry item in dictionary)
			{
				string text = item.Key as string;
				object? value = item.Value;
				string value2 = ((ConsoleCommand)(((value is ConsoleCommand) ? value : null)?)).Description ?? "";
				if (text != null)
				{
					list.Add(new KeyValuePair<string, string>(text, value2));
				}
			}
		}
		return list.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> c) => c.Key, StringComparer.Ordinal).ToList();
	}

	public static CommandResult Run(string commandLine)
	{
		if (!ModConfig.IsCommandAllowed(commandLine, out var reason))
		{
			return new CommandResult
			{
				Ok = false,
				Error = reason
			};
		}
		Console instance = Console.instance;
		if ((Object)(object)instance == (Object)null)
		{
			return new CommandResult
			{
				Ok = false,
				Error = "Console.instance is null (no game loaded yet)"
			};
		}
		ConsoleOutputCapture.Begin();
		try
		{
			((Terminal)instance).TryRunCommand(commandLine, false, true);
		}
		catch (Exception ex)
		{
			return new CommandResult
			{
				Ok = false,
				Error = "command threw: " + ex.Message,
				Output = ConsoleOutputCapture.End()
			};
		}
		return new CommandResult
		{
			Ok = true,
			Output = ConsoleOutputCapture.End()
		};
	}
}
[HarmonyPatch]
internal static class ConsoleOutputCapture
{
	private static List<string> _sink;

	public static void Begin()
	{
		_sink = new List<string>();
	}

	public static List<string> End()
	{
		List<string> result = _sink ?? new List<string>();
		_sink = null;
		return result;
	}

	[HarmonyPostfix]
	[HarmonyPatch(typeof(Terminal), "AddString", new Type[] { typeof(string) })]
	private static void Terminal_AddString_Postfix(string text)
	{
		_sink?.Add(text);
	}
}
internal sealed class HttpServer
{
	private sealed class HttpMcpTransport : IMcpTransport
	{
		private readonly HttpListenerContext _ctx;

		private readonly object _lock = new object();

		private bool _sse;

		private bool _closed;

		public bool AcceptsSse
		{
			get
			{
				string text = _ctx.Request.Headers["Accept"];
				if (text != null)
				{
					return text.IndexOf("text/event-stream", StringComparison.OrdinalIgnoreCase) >= 0;
				}
				return false;
			}
		}

		public HttpMcpTransport(HttpListenerContext ctx)
		{
			_ctx = ctx;
		}

		public void WriteJson(int status, string body)
		{
			lock (_lock)
			{
				if (!_closed)
				{
					_closed = true;
					Write(_ctx, status, body);
				}
			}
		}

		public void WriteAccepted()
		{
			lock (_lock)
			{
				if (!_closed)
				{
					_closed = true;
					WriteEmpty(_ctx, 202);
				}
			}
		}

		public void BeginSse()
		{
			lock (_lock)
			{
				if (!_sse && !_closed)
				{
					_sse = true;
					_ctx.Response.StatusCode = 200;
					_ctx.Response.ContentType = "text/event-stream";
					_ctx.Response.Headers["Cache-Control"] = "no-cache";
					_ctx.Response.SendChunked = true;
				}
			}
		}

		public void SendSse(string jsonMessage)
		{
			lock (_lock)
			{
				if (!_sse || _closed)
				{
					return;
				}
				try
				{
					byte[] bytes = Encoding.UTF8.GetBytes("data: " + jsonMessage + "\n\n");
					_ctx.Response.OutputStream.Write(bytes, 0, bytes.Length);
					_ctx.Response.OutputStream.Flush();
				}
				catch (Exception ex)
				{
					_closed = true;
					ManualLogSource log = Plugin.Log;
					if (log != null)
					{
						log.LogWarning((object)("[ValheimMCP] SSE write failed (client gone?): " + ex.Message));
					}
				}
			}
		}

		public void EndSse()
		{
			lock (_lock)
			{
				if (!_sse || _closed)
				{
					return;
				}
				_closed = true;
				try
				{
					_ctx.Response.OutputStream.Close();
				}
				catch
				{
				}
			}
		}
	}

	private readonly HttpListener _listener = new HttpListener();

	private readonly int _commandTimeoutMs;

	private Thread _thread;

	private volatile bool _running;

	public HttpServer(string prefix, int commandTimeoutMs)
	{
		_listener.Prefixes.Add(prefix);
		_commandTimeoutMs = commandTimeoutMs;
	}

	public void Start()
	{
		_listener.Start();
		_running = true;
		_thread = new Thread(Loop)
		{
			IsBackground = true,
			Name = "ValheimMCP-http"
		};
		_thread.Start();
	}

	public void Stop()
	{
		_running = false;
		try
		{
			_listener.Stop();
			_listener.Close();
		}
		catch
		{
		}
	}

	private void Loop()
	{
		while (_running)
		{
			HttpListenerContext ctx;
			try
			{
				ctx = _listener.GetContext();
			}
			catch
			{
				if (!_running)
				{
					break;
				}
				continue;
			}
			ThreadPool.QueueUserWorkItem(delegate
			{
				try
				{
					Handle(ctx);
				}
				catch (Exception ex)
				{
					ManualLogSource log = Plugin.Log;
					if (log != null)
					{
						log.LogError((object)$"[ValheimMCP] request handler threw: {ex}");
					}
					try
					{
						Write(ctx, 500, Json.Error(ex.Message));
					}
					catch
					{
					}
				}
			});
		}
	}

	private void Handle(HttpListenerContext ctx)
	{
		string text = ctx.Request.Url.AbsolutePath.TrimEnd(new char[1] { '/' });
		string httpMethod = ctx.Request.HttpMethod;
		switch (text)
		{
		case "/mcp":
			if (httpMethod == "POST")
			{
				McpServer.Dispatch(ReadBody(ctx.Request), _commandTimeoutMs, new HttpMcpTransport(ctx));
			}
			else if (httpMethod == "DELETE")
			{
				WriteEmpty(ctx, 200);
			}
			else
			{
				Write(ctx, 405, Json.Error("MCP endpoint supports POST (and DELETE); server-initiated GET stream not implemented"));
			}
			return;
		case "/sse":
			Write(ctx, 501, Json.Error("SSE transport not implemented; use POST /mcp (JSON-RPC over HTTP)"));
			return;
		case "":
		case "/health":
		{
			MainThreadDispatcher.RunBlocking(() => ConsoleBridge.IsReady, 2000, out var result2, out var _);
			Write(ctx, 200, Json.Health(result2));
			return;
		}
		case "/commands":
			if (httpMethod == "GET")
			{
				if (!MainThreadDispatcher.RunBlocking(() => Json.Commands(ConsoleBridge.ListCommands()), 5000, out var result, out var error))
				{
					Write(ctx, 504, Json.Error("timed out listing commands (game not ticking?)"));
				}
				else if (error != null)
				{
					Write(ctx, 500, Json.Error(error.Message));
				}
				else
				{
					Write(ctx, 200, result);
				}
				return;
			}
			break;
		}
		if (text == "/command" && httpMethod == "POST")
		{
			string text2 = ReadCommandText(ctx.Request);
			CommandResult result3;
			Exception error3;
			if (string.IsNullOrWhiteSpace(text2))
			{
				Write(ctx, 400, Json.Error("missing command text (send as raw body or ?text=)"));
			}
			else if (!MainThreadDispatcher.RunBlocking(() => ConsoleBridge.Run(text2), _commandTimeoutMs, out result3, out error3))
			{
				Write(ctx, 504, Json.Error($"timed out after {_commandTimeoutMs}ms (game paused or command hung?)"));
			}
			else if (error3 != null)
			{
				Write(ctx, 500, Json.Error(error3.Message));
			}
			else
			{
				Write(ctx, result3.Ok ? 200 : 500, Json.CommandResult(text2, result3));
			}
		}
		else if (text == "/log" && httpMethod == "GET")
		{
			NameValueCollection queryString = ctx.Request.QueryString;
			long result4;
			long since = (long.TryParse(queryString["since"], out result4) ? result4 : (-1));
			int.TryParse(queryString["maxLines"], out var result5);
			int maxLines = ModConfig.ClampLogLines(result5);
			string text3 = queryString["contains"];
			bool flag = queryString["regex"] == "1" || string.Equals(queryString["regex"], "true", StringComparison.OrdinalIgnoreCase);
			Func<string, bool> filter = null;
			if (!string.IsNullOrEmpty(text3))
			{
				if (flag)
				{
					Regex rx;
					try
					{
						rx = new Regex(text3);
					}
					catch (Exception ex)
					{
						Write(ctx, 400, Json.Error("invalid regex: " + ex.Message));
						return;
					}
					filter = (string line) => rx.IsMatch(line);
				}
				else
				{
					string needle = text3;
					filter = (string line) => line.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0;
				}
			}
			long cursor;
			int totalMatching;
			int dropped;
			List<string> log = LogWatch.GetLog(since, maxLines, filter, out cursor, out totalMatching, out dropped);
			Write(ctx, 200, Json.LogResult(cursor, totalMatching, dropped, log));
		}
		else
		{
			Write(ctx, 404, Json.Error("unknown route: " + httpMethod + " " + text));
		}
	}

	private static string ReadCommandText(HttpListenerRequest req)
	{
		string text = req.QueryString["text"];
		if (!string.IsNullOrEmpty(text))
		{
			return text.Trim();
		}
		return ReadBody(req).Trim();
	}

	private static string ReadBody(HttpListenerRequest req)
	{
		using StreamReader streamReader = new StreamReader(req.InputStream, req.ContentEncoding ?? Encoding.UTF8);
		return streamReader.ReadToEnd();
	}

	private static void WriteEmpty(HttpListenerContext ctx, int status)
	{
		try
		{
			ctx.Response.StatusCode = status;
			ctx.Response.ContentLength64 = 0L;
			ctx.Response.OutputStream.Close();
		}
		catch (Exception ex)
		{
			ManualLogSource log = Plugin.Log;
			if (log != null)
			{
				log.LogWarning((object)("[ValheimMCP] failed to write empty response: " + ex.Message));
			}
		}
	}

	private static void Write(HttpListenerContext ctx, int status, string json)
	{
		try
		{
			byte[] bytes = Encoding.UTF8.GetBytes(json);
			ctx.Response.StatusCode = status;
			ctx.Response.ContentType = "application/json";
			ctx.Response.ContentLength64 = bytes.Length;
			ctx.Response.OutputStream.Write(bytes, 0, bytes.Length);
			ctx.Response.OutputStream.Close();
		}
		catch (Exception ex)
		{
			ManualLogSource log = Plugin.Log;
			if (log != null)
			{
				log.LogWarning((object)("[ValheimMCP] failed to write response: " + ex.Message));
			}
		}
	}
}
internal static class Json
{
	public static string Str(string s)
	{
		if (s == null)
		{
			return "null";
		}
		StringBuilder stringBuilder = new StringBuilder(s.Length + 2);
		stringBuilder.Append('"');
		foreach (char c in s)
		{
			switch (c)
			{
			case '"':
				stringBuilder.Append("\\\"");
				continue;
			case '\\':
				stringBuilder.Append("\\\\");
				continue;
			case '\n':
				stringBuilder.Append("\\n");
				continue;
			case '\r':
				stringBuilder.Append("\\r");
				continue;
			case '\t':
				stringBuilder.Append("\\t");
				continue;
			}
			if (c < ' ')
			{
				StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
				int num = c;
				stringBuilder2.Append(num.ToString("x4"));
			}
			else
			{
				stringBuilder.Append(c);
			}
		}
		stringBuilder.Append('"');
		return stringBuilder.ToString();
	}

	public static string Array(IEnumerable<string> items)
	{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append('[');
		bool flag = true;
		foreach (string item in items)
		{
			if (!flag)
			{
				stringBuilder.Append(',');
			}
			flag = false;
			stringBuilder.Append(Str(item));
		}
		stringBuilder.Append(']');
		return stringBuilder.ToString();
	}

	public static string Error(string message)
	{
		return "{\"ok\":false,\"error\":" + Str(message) + "}";
	}

	public static string Health(bool inGame)
	{
		return "{\"ok\":true,\"inGame\":" + (inGame ? "true" : "false") + "}";
	}

	public static string Commands(IReadOnlyList<KeyValuePair<string, string>> commands)
	{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append("{\"ok\":true,\"commands\":[");
		for (int i = 0; i < commands.Count; i++)
		{
			if (i > 0)
			{
				stringBuilder.Append(',');
			}
			stringBuilder.Append("{\"name\":").Append(Str(commands[i].Key)).Append(",\"description\":")
				.Append(Str(commands[i].Value))
				.Append('}');
		}
		stringBuilder.Append("]}");
		return stringBuilder.ToString();
	}

	public static string LogResult(long cursor, int matching, int dropped, IEnumerable<string> lines)
	{
		return "{\"ok\":true,\"cursor\":" + cursor + ",\"matching\":" + matching + ",\"dropped\":" + dropped + ",\"lines\":" + Array(lines) + "}";
	}

	public static string CommandResult(string ran, CommandResult result)
	{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append("{\"ok\":").Append(result.Ok ? "true" : "false").Append(",\"ran\":")
			.Append(Str(ran))
			.Append(",\"output\":")
			.Append(Array(result.Output));
		if (!result.Ok)
		{
			stringBuilder.Append(",\"error\":").Append(Str(result.Error));
		}
		stringBuilder.Append('}');
		return stringBuilder.ToString();
	}
}
internal static class LogWatch
{
	private sealed class Waiter
	{
		public Func<string, bool> Match;

		public Action<string> OnLine;

		public readonly ManualResetEventSlim Done = new ManualResetEventSlim(initialState: false);

		public volatile string MatchedLine;
	}

	private struct Rec
	{
		public long Seq;

		public string Line;
	}

	private sealed class Listener : ILogListener, IDisposable
	{
		public void LogEvent(object sender, LogEventArgs eventArgs)
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			if (eventArgs != null)
			{
				ILogSource source = eventArgs.Source;
				string arg = ((source != null) ? source.SourceName : null) ?? "?";
				string line = $"[{eventArgs.Level}:{arg}] {eventArgs.Data}";
				Buffer(line);
				Dispatch(line);
			}
		}

		public void Dispose()
		{
		}
	}

	private static readonly object Gate = new object();

	private static readonly List<Waiter> Waiters = new List<Waiter>();

	private static Listener _listener;

	private static readonly object BufGate = new object();

	private static Rec[] _ring;

	private static int _ringHead;

	private static int _ringCount;

	private static long _ringSeq;

	public static void Install()
	{
		if (_listener != null)
		{
			return;
		}
		lock (BufGate)
		{
			if (_ring == null)
			{
				_ring = new Rec[Math.Max(16, ModConfig.LogBufferCapacity)];
			}
		}
		_listener = new Listener();
		Logger.Listeners.Add((ILogListener)(object)_listener);
	}

	public static void Uninstall()
	{
		if (_listener != null)
		{
			try
			{
				Logger.Listeners.Remove((ILogListener)(object)_listener);
			}
			catch
			{
			}
			_listener.Dispose();
			_listener = null;
		}
		lock (Gate)
		{
			foreach (Waiter waiter in Waiters)
			{
				waiter.Done.Set();
			}
			Waiters.Clear();
		}
	}

	public static string Wait(Func<string, bool> match, int timeoutMs, Action<string> onLine, out bool timedOut)
	{
		Waiter waiter = new Waiter
		{
			Match = match,
			OnLine = onLine
		};
		lock (Gate)
		{
			Waiters.Add(waiter);
		}
		try
		{
			timedOut = !waiter.Done.Wait(timeoutMs);
			return timedOut ? null : waiter.MatchedLine;
		}
		finally
		{
			lock (Gate)
			{
				Waiters.Remove(waiter);
			}
			waiter.Done.Dispose();
		}
	}

	private static void Dispatch(string line)
	{
		Waiter[] array;
		lock (Gate)
		{
			if (Waiters.Count == 0)
			{
				return;
			}
			array = Waiters.ToArray();
		}
		Waiter[] array2 = array;
		foreach (Waiter waiter in array2)
		{
			if (waiter.Done.IsSet)
			{
				continue;
			}
			if (waiter.OnLine != null)
			{
				try
				{
					waiter.OnLine(line);
				}
				catch (Exception ex)
				{
					ManualLogSource log = Plugin.Log;
					if (log != null)
					{
						log.LogWarning((object)("[ValheimMCP] log-watch onLine threw: " + ex.Message));
					}
				}
			}
			bool flag;
			try
			{
				flag = waiter.Match(line);
			}
			catch (Exception ex2)
			{
				ManualLogSource log2 = Plugin.Log;
				if (log2 != null)
				{
					log2.LogWarning((object)("[ValheimMCP] log-watch match threw: " + ex2.Message));
				}
				continue;
			}
			if (flag)
			{
				waiter.MatchedLine = line;
				waiter.Done.Set();
			}
		}
	}

	private static void Buffer(string line)
	{
		lock (BufGate)
		{
			if (_ring != null)
			{
				_ringSeq++;
				_ring[_ringHead] = new Rec
				{
					Seq = _ringSeq,
					Line = line
				};
				_ringHead = (_ringHead + 1) % _ring.Length;
				if (_ringCount < _ring.Length)
				{
					_ringCount++;
				}
			}
		}
	}

	public static List<string> GetLog(long since, int maxLines, Func<string, bool> filter, out long cursor, out int totalMatching, out int dropped)
	{
		List<string> list = new List<string>();
		totalMatching = 0;
		dropped = 0;
		lock (BufGate)
		{
			cursor = _ringSeq;
			if (_ring == null || _ringCount == 0)
			{
				return list;
			}
			long num = _ringSeq - _ringCount + 1;
			if (since >= 0 && since + 1 < num)
			{
				dropped = (int)(num - 1 - since);
			}
			int num2 = (_ringHead - _ringCount + _ring.Length) % _ring.Length;
			List<string> list2 = new List<string>();
			for (int i = 0; i < _ringCount; i++)
			{
				Rec rec = _ring[(num2 + i) % _ring.Length];
				if (rec.Seq > since && (filter == null || filter(rec.Line)))
				{
					list2.Add(rec.Line);
				}
			}
			totalMatching = list2.Count;
			for (int j = ((maxLines > 0 && list2.Count > maxLines) ? (list2.Count - maxLines) : 0); j < list2.Count; j++)
			{
				list.Add(list2[j]);
			}
			return list;
		}
	}
}
internal static class MainThreadDispatcher
{
	private static readonly ConcurrentQueue<Action> Queue = new ConcurrentQueue<Action>();

	public static bool RunBlocking<T>(Func<T> func, int timeoutMs, out T result, out Exception error)
	{
		T captured = default(T);
		Exception err = null;
		ManualResetEventSlim done = new ManualResetEventSlim(initialState: false);
		try
		{
			Queue.Enqueue(delegate
			{
				try
				{
					captured = func();
				}
				catch (Exception ex)
				{
					err = ex;
				}
				finally
				{
					done.Set();
				}
			});
			bool result2 = done.Wait(timeoutMs);
			result = captured;
			error = err;
			return result2;
		}
		finally
		{
			if (done != null)
			{
				((IDisposable)done).Dispose();
			}
		}
	}

	public static void Pump()
	{
		Action result;
		while (Queue.TryDequeue(out result))
		{
			try
			{
				result();
			}
			catch (Exception arg)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogError((object)$"[ValheimMCP] queued main-thread action threw: {arg}");
				}
			}
		}
	}
}
internal interface IMcpTransport
{
	bool AcceptsSse { get; }

	void WriteJson(int status, string body);

	void WriteAccepted();

	void BeginSse();

	void SendSse(string jsonMessage);

	void EndSse();
}
internal static class McpServer
{
	public const string ServerName = "valheim-mcp";

	public const string ServerVersion = "0.2.0";

	private const string DefaultProtocol = "2024-11-05";

	public static void Dispatch(string body, int commandTimeoutMs, IMcpTransport tx)
	{
		object obj;
		try
		{
			obj = MiniJson.Parse(body);
		}
		catch (Exception ex)
		{
			tx.WriteJson(200, ErrorResponse(null, -32700, "Parse error: " + ex.Message));
			return;
		}
		if (obj is List<object> list)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append('[');
			bool flag = false;
			foreach (object item in list)
			{
				string text = HandleOne(item as Dictionary<string, object>, commandTimeoutMs);
				if (text != null)
				{
					if (flag)
					{
						stringBuilder.Append(',');
					}
					stringBuilder.Append(text);
					flag = true;
				}
			}
			stringBuilder.Append(']');
			if (flag)
			{
				tx.WriteJson(200, stringBuilder.ToString());
			}
			else
			{
				tx.WriteAccepted();
			}
			return;
		}
		Dictionary<string, object> req = obj as Dictionary<string, object>;
		if (tx.AcceptsSse && IsWaitForLogCall(req, out var id, out var args, out var progressToken) && progressToken != null)
		{
			StreamWaitForLog(id, args, progressToken, tx);
			return;
		}
		string text2 = HandleOne(req, commandTimeoutMs);
		if (text2 == null)
		{
			tx.WriteAccepted();
		}
		else
		{
			tx.WriteJson(200, text2);
		}
	}

	private static string HandleOne(Dictionary<string, object> req, int commandTimeoutMs)
	{
		if (req == null)
		{
			return ErrorResponse(null, -32600, "Invalid Request");
		}
		object value;
		string text = (req.TryGetValue("method", out value) ? (value as string) : null);
		object value2;
		bool flag = req.TryGetValue("id", out value2);
		if (text == null)
		{
			if (!flag)
			{
				return null;
			}
			return ErrorResponse(value2, -32600, "Missing method");
		}
		switch (text)
		{
		case "initialize":
			return Result(value2, InitializeResult(req));
		case "ping":
			return Result(value2, "{}");
		case "tools/list":
			return Result(value2, ToolsListResult());
		case "tools/call":
			return ToolsCall(value2, req, commandTimeoutMs);
		default:
			if (!flag)
			{
				return null;
			}
			return ErrorResponse(value2, -32601, "Method not found: " + text);
		}
	}

	private static string InitializeResult(Dictionary<string, object> req)
	{
		string s = "2024-11-05";
		if (req.TryGetValue("params", out var value) && value is Dictionary<string, object> dictionary && dictionary.TryGetValue("protocolVersion", out var value2) && value2 is string { Length: >0 } text)
		{
			s = text;
		}
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append("{\"protocolVersion\":").Append(Json.Str(s)).Append(",\"capabilities\":{\"tools\":{}}")
			.Append(",\"serverInfo\":{\"name\":")
			.Append(Json.Str("valheim-mcp"))
			.Append(",\"version\":")
			.Append(Json.Str("0.2.0"))
			.Append("}}");
		return stringBuilder.ToString();
	}

	private static string ToolsListResult()
	{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append("{\"tools\":[");
		AppendTool(stringBuilder, "run_command", "Run a Valheim console command (e.g. 'pos' to print the player's position, or any registered command — call list_commands to discover them) and return the lines it printed to the in-game console.", "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\",\"description\":\"The full console command line to execute.\"}},\"required\":[\"text\"]}");
		stringBuilder.Append(',');
		AppendTool(stringBuilder, "list_commands", "List all registered Valheim console commands with their descriptions.", "{\"type\":\"object\",\"properties\":{}}");
		stringBuilder.Append(',');
		AppendTool(stringBuilder, "health", "Check whether Valheim is running with a world loaded (so commands can execute).", "{\"type\":\"object\",\"properties\":{}}");
		stringBuilder.Append(',');
		AppendTool(stringBuilder, "render_view", "Render a PNG of the game world at a point, using an independent off-screen camera (does NOT move the player's view). Returns the image inline.", "{\"type\":\"object\",\"properties\":{\"x\":{\"type\":\"number\",\"description\":\"World X of the look-at point.\"},\"z\":{\"type\":\"number\",\"description\":\"World Z of the look-at point.\"},\"y\":{\"type\":\"number\",\"description\":\"World Y of the look-at point. Defaults to terrain ground height; pass the feature/villager Y for interiors or elevated floors.\"},\"yaw\":{\"type\":\"number\",\"description\":\"Camera compass azimuth in degrees (default 45).\"},\"pitch\":{\"type\":\"number\",\"description\":\"Camera elevation above horizon in degrees: 0=level, 90=top-down (default 35).\"},\"dist\":{\"type\":\"number\",\"description\":\"Camera distance from the look-at point in meters (default 12).\"},\"size\":{\"type\":\"number\",\"description\":\"Square output size in pixels. Defaults to and is clamped by the mod config (render.defaultSize / minSize / maxSize).\"}},\"required\":[\"x\",\"z\"]}");
		stringBuilder.Append(',');
		AppendTool(stringBuilder, "wait_for_log", "Block until a line matching 'pattern' appears in the BepInEx/Valheim log (from the MCP server, the game, or any other mod), then return that line. Use this to wait for an asynchronous event — e.g. a mod hot-reload completing — instead of sleeping or polling: it returns the moment the line appears, or reports a timeout. Each log line is matched in the form [Level:Source] message. When the request carries a progressToken, observed log lines stream back as progress notifications while waiting.", "{\"type\":\"object\",\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Text to wait for, matched against each formatted log line (case-insensitive substring by default).\"},\"regex\":{\"type\":\"boolean\",\"description\":\"Treat 'pattern' as a .NET regular expression instead of a substring (default false).\"},\"timeoutMs\":{\"type\":\"number\",\"description\":\"Maximum time to wait in milliseconds. Clamped to the server's configured maximum (default 120000).\"}},\"required\":[\"pattern\"]}");
		stringBuilder.Append(',');
		AppendTool(stringBuilder, "get_log", "Fetch recent lines from the in-memory BepInEx/Valheim log buffer (the MCP server, the game, and every other mod) — i.e. tail the log on demand, including on a dedicated server whose log file isn't directly accessible. Omit 'since' to get the most recent lines; pass the 'cursor' value from a previous call as 'since' to get only what's new since then (incremental polling). Optionally filter with 'contains' (case-insensitive substring) or 'regex'. The first output line is a header: 'cursor=<n> returned=<k> matching=<t> dropped=<d>' — pass that cursor back as 'since' next time; dropped>0 means older lines were evicted from the buffer between calls (a gap). Unlike wait_for_log (which blocks for a pattern), this returns immediately with whatever is buffered.", "{\"type\":\"object\",\"properties\":{\"since\":{\"type\":\"number\",\"description\":\"Return only lines after this cursor (from a prior get_log call). Omit for the most recent lines.\"},\"maxLines\":{\"type\":\"number\",\"description\":\"Max lines to return (default 200, clamped to the server's configured maximum).\"},\"contains\":{\"type\":\"string\",\"description\":\"Only return lines containing this text (case-insensitive substring).\"},\"regex\":{\"type\":\"boolean\",\"description\":\"Treat 'contains' as a .NET regular expression instead of a substring (default false).\"}}}");
		stringBuilder.Append("]}");
		return stringBuilder.ToString();
	}

	private static void AppendTool(StringBuilder sb, string name, string description, string schemaJson)
	{
		sb.Append("{\"name\":").Append(Json.Str(name)).Append(",\"description\":")
			.Append(Json.Str(description))
			.Append(",\"inputSchema\":")
			.Append(schemaJson)
			.Append('}');
	}

	private static string ToolsCall(object id, Dictionary<string, object> req, int commandTimeoutMs)
	{
		if (!req.TryGetValue("params", out var value) || !(value is Dictionary<string, object> dictionary))
		{
			return ErrorResponse(id, -32602, "Invalid params");
		}
		object value2;
		string text = (dictionary.TryGetValue("name", out value2) ? (value2 as string) : null);
		object value3;
		Dictionary<string, object> dictionary2 = (dictionary.TryGetValue("arguments", out value3) ? (value3 as Dictionary<string, object>) : null);
		if (string.IsNullOrEmpty(text))
		{
			return ErrorResponse(id, -32602, "Missing tool name");
		}
		switch (text)
		{
		case "health":
		{
			MainThreadDispatcher.RunBlocking(() => ConsoleBridge.IsReady, 2000, out var result4, out var _);
			return Result(id, ToolText(Json.Health(result4), isError: false));
		}
		case "list_commands":
		{
			if (!MainThreadDispatcher.RunBlocking(() => Json.Commands(ConsoleBridge.ListCommands()), 5000, out var result3, out var error3))
			{
				return Result(id, ToolText("timed out listing commands (game not ticking?)", isError: true));
			}
			if (error3 != null)
			{
				return Result(id, ToolText(error3.Message, isError: true));
			}
			return Result(id, ToolText(result3, isError: false));
		}
		case "run_command":
		{
			object value7;
			string text2 = ((dictionary2 != null && dictionary2.TryGetValue("text", out value7)) ? (value7 as string) : null);
			if (string.IsNullOrWhiteSpace(text2))
			{
				return Result(id, ToolText("missing 'text' argument", isError: true));
			}
			if (!MainThreadDispatcher.RunBlocking(() => ConsoleBridge.Run(text2), commandTimeoutMs, out var result2, out var error2))
			{
				return Result(id, ToolText($"timed out after {commandTimeoutMs}ms (game paused or command hung?)", isError: true));
			}
			if (error2 != null)
			{
				return Result(id, ToolText(error2.Message, isError: true));
			}
			string text3 = ((result2.Output.Count > 0) ? string.Join("\n", result2.Output) : "(no console output)");
			if (!result2.Ok)
			{
				text3 = (result2.Error ?? "command failed") + ((result2.Output.Count > 0) ? ("\n" + string.Join("\n", result2.Output)) : "");
			}
			return Result(id, ToolText(text3, !result2.Ok));
		}
		case "render_view":
		{
			if (dictionary2 == null || !dictionary2.TryGetValue("x", out var value4) || !(value4 is double num) || !dictionary2.TryGetValue("z", out var value5) || !(value5 is double num2))
			{
				return Result(id, ToolText("render_view requires numeric 'x' and 'z'", isError: true));
			}
			float x = (float)num;
			float z = (float)num2;
			object value6;
			float? y = ((dictionary2.TryGetValue("y", out value6) && value6 is double num3) ? new float?((float)num3) : ((float?)null));
			float yaw = (float)Num(dictionary2, "yaw", 45.0);
			float pitch = (float)Num(dictionary2, "pitch", 35.0);
			float dist = (float)Num(dictionary2, "dist", 12.0);
			int size = (int)Num(dictionary2, "size", ModConfig.RenderDefaultSize);
			if (!MainThreadDispatcher.RunBlocking(() => CameraRenderer.Render(x, z, y, yaw, pitch, dist, size), 20000, out var result, out var error))
			{
				return Result(id, ToolText("render timed out (game not ticking?)", isError: true));
			}
			if (error != null)
			{
				return Result(id, ToolText("render threw: " + error.Message, isError: true));
			}
			if (result?.Png == null)
			{
				return Result(id, ToolText("render failed: " + (result?.Error ?? "unknown"), isError: true));
			}
			return Result(id, ToolImage(Convert.ToBase64String(result.Png), "image/png"));
		}
		case "wait_for_log":
			return Result(id, DoWaitForLog(dictionary2, null));
		case "get_log":
			return Result(id, DoGetLog(dictionary2));
		default:
			return ErrorResponse(id, -32602, "Unknown tool: " + text);
		}
	}

	private static double Num(Dictionary<string, object> args, string key, double dflt)
	{
		if (args != null && args.TryGetValue(key, out var value) && value is double)
		{
			return (double)value;
		}
		return dflt;
	}

	private static bool IsWaitForLogCall(Dictionary<string, object> req, out object id, out Dictionary<string, object> args, out object progressToken)
	{
		id = null;
		args = null;
		progressToken = null;
		if (req == null)
		{
			return false;
		}
		if (!req.TryGetValue("method", out var value) || !(value as string == "tools/call"))
		{
			return false;
		}
		if (!req.TryGetValue("id", out id))
		{
			return false;
		}
		if (!req.TryGetValue("params", out var value2) || !(value2 is Dictionary<string, object> dictionary))
		{
			return false;
		}
		if ((dictionary.TryGetValue("name", out var value3) ? (value3 as string) : null) != "wait_for_log")
		{
			return false;
		}
		args = (dictionary.TryGetValue("arguments", out var value4) ? (value4 as Dictionary<string, object>) : null);
		if (dictionary.TryGetValue("_meta", out var value5) && value5 is Dictionary<string, object> dictionary2 && dictionary2.TryGetValue("progressToken", out var value6))
		{
			progressToken = value6;
		}
		return true;
	}

	private static string DoWaitForLog(Dictionary<string, object> args, Action<string> onLine)
	{
		object value;
		string pattern = ((args != null && args.TryGetValue("pattern", out value)) ? (value as string) : null);
		if (string.IsNullOrEmpty(pattern))
		{
			return ToolText("missing 'pattern' argument", isError: true);
		}
		bool flag = default(bool);
		int num;
		if (args.TryGetValue("regex", out var value2))
		{
			if (value2 is bool)
			{
				flag = (bool)value2;
				num = 1;
			}
			else
			{
				num = 0;
			}
		}
		else
		{
			num = 0;
		}
		int num2 = num & (flag ? 1 : 0);
		int num3 = ModConfig.ClampWaitTimeout((int)Num(args, "timeoutMs", ModConfig.WaitDefaultTimeoutMs));
		Func<string, bool> match;
		if (num2 != 0)
		{
			Regex rx;
			try
			{
				rx = new Regex(pattern);
			}
			catch (Exception ex)
			{
				return ToolText("invalid regex: " + ex.Message, isError: true);
			}
			match = (string line) => rx.IsMatch(line);
		}
		else
		{
			match = (string line) => line.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0;
		}
		bool timedOut;
		string text = LogWatch.Wait(match, num3, onLine, out timedOut);
		if (timedOut)
		{
			return ToolText($"timed out after {num3}ms waiting for log match: {pattern}", isError: true);
		}
		return ToolText("matched: " + text, isError: false);
	}

	private static string DoGetLog(Dictionary<string, object> args)
	{
		long since = (long)Num(args, "since", -1.0);
		int maxLines = ModConfig.ClampLogLines((int)Num(args, "maxLines", ModConfig.LogDefaultLines));
		object value;
		string text = ((args != null && args.TryGetValue("contains", out value)) ? (value as string) : null);
		bool flag = default(bool);
		int num;
		if (args != null && args.TryGetValue("regex", out var value2))
		{
			if (value2 is bool)
			{
				flag = (bool)value2;
				num = 1;
			}
			else
			{
				num = 0;
			}
		}
		else
		{
			num = 0;
		}
		bool flag2 = (byte)((uint)num & (flag ? 1u : 0u)) != 0;
		Func<string, bool> filter = null;
		if (!string.IsNullOrEmpty(text))
		{
			if (flag2)
			{
				Regex rx;
				try
				{
					rx = new Regex(text);
				}
				catch (Exception ex)
				{
					return ToolText("invalid regex: " + ex.Message, isError: true);
				}
				filter = (string line) => rx.IsMatch(line);
			}
			else
			{
				string needle = text;
				filter = (string line) => line.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0;
			}
		}
		long cursor;
		int totalMatching;
		int dropped;
		List<string> log = LogWatch.GetLog(since, maxLines, filter, out cursor, out totalMatching, out dropped);
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append("cursor=").Append(cursor.ToString(CultureInfo.InvariantCulture)).Append(" returned=")
			.Append(log.Count.ToString(CultureInfo.InvariantCulture))
			.Append(" matching=")
			.Append(totalMatching.ToString(CultureInfo.InvariantCulture))
			.Append(" dropped=")
			.Append(dropped.ToString(CultureInfo.InvariantCulture))
			.Append('\n');
		stringBuilder.Append((log.Count > 0) ? string.Join("\n", log) : "(no matching log lines)");
		return ToolText(stringBuilder.ToString(), isError: false);
	}

	private static void StreamWaitForLog(object id, Dictionary<string, object> args, object progressToken, IMcpTransport tx)
	{
		tx.BeginSse();
		int counter = 0;
		Timer timer = null;
		try
		{
			timer = new Timer(delegate
			{
				try
				{
					Emit("waiting…");
				}
				catch
				{
				}
			}, null, ModConfig.WaitHeartbeatMs, ModConfig.WaitHeartbeatMs);
			string resultJson = DoWaitForLog(args, delegate(string line)
			{
				try
				{
					Emit(line);
				}
				catch
				{
				}
			});
			timer.Dispose();
			timer = null;
			tx.SendSse(Result(id, resultJson));
		}
		catch (Exception ex)
		{
			tx.SendSse(Result(id, ToolText("wait_for_log threw: " + ex.Message, isError: true)));
		}
		finally
		{
			timer?.Dispose();
			tx.EndSse();
		}
		void Emit(string message)
		{
			int progress = Interlocked.Increment(ref counter);
			tx.SendSse(ProgressNotification(progressToken, progress, message));
		}
	}

	private static string ProgressNotification(object progressToken, int progress, string message)
	{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append("{\"jsonrpc\":\"2.0\",\"method\":\"notifications/progress\",\"params\":{");
		stringBuilder.Append("\"progressToken\":").Append(FormatId(progressToken));
		stringBuilder.Append(",\"progress\":").Append(progress.ToString(CultureInfo.InvariantCulture));
		if (message != null)
		{
			stringBuilder.Append(",\"message\":").Append(Json.Str(message));
		}
		stringBuilder.Append("}}");
		return stringBuilder.ToString();
	}

	private static string ToolText(string text, bool isError)
	{
		return "{\"content\":[{\"type\":\"text\",\"text\":" + Json.Str(text) + "}],\"isError\":" + (isError ? "true" : "false") + "}";
	}

	private static string ToolImage(string base64, string mimeType)
	{
		return "{\"content\":[{\"type\":\"image\",\"data\":" + Json.Str(base64) + ",\"mimeType\":" + Json.Str(mimeType) + "}],\"isError\":false}";
	}

	private static string Result(object id, string resultJson)
	{
		return "{\"jsonrpc\":\"2.0\",\"id\":" + FormatId(id) + ",\"result\":" + resultJson + "}";
	}

	private static string ErrorResponse(object id, int code, string message)
	{
		return "{\"jsonrpc\":\"2.0\",\"id\":" + FormatId(id) + ",\"error\":{\"code\":" + code.ToString(CultureInfo.InvariantCulture) + ",\"message\":" + Json.Str(message) + "}}";
	}

	private static string FormatId(object id)
	{
		if (id == null)
		{
			return "null";
		}
		if (id is string s)
		{
			return Json.Str(s);
		}
		if (id is bool)
		{
			if (!(bool)id)
			{
				return "false";
			}
			return "true";
		}
		if (id is double num)
		{
			if (!double.IsInfinity(num) && !double.IsNaN(num) && Math.Abs(num - Math.Floor(num)) < double.Epsilon && Math.Abs(num) < 9.2E+18)
			{
				return ((long)num).ToString(CultureInfo.InvariantCulture);
			}
			return num.ToString("R", CultureInfo.InvariantCulture);
		}
		return Json.Str(id.ToString());
	}
}
internal static class MiniJson
{
	public static object Parse(string text)
	{
		int i = 0;
		object result = ParseValue(text, ref i);
		SkipWs(text, ref i);
		return result;
	}

	private static object ParseValue(string s, ref int i)
	{
		SkipWs(s, ref i);
		if (i >= s.Length)
		{
			throw new FormatException("Unexpected end of JSON");
		}
		switch (s[i])
		{
		case '{':
			return ParseObject(s, ref i);
		case '[':
			return ParseArray(s, ref i);
		case '"':
			return ParseString(s, ref i);
		case 't':
			Expect(s, ref i, "true");
			return true;
		case 'f':
			Expect(s, ref i, "false");
			return false;
		case 'n':
			Expect(s, ref i, "null");
			return null;
		default:
			return ParseNumber(s, ref i);
		}
	}

	private static Dictionary<string, object> ParseObject(string s, ref int i)
	{
		Dictionary<string, object> dictionary = new Dictionary<string, object>();
		i++;
		SkipWs(s, ref i);
		if (i < s.Length && s[i] == '}')
		{
			i++;
			return dictionary;
		}
		while (true)
		{
			SkipWs(s, ref i);
			if (i >= s.Length || s[i] != '"')
			{
				throw new FormatException("Expected string key");
			}
			string key = ParseString(s, ref i);
			SkipWs(s, ref i);
			if (i >= s.Length || s[i] != ':')
			{
				throw new FormatException("Expected ':'");
			}
			i++;
			dictionary[key] = ParseValue(s, ref i);
			SkipWs(s, ref i);
			if (i >= s.Length)
			{
				throw new FormatException("Unterminated object");
			}
			if (s[i] != ',')
			{
				break;
			}
			i++;
		}
		if (s[i] == '}')
		{
			i++;
			return dictionary;
		}
		throw new FormatException("Expected ',' or '}'");
	}

	private static List<object> ParseArray(string s, ref int i)
	{
		List<object> list = new List<object>();
		i++;
		SkipWs(s, ref i);
		if (i < s.Length && s[i] == ']')
		{
			i++;
			return list;
		}
		while (true)
		{
			list.Add(ParseValue(s, ref i));
			SkipWs(s, ref i);
			if (i >= s.Length)
			{
				throw new FormatException("Unterminated array");
			}
			if (s[i] != ',')
			{
				break;
			}
			i++;
		}
		if (s[i] == ']')
		{
			i++;
			return list;
		}
		throw new FormatException("Expected ',' or ']'");
	}

	private static string ParseString(string s, ref int i)
	{
		StringBuilder stringBuilder = new StringBuilder();
		i++;
		while (i < s.Length)
		{
			char c = s[i++];
			switch (c)
			{
			case '"':
				return stringBuilder.ToString();
			case '\\':
				break;
			default:
				stringBuilder.Append(c);
				continue;
			}
			if (i >= s.Length)
			{
				break;
			}
			char c2 = s[i++];
			switch (c2)
			{
			case '"':
				stringBuilder.Append('"');
				break;
			case '\\':
				stringBuilder.Append('\\');
				break;
			case '/':
				stringBuilder.Append('/');
				break;
			case 'b':
				stringBuilder.Append('\b');
				break;
			case 'f':
				stringBuilder.Append('\f');
				break;
			case 'n':
				stringBuilder.Append('\n');
				break;
			case 'r':
				stringBuilder.Append('\r');
				break;
			case 't':
				stringBuilder.Append('\t');
				break;
			case 'u':
				if (i + 4 > s.Length)
				{
					throw new FormatException("Bad \\u escape");
				}
				stringBuilder.Append((char)int.Parse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
				i += 4;
				break;
			default:
				throw new FormatException("Bad escape: \\" + c2);
			}
		}
		throw new FormatException("Unterminated string");
	}

	private static double ParseNumber(string s, ref int i)
	{
		int num = i;
		while (i < s.Length && "+-0123456789.eE".IndexOf(s[i]) >= 0)
		{
			i++;
		}
		string text = s.Substring(num, i - num);
		if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
		{
			throw new FormatException("Bad number: " + text);
		}
		return result;
	}

	private static void Expect(string s, ref int i, string literal)
	{
		if (i + literal.Length > s.Length || s.Substring(i, literal.Length) != literal)
		{
			throw new FormatException("Expected '" + literal + "'");
		}
		i += literal.Length;
	}

	private static void SkipWs(string s, ref int i)
	{
		while (i < s.Length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r'))
		{
			i++;
		}
	}
}
internal sealed class MiniYaml
{
	private readonly Dictionary<string, string> _scalars = new Dictionary<string, string>();

	private readonly Dictionary<string, List<string>> _lists = new Dictionary<string, List<string>>();

	public static MiniYaml Parse(string text)
	{
		MiniYaml miniYaml = new MiniYaml();
		string text2 = null;
		string text3 = null;
		string[] array = text.Replace("\r\n", "\n").Split(new char[1] { '\n' });
		for (int i = 0; i < array.Length; i++)
		{
			string text4 = StripComment(array[i]);
			string text5 = text4.Trim();
			if (text5.Length == 0)
			{
				continue;
			}
			int num = text4.Length - text4.TrimStart(new char[1] { ' ' }).Length;
			if (text5[0] == '-')
			{
				if (text3 != null)
				{
					string text6 = Unquote(text5.Substring(1).Trim());
					if (text6.Length > 0)
					{
						miniYaml._lists[text3].Add(text6);
					}
				}
				continue;
			}
			int num2 = text5.IndexOf(':');
			if (num2 < 0)
			{
				continue;
			}
			string text7 = text5.Substring(0, num2).Trim();
			string text8 = text5.Substring(num2 + 1).Trim();
			if (num == 0)
			{
				text3 = null;
				if (text8.Length == 0)
				{
					text2 = text7;
					continue;
				}
				miniYaml._scalars[text7] = Unquote(text8);
				text2 = null;
				continue;
			}
			string text9 = ((text2 != null) ? (text2 + "." + text7) : text7);
			if (text8.Length == 0)
			{
				text3 = text9;
				if (!miniYaml._lists.ContainsKey(text9))
				{
					miniYaml._lists[text9] = new List<string>();
				}
			}
			else if (text8.StartsWith("[") && text8.EndsWith("]"))
			{
				string text10 = text8.Substring(1, text8.Length - 2);
				List<string> list = new List<string>();
				string[] array2 = text10.Split(new char[1] { ',' });
				for (int j = 0; j < array2.Length; j++)
				{
					string text11 = Unquote(array2[j].Trim());
					if (text11.Length > 0)
					{
						list.Add(text11);
					}
				}
				miniYaml._lists[text9] = list;
				text3 = null;
			}
			else
			{
				miniYaml._scalars[text9] = Unquote(text8);
				text3 = null;
			}
		}
		return miniYaml;
	}

	public string Get(string path, string dflt)
	{
		if (!_scalars.TryGetValue(path, out var value))
		{
			return dflt;
		}
		return value;
	}

	public int GetInt(string path, int dflt)
	{
		if (!_scalars.TryGetValue(path, out var value) || !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
		{
			return dflt;
		}
		return result;
	}

	public List<string> GetList(string path)
	{
		if (!_lists.TryGetValue(path, out var value))
		{
			return new List<string>();
		}
		return value;
	}

	private static string StripComment(string line)
	{
		string text = line.TrimStart(Array.Empty<char>());
		if (text.Length > 0 && text[0] == '#')
		{
			return "";
		}
		int num = line.IndexOf(" #", StringComparison.Ordinal);
		if (num < 0)
		{
			return line;
		}
		return line.Substring(0, num);
	}

	private static string Unquote(string s)
	{
		if (s.Length >= 2 && ((s[0] == '"' && s[s.Length - 1] == '"') || (s[0] == '\'' && s[s.Length - 1] == '\'')))
		{
			return s.Substring(1, s.Length - 2);
		}
		return s;
	}
}
internal static class ModConfig
{
	public const string FileName = "valheimmcp.yml";

	public static string Host = "127.0.0.1";

	public static int Port = 8731;

	public static int CommandTimeoutMs = 15000;

	public static int RenderDefaultSize = 768;

	public static int RenderMinSize = 256;

	public static int RenderMaxSize = 1280;

	public static int WaitDefaultTimeoutMs = 120000;

	public static int WaitMaxTimeoutMs = 600000;

	public static int WaitHeartbeatMs = 5000;

	public static int LogBufferCapacity = 2000;

	public static int LogDefaultLines = 200;

	public static int LogMaxLines = 1000;

	private static List<string> _allow = new List<string>();

	private static List<string> _deny = new List<string>();

	private const string DefaultYaml = "# ValheimMCP configuration (YAML). Full-line comments (#) only.\n\nserver:\n  host: 127.0.0.1          # loopback only — endpoint is unauthenticated, keep it local\n  port: 8731\n  commandTimeoutMs: 15000  # max wait for a command to run on the main thread\n\nrender:\n  defaultSize: 768         # render_view size (px, square) when 'size' is omitted\n  minSize: 256\n  maxSize: 1280\n\nwait:\n  defaultTimeoutMs: 120000 # wait_for_log timeout when 'timeoutMs' is omitted\n  maxTimeoutMs: 600000     # hard cap on any wait_for_log request\n  heartbeatMs: 5000        # SSE progress heartbeat interval while a wait is pending\n\nlog:\n  bufferCapacity: 2000     # in-memory ring of recent log lines retained for get_log\n  defaultLines: 200        # get_log lines returned when 'maxLines' is omitted\n  maxLines: 1000           # hard cap on lines per get_log request\n\n# Access control for run_command (and POST /command). 'deny' always wins. If\n# 'allow' is non-empty, ONLY matching commands may run. Match is by command name;\n# a trailing '*' is a prefix wildcard, e.g. \"spawn*\" matches every spawn command.\ncommands:\n  allow: []\n  deny: []\n";

	public static void Load()
	{
		try
		{
			string text = Path.Combine(Paths.ConfigPath, "valheimmcp.yml");
			if (!File.Exists(text))
			{
				File.WriteAllText(text, "# ValheimMCP configuration (YAML). Full-line comments (#) only.\n\nserver:\n  host: 127.0.0.1          # loopback only — endpoint is unauthenticated, keep it local\n  port: 8731\n  commandTimeoutMs: 15000  # max wait for a command to run on the main thread\n\nrender:\n  defaultSize: 768         # render_view size (px, square) when 'size' is omitted\n  minSize: 256\n  maxSize: 1280\n\nwait:\n  defaultTimeoutMs: 120000 # wait_for_log timeout when 'timeoutMs' is omitted\n  maxTimeoutMs: 600000     # hard cap on any wait_for_log request\n  heartbeatMs: 5000        # SSE progress heartbeat interval while a wait is pending\n\nlog:\n  bufferCapacity: 2000     # in-memory ring of recent log lines retained for get_log\n  defaultLines: 200        # get_log lines returned when 'maxLines' is omitted\n  maxLines: 1000           # hard cap on lines per get_log request\n\n# Access control for run_command (and POST /command). 'deny' always wins. If\n# 'allow' is non-empty, ONLY matching commands may run. Match is by command name;\n# a trailing '*' is a prefix wildcard, e.g. \"spawn*\" matches every spawn command.\ncommands:\n  allow: []\n  deny: []\n");
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogInfo((object)("[ValheimMCP] wrote default config: " + text));
				}
			}
			MiniYaml miniYaml = MiniYaml.Parse(File.ReadAllText(text));
			Host = miniYaml.Get("server.host", Host);
			Port = miniYaml.GetInt("server.port", Port);
			CommandTimeoutMs = miniYaml.GetInt("server.commandTimeoutMs", CommandTimeoutMs);
			RenderDefaultSize = miniYaml.GetInt("render.defaultSize", RenderDefaultSize);
			RenderMinSize = miniYaml.GetInt("render.minSize", RenderMinSize);
			RenderMaxSize = miniYaml.GetInt("render.maxSize", RenderMaxSize);
			WaitDefaultTimeoutMs = miniYaml.GetInt("wait.defaultTimeoutMs", WaitDefaultTimeoutMs);
			WaitMaxTimeoutMs = miniYaml.GetInt("wait.maxTimeoutMs", WaitMaxTimeoutMs);
			WaitHeartbeatMs = miniYaml.GetInt("wait.heartbeatMs", WaitHeartbeatMs);
			LogBufferCapacity = miniYaml.GetInt("log.bufferCapacity", LogBufferCapacity);
			LogDefaultLines = miniYaml.GetInt("log.defaultLines", LogDefaultLines);
			LogMaxLines = miniYaml.GetInt("log.maxLines", LogMaxLines);
			_allow = miniYaml.GetList("commands.allow");
			_deny = miniYaml.GetList("commands.deny");
			ManualLogSource log2 = Plugin.Log;
			if (log2 != null)
			{
				log2.LogInfo((object)($"[ValheimMCP] config: {Host}:{Port}, render {RenderMinSize}-{RenderMaxSize} " + $"(default {RenderDefaultSize}), allow={_allow.Count} deny={_deny.Count}"));
			}
		}
		catch (Exception ex)
		{
			ManualLogSource log3 = Plugin.Log;
			if (log3 != null)
			{
				log3.LogError((object)("[ValheimMCP] failed to load config, using defaults: " + ex.Message));
			}
		}
	}

	public static int ClampRenderSize(int requested)
	{
		if (requested <= 0)
		{
			requested = RenderDefaultSize;
		}
		int val = Math.Max(64, RenderMinSize);
		return Math.Min(Math.Max(val, RenderMaxSize), Math.Max(val, requested));
	}

	public static int ClampWaitTimeout(int requested)
	{
		if (requested <= 0)
		{
			requested = WaitDefaultTimeoutMs;
		}
		return Math.Min(WaitMaxTimeoutMs, Math.Max(1000, requested));
	}

	public static int ClampLogLines(int requested)
	{
		if (requested <= 0)
		{
			requested = LogDefaultLines;
		}
		return Math.Min(Math.Max(1, Math.Min(LogMaxLines, LogBufferCapacity)), Math.Max(1, requested));
	}

	public static bool IsCommandAllowed(string commandLine, out string reason)
	{
		reason = null;
		string text = (commandLine ?? "").Trim();
		int num = text.IndexOfAny(new char[2] { ' ', '\t' });
		if (num >= 0)
		{
			text = text.Substring(0, num);
		}
		text = text.ToLowerInvariant();
		if (Matches(_deny, text))
		{
			reason = "command '" + text + "' is denied by config (commands.deny)";
			return false;
		}
		if (_allow.Count > 0 && !Matches(_allow, text))
		{
			reason = "command '" + text + "' is not in the config allowlist (commands.allow)";
			return false;
		}
		return true;
	}

	private static bool Matches(List<string> patterns, string name)
	{
		foreach (string pattern in patterns)
		{
			string text = pattern.ToLowerInvariant();
			if (text.EndsWith("*"))
			{
				if (name.StartsWith(text.Substring(0, text.Length - 1)))
				{
					return true;
				}
			}
			else if (name == text)
			{
				return true;
			}
		}
		return false;
	}
}
[BepInPlugin("com.valheimmcp.server", "Valheim MCP Server", "0.2.0")]
public class Plugin : BaseUnityPlugin
{
	private Harmony _harmony;

	private HttpServer _server;

	public static ManualLogSource Log { get; private set; }

	private void Awake()
	{
		//IL_0016: Unknown result type (might be due to invalid IL or missing references)
		//IL_0020: Expected O, but got Unknown
		Log = ((BaseUnityPlugin)this).Logger;
		ModConfig.Load();
		_harmony = new Harmony("com.valheimmcp.server");
		_harmony.PatchAll(typeof(ConsoleOutputCapture));
		LogWatch.Install();
		string text = $"http://{ModConfig.Host}:{ModConfig.Port}/";
		try
		{
			_server = new HttpServer(text, ModConfig.CommandTimeoutMs);
			_server.Start();
			Log.LogInfo((object)("Valheim MCP Server v0.2.0 listening on " + text));
		}
		catch (Exception arg)
		{
			Log.LogError((object)string.Format("{0} failed to start on {1}: {2}", "Valheim MCP Server", text, arg));
		}
	}

	private void Update()
	{
		MainThreadDispatcher.Pump();
	}

	private void OnDestroy()
	{
		_server?.Stop();
		LogWatch.Uninstall();
		Harmony harmony = _harmony;
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
		ManualLogSource log = Log;
		if (log != null)
		{
			log.LogInfo((object)"Valheim MCP Server stopped.");
		}
	}
}
internal static class PluginInfo
{
	public const string Guid = "com.valheimmcp.server";

	public const string Name = "Valheim MCP Server";

	public const string Version = "0.2.0";
}