Decompiled source of ValheimMCP v0.1.0

plugins/ValheimMCP/ValheimMCP.dll

Decompiled 11 hours ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
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.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.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+1050b1c647d83d8e7bf4868e987daee96df7d72f")]
[assembly: AssemblyProduct("ValheimMCP")]
[assembly: AssemblyTitle("ValheimMCP")]
[assembly: AssemblyVersion("0.1.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 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 context;
			try
			{
				context = _listener.GetContext();
			}
			catch
			{
				if (!_running)
				{
					break;
				}
				continue;
			}
			try
			{
				Handle(context);
			}
			catch (Exception ex)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogError((object)$"[ValheimMCP] request handler threw: {ex}");
				}
				Write(context, 500, Json.Error(ex.Message));
			}
		}
	}

	private void Handle(HttpListenerContext ctx)
	{
		string text2 = ctx.Request.Url.AbsolutePath.TrimEnd(new char[1] { '/' });
		string httpMethod = ctx.Request.HttpMethod;
		switch (text2)
		{
		case "/mcp":
			if (httpMethod == "POST")
			{
				McpHttpReply mcpHttpReply = McpServer.HandleHttp(ReadBody(ctx.Request), _commandTimeoutMs);
				if (mcpHttpReply.HasBody)
				{
					Write(ctx, mcpHttpReply.Status, mcpHttpReply.Body);
				}
				else
				{
					WriteEmpty(ctx, mcpHttpReply.Status);
				}
			}
			else if (httpMethod == "DELETE")
			{
				WriteEmpty(ctx, 200);
			}
			else
			{
				Write(ctx, 405, Json.Error("MCP endpoint supports POST only (no SSE stream)"));
			}
			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 (text2 == "/command" && httpMethod == "POST")
		{
			string text = ReadCommandText(ctx.Request);
			CommandResult result3;
			Exception error3;
			if (string.IsNullOrWhiteSpace(text))
			{
				Write(ctx, 400, Json.Error("missing command text (send as raw body or ?text=)"));
			}
			else if (!MainThreadDispatcher.RunBlocking(() => ConsoleBridge.Run(text), _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(text, result3));
			}
		}
		else
		{
			Write(ctx, 404, Json.Error("unknown route: " + httpMethod + " " + text2));
		}
	}

	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 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 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 struct McpHttpReply
{
	public int Status;

	public string Body;

	public bool HasBody;
}
internal static class McpServer
{
	public const string ServerName = "valheim-mcp";

	public const string ServerVersion = "0.1.0";

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

	public static McpHttpReply HandleHttp(string body, int commandTimeoutMs)
	{
		object obj;
		try
		{
			obj = MiniJson.Parse(body);
		}
		catch (Exception ex)
		{
			return Json200(ErrorResponse(null, -32700, "Parse error: " + ex.Message));
		}
		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)
			{
				return Accepted();
			}
			return Json200(stringBuilder.ToString());
		}
		string text2 = HandleOne(obj as Dictionary<string, object>, commandTimeoutMs);
		if (text2 != null)
		{
			return Json200(text2);
		}
		return Accepted();
	}

	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 text && text.Length > 0)
		{
			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.1.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("]}");
		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 text2 = (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(text2))
		{
			return ErrorResponse(id, -32602, "Missing tool name");
		}
		switch (text2)
		{
		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 text = ((dictionary2 != null && dictionary2.TryGetValue("text", out value7)) ? (value7 as string) : null);
			if (string.IsNullOrWhiteSpace(text))
			{
				return Result(id, ToolText("missing 'text' argument", isError: true));
			}
			if (!MainThreadDispatcher.RunBlocking(() => ConsoleBridge.Run(text), 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) : 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"));
		}
		default:
			return ErrorResponse(id, -32602, "Unknown tool: " + text2);
		}
	}

	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 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) && num == Math.Floor(num) && Math.Abs(num) < 9.2E+18)
			{
				return ((long)num).ToString(CultureInfo.InvariantCulture);
			}
			return num.ToString("R", CultureInfo.InvariantCulture);
		}
		return Json.Str(id.ToString());
	}

	private static McpHttpReply Json200(string body)
	{
		McpHttpReply result = default(McpHttpReply);
		result.Status = 200;
		result.Body = body;
		result.HasBody = true;
		return result;
	}

	private static McpHttpReply Accepted()
	{
		McpHttpReply result = default(McpHttpReply);
		result.Status = 202;
		result.Body = null;
		result.HasBody = false;
		return result;
	}
}
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;

	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\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\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);
			_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 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.1.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));
		string text = $"http://{ModConfig.Host}:{ModConfig.Port}/";
		try
		{
			_server = new HttpServer(text, ModConfig.CommandTimeoutMs);
			_server.Start();
			Log.LogInfo((object)("Valheim MCP Server v0.1.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();
		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.1.0";
}