Index: binary-improvements2/WebServer/ModInfo.xml
===================================================================
--- binary-improvements2/WebServer/ModInfo.xml	(revision 401)
+++ binary-improvements2/WebServer/ModInfo.xml	(revision 402)
@@ -1,10 +1,9 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <xml>
-	<ModInfo>
-		<Name value="TFP WebServer" />
-		<Description value="WebServer implementation" />
-		<Author value="The Fun Pimps LLC" />
-		<Version value="1" />
-		<Website value="" />
-	</ModInfo>
+	<Name value="TFP_WebServer" />
+	<DisplayName value="Web Dashboard" />
+	<Description value="Integrated Webserver for the Web Dashboard and server APIs" />
+	<Author value="The Fun Pimps LLC" />
+	<Version value="21.0" />
+	<Website value="" />
 </xml>
Index: binary-improvements2/WebServer/WebServer.csproj
===================================================================
--- binary-improvements2/WebServer/WebServer.csproj	(revision 401)
+++ binary-improvements2/WebServer/WebServer.csproj	(revision 402)
@@ -11,5 +11,5 @@
     <AssemblyName>WebServer</AssemblyName>
     <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
-    <LangVersion>8</LangVersion>
+    <LangVersion>9</LangVersion>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
@@ -45,4 +45,8 @@
     <Reference Include="Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
       <HintPath>..\7dtd-binaries\Assembly-CSharp-firstpass.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="com.rlabrecque.steamworks.net, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>..\7dtd-binaries\com.rlabrecque.steamworks.net.dll</HintPath>
       <Private>False</Private>
     </Reference>
@@ -83,6 +87,17 @@
       <Private>False</Private>
     </Reference>
+    <Reference Include="Utf8Json, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>..\7dtd-binaries\Utf8Json.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="src\FileCache\AbstractCache.cs" />
+    <Compile Include="src\FileCache\DirectAccess.cs" />
+    <Compile Include="src\FileCache\InvalidateCachesCmd.cs" />
+    <Compile Include="src\FileCache\SimpleCache.cs" />
+    <Compile Include="src\LiveData\Animals.cs" />
+    <Compile Include="src\LiveData\EntityFilterList.cs" />
+    <Compile Include="src\LiveData\Hostiles.cs" />
     <Compile Include="src\ModApi.cs" />
     <Compile Include="src\UrlHandlers\ApiHandler.cs" />
@@ -90,19 +105,19 @@
     <Compile Include="src\WebAPI\AbsRestApi.cs" />
     <Compile Include="src\WebAPI\AbsWebAPI.cs" />
-    <Compile Include="src\WebAPI\ExecuteConsoleCommand.cs" />
-    <Compile Include="src\WebAPI\GetAllowedCommands.cs" />
-    <Compile Include="src\WebAPI\GetAnimalsLocation.cs" />
-    <Compile Include="src\WebAPI\GetHostileLocation.cs" />
-    <Compile Include="src\WebAPI\GetLandClaims.cs" />
-    <Compile Include="src\WebAPI\GetLog.cs" />
-    <Compile Include="src\WebAPI\GetPlayerInventories.cs" />
-    <Compile Include="src\WebAPI\GetPlayerInventory.cs" />
-    <Compile Include="src\WebAPI\GetPlayerList.cs" />
-    <Compile Include="src\WebAPI\GetPlayersLocation.cs" />
-    <Compile Include="src\WebAPI\GetPlayersOnline.cs" />
-    <Compile Include="src\WebAPI\GetServerInfo.cs" />
-    <Compile Include="src\WebAPI\GetStats.cs" />
-    <Compile Include="src\WebAPI\GetWebMods.cs" />
-    <Compile Include="src\WebAPI\GetWebUIUpdates.cs" />
+    <Compile Include="src\WebAPI\APIs\Animal.cs" />
+    <Compile Include="src\WebAPI\APIs\Command.cs" />
+    <Compile Include="src\WebAPI\APIs\GetLandClaims.cs" />
+    <Compile Include="src\WebAPI\APIs\GetPlayerInventories.cs" />
+    <Compile Include="src\WebAPI\APIs\GetPlayerInventory.cs" />
+    <Compile Include="src\WebAPI\APIs\GetPlayerList.cs" />
+    <Compile Include="src\WebAPI\APIs\GetPlayersLocation.cs" />
+    <Compile Include="src\WebAPI\APIs\GetPlayersOnline.cs" />
+    <Compile Include="src\WebAPI\APIs\Hostile.cs" />
+    <Compile Include="src\WebAPI\APIs\Log.cs" />
+    <Compile Include="src\WebAPI\APIs\ServerInfo.cs" />
+    <Compile Include="src\WebAPI\APIs\ServerStats.cs" />
+    <Compile Include="src\WebAPI\APIs\WebMods.cs" />
+    <Compile Include="src\WebAPI\APIs\WebUiUpdates.cs" />
+    <Compile Include="src\WebAPI\JsonCommons.cs" />
     <Compile Include="src\WebAPI\Null.cs" />
     <Compile Include="src\AssemblyInfo.cs" />
@@ -145,14 +160,4 @@
   </ItemGroup>
   <ItemGroup>
-    <ProjectReference Include="..\7dtd-server-fixes\7dtd-server-fixes.csproj">
-      <Project>{81da7f87-1a66-4920-aada-6eaf1971f8d0}</Project>
-      <Name>7dtd-server-fixes</Name>
-      <Private>False</Private>
-    </ProjectReference>
-    <ProjectReference Include="..\MapRendering\MapRendering.csproj">
-      <Project>{a1847b5f-7bfc-4bcd-94aa-a6c9fb7e7c54}</Project>
-      <Name>MapRendering</Name>
-      <Private>False</Private>
-    </ProjectReference>
     <ProjectReference Include="..\SpaceWizards.HttpListener\SpaceWizards.HttpListener.csproj">
       <Project>{1c5368e1-a4cf-4580-86bb-dffb20ab682c}</Project>
Index: binary-improvements2/WebServer/src/Commands/EnableOpenIDDebug.cs
===================================================================
--- binary-improvements2/WebServer/src/Commands/EnableOpenIDDebug.cs	(revision 401)
+++ binary-improvements2/WebServer/src/Commands/EnableOpenIDDebug.cs	(revision 402)
@@ -15,10 +15,10 @@
 		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
 			if (_params.Count != 1) {
-				SdtdConsole.Instance.Output ("Current state: " + OpenID.debugOpenId);
+				SdtdConsole.Instance.Output ($"Current state: {OpenID.debugOpenId}");
 				return;
 			}
 
 			OpenID.debugOpenId = _params [0].Equals ("1");
-			SdtdConsole.Instance.Output ("Set OpenID debugging to " + _params [0].Equals ("1"));
+			SdtdConsole.Instance.Output ($"Set OpenID debugging to {_params [0].Equals ("1")}");
 		}
 	}
Index: binary-improvements2/WebServer/src/Commands/WebTokens.cs
===================================================================
--- binary-improvements2/WebServer/src/Commands/WebTokens.cs	(revision 401)
+++ binary-improvements2/WebServer/src/Commands/WebTokens.cs	(revision 402)
@@ -33,5 +33,5 @@
 					ExecuteList ();
 				} else {
-					SdtdConsole.Instance.Output ("Invalid sub command \"" + _params [0] + "\".");
+					SdtdConsole.Instance.Output ($"Invalid sub command \"{_params [0]}\".");
 				}
 			} else {
@@ -42,5 +42,5 @@
 		private void ExecuteAdd (List<string> _params) {
 			if (_params.Count != 4) {
-				SdtdConsole.Instance.Output ("Wrong number of arguments, expected 4, found " + _params.Count + ".");
+				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 4, found {_params.Count}.");
 				return;
 			}
@@ -79,5 +79,5 @@
 		private void ExecuteRemove (List<string> _params) {
 			if (_params.Count != 2) {
-				SdtdConsole.Instance.Output ("Wrong number of arguments, expected 2, found " + _params.Count + ".");
+				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 2, found {_params.Count}.");
 				return;
 			}
Index: binary-improvements2/WebServer/src/ConnectionHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/ConnectionHandler.cs	(revision 401)
+++ binary-improvements2/WebServer/src/ConnectionHandler.cs	(revision 402)
@@ -2,5 +2,4 @@
 using System.Collections.Generic;
 using System.Net;
-using Platform.Steam;
 
 namespace Webserver {
@@ -19,6 +18,5 @@
 
 			if (!Equals (con.Endpoint, _ip)) {
-				// Fixed: Allow different clients from same NAT network
-//				connections.Remove (_sessionId);
+				connections.Remove (_sessionId);
 				return null;
 			}
@@ -33,8 +31,7 @@
 		}
 
-		public WebConnection LogIn (ulong _steamId, IPAddress _ip) {
+		public WebConnection LogIn (PlatformUserIdentifierAbs _userId, IPAddress _ip) {
 			string sessionId = Guid.NewGuid ().ToString ();
-			PlatformUserIdentifierAbs userId = new UserIdentifierSteam (_steamId);
-			WebConnection con = new WebConnection (sessionId, _ip, userId);
+			WebConnection con = new WebConnection (sessionId, _ip, _userId);
 			connections.Add (sessionId, con);
 			return con;
Index: binary-improvements2/WebServer/src/FileCache/AbstractCache.cs
===================================================================
--- binary-improvements2/WebServer/src/FileCache/AbstractCache.cs	(revision 402)
+++ binary-improvements2/WebServer/src/FileCache/AbstractCache.cs	(revision 402)
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+
+namespace Webserver.FileCache {
+	public abstract class AbstractCache {
+		public abstract byte[] GetFileContent (string _filename);
+		public abstract (int filesDropped, int bytesDropped) Invalidate ();
+
+		protected AbstractCache () {
+			caches.Add (this);
+		}
+
+		private static readonly List<AbstractCache> caches = new List<AbstractCache> ();
+		public static (int, int) InvalidateAllCaches () {
+			int filesDropped = 0;
+			int bytesDropped = 0;
+			
+			foreach (AbstractCache cache in caches) {
+				(int files, int bytes) = cache.Invalidate ();
+				filesDropped += files;
+				bytesDropped += bytes;
+			}
+
+			return (filesDropped, bytesDropped);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/FileCache/DirectAccess.cs
===================================================================
--- binary-improvements2/WebServer/src/FileCache/DirectAccess.cs	(revision 402)
+++ binary-improvements2/WebServer/src/FileCache/DirectAccess.cs	(revision 402)
@@ -0,0 +1,21 @@
+using System;
+using System.IO;
+
+namespace Webserver.FileCache {
+	// Not caching at all, simply reading from disk on each request
+	public class DirectAccess : AbstractCache {
+		public override byte[] GetFileContent (string _filename) {
+			try {
+				return File.Exists (_filename) ? File.ReadAllBytes (_filename) : null;
+			} catch (Exception e) {
+				Log.Out ($"Error in DirectAccess.GetFileContent: {e}");
+			}
+
+			return null;
+		}
+
+		public override (int, int) Invalidate () {
+			return (0, 0);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/FileCache/InvalidateCachesCmd.cs
===================================================================
--- binary-improvements2/WebServer/src/FileCache/InvalidateCachesCmd.cs	(revision 402)
+++ binary-improvements2/WebServer/src/FileCache/InvalidateCachesCmd.cs	(revision 402)
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace Webserver.FileCache {
+	[UsedImplicitly]
+	public class InvalidateCachesCmd : ConsoleCmdAbstract {
+		public override string[] GetCommands () {
+			return new[] {"invalidatecaches"};
+		}
+
+		public override string GetDescription () {
+			return "Invalidate contents of web file caches";
+		}
+
+		public override string GetHelp () {
+			return "TODO";
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			(int files, int bytes) = AbstractCache.InvalidateAllCaches ();
+			SdtdConsole.Instance.Output ($"Caches invalidated, dropped {files} files with {bytes} Bytes");
+		}
+
+	}
+}
Index: binary-improvements2/WebServer/src/FileCache/SimpleCache.cs
===================================================================
--- binary-improvements2/WebServer/src/FileCache/SimpleCache.cs	(revision 402)
+++ binary-improvements2/WebServer/src/FileCache/SimpleCache.cs	(revision 402)
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Webserver.FileCache {
+	// Caching all files, useful for completely static folders only
+	public class SimpleCache : AbstractCache {
+		private readonly Dictionary<string, byte[]> fileCache = new Dictionary<string, byte[]> ();
+
+		public override byte[] GetFileContent (string _filename) {
+			try {
+				lock (fileCache) {
+					if (fileCache.ContainsKey (_filename)) {
+						return fileCache [_filename];
+					}
+
+					if (!File.Exists (_filename)) {
+						return null;
+					}
+
+					fileCache.Add (_filename, File.ReadAllBytes (_filename));
+
+					return fileCache [_filename];
+				}
+			} catch (Exception e) {
+				Log.Out ($"Error in SimpleCache.GetFileContent: {e}");
+			}
+
+			return null;
+		}
+
+		public override (int, int) Invalidate () {
+			(int, int) result = (0, 0);
+			
+			lock (fileCache) {
+				result.Item1 = fileCache.Count;
+				foreach ((string _, byte[] data) in fileCache) {
+					result.Item2 += data.Length;
+				}
+				
+				fileCache.Clear ();
+			}
+
+			return result;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/LiveData/Animals.cs
===================================================================
--- binary-improvements2/WebServer/src/LiveData/Animals.cs	(revision 402)
+++ binary-improvements2/WebServer/src/LiveData/Animals.cs	(revision 402)
@@ -0,0 +1,13 @@
+﻿namespace Webserver.LiveData {
+	public class Animals : EntityFilterList<EntityAnimal> {
+		public static readonly Animals Instance = new Animals ();
+
+		protected override EntityAnimal predicate (Entity _e) {
+			if (_e is EntityAnimal ea && ea.IsAlive ()) {
+				return ea;
+			}
+
+			return null;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/LiveData/EntityFilterList.cs
===================================================================
--- binary-improvements2/WebServer/src/LiveData/EntityFilterList.cs	(revision 402)
+++ binary-improvements2/WebServer/src/LiveData/EntityFilterList.cs	(revision 402)
@@ -0,0 +1,43 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Webserver.LiveData {
+	public abstract class EntityFilterList<T> where T : Entity {
+		public void Get (List<T> _list) {
+			_list.Clear ();
+			try {
+				List<Entity> entities = GameManager.Instance.World.Entities.list;
+				for (int i = 0; i < entities.Count; i++) {
+					Entity entity = entities [i];
+
+					T element = predicate (entity);
+					if (element != null) {
+						_list.Add (element);
+					}
+				}
+			} catch (Exception e) {
+				Log.Exception (e);
+			}
+		}
+
+		public int GetCount () {
+			int count = 0;
+			try {
+				List<Entity> entities = GameManager.Instance.World.Entities.list;
+				for (int i = 0; i < entities.Count; i++) {
+					Entity entity = entities [i];
+
+					if (predicate (entity) != null) {
+						count++;
+					}
+				}
+			} catch (Exception e) {
+				Log.Exception (e);
+			}
+
+			return count;
+		}
+
+		protected abstract T predicate (Entity _e);
+	}
+}
Index: binary-improvements2/WebServer/src/LiveData/Hostiles.cs
===================================================================
--- binary-improvements2/WebServer/src/LiveData/Hostiles.cs	(revision 402)
+++ binary-improvements2/WebServer/src/LiveData/Hostiles.cs	(revision 402)
@@ -0,0 +1,13 @@
+﻿namespace Webserver.LiveData {
+	public class Hostiles : EntityFilterList<EntityEnemy> {
+		public static readonly Hostiles Instance = new Hostiles ();
+
+		protected override EntityEnemy predicate (Entity _e) {
+			if (_e is EntityEnemy enemy && enemy.IsAlive ()) {
+				return enemy;
+			}
+
+			return null;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/MimeType.cs
===================================================================
--- binary-improvements2/WebServer/src/MimeType.cs	(revision 401)
+++ binary-improvements2/WebServer/src/MimeType.cs	(revision 402)
@@ -574,5 +574,5 @@
 
 			if (!_extension.StartsWith (".")) {
-				_extension = "." + _extension;
+				_extension = $".{_extension}";
 			}
 
Index: binary-improvements2/WebServer/src/OpenID.cs
===================================================================
--- binary-improvements2/WebServer/src/OpenID.cs	(revision 401)
+++ binary-improvements2/WebServer/src/OpenID.cs	(revision 402)
@@ -19,10 +19,8 @@
 
 		private static readonly X509Certificate2 caCert =
-			new X509Certificate2 (Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) +
-			                      "/steam-rootca.cer");
+			new X509Certificate2 ($"{Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location)}/steam-rootca.cer");
 
 		private static readonly X509Certificate2 caIntermediateCert =
-			new X509Certificate2 (Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) +
-			                      "/steam-intermediate.cer");
+			new X509Certificate2 ($"{Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location)}/steam-intermediate.cer");
 
 		private const bool verboseSsl = false;
@@ -74,5 +72,5 @@
 				foreach (X509ChainElement chainEl in privateChain.ChainElements) {
 					if (verboseSsl) {
-						Log.Out ("Validating cert: " + chainEl.Certificate.Subject);
+						Log.Out ($"Validating cert: {chainEl.Certificate.Subject}");
 					}
 
@@ -80,5 +78,5 @@
 					foreach (X509ChainStatus chainStatus in chainEl.ChainElementStatus) {
 						if (verboseSsl) {
-							Log.Out ("   Status: " + chainStatus.Status);
+							Log.Out ($"   Status: {chainStatus.Status}");
 						}
 
@@ -94,6 +92,5 @@
 
 						// This status is an error, print information
-						Log.Warning ("[OpenID] Steam certificate error: " + chainEl.Certificate.Subject + " ### Error: " +
-						             chainStatus.Status);
+						Log.Warning ($"[OpenID] Steam certificate error: {chainEl.Certificate.Subject} ### Error: {chainStatus.Status}");
 						privateChain.Reset ();
 						return false;
@@ -104,5 +101,5 @@
 					if (chainStatus.Status != X509ChainStatusFlags.NoError &&
 					    chainStatus.Status != X509ChainStatusFlags.UntrustedRoot) {
-						Log.Warning ("[OpenID] Steam certificate error: " + chainStatus.Status);
+						Log.Warning ($"[OpenID] Steam certificate error: {chainStatus.Status}");
 						privateChain.Reset ();
 						return false;
@@ -130,5 +127,5 @@
 			};
 
-			return STEAM_LOGIN + '?' + buildUrlParams (queryParams);
+			return $"{STEAM_LOGIN}?{buildUrlParams (queryParams)}";
 		}
 
@@ -141,5 +138,5 @@
 
 			if (mode == "error") {
-				Log.Warning ("[OpenID] Steam OpenID login error: " + getValue (_req, "openid.error"));
+				Log.Warning ($"[OpenID] Steam OpenID login error: {getValue (_req, "openid.error")}");
 				if (debugOpenId) {
 					PrintOpenIdResponse (_req);
@@ -174,5 +171,6 @@
 			string[] signeds = getValue (_req, "openid.signed").Split (',');
 			foreach (string s in signeds) {
-				queryParams ["openid." + s] = getValue (_req, "openid." + s);
+				string name = $"openid.{s}";
+				queryParams [name] = getValue (_req, name);
 			}
 
@@ -201,5 +199,5 @@
 			}
 
-			Log.Warning ("[OpenID] Steam OpenID login failed: {0}", responseString);
+			Log.Warning ($"[OpenID] Steam OpenID login failed: {responseString}");
 			return 0;
 		}
@@ -209,5 +207,5 @@
 			int i = 0;
 			foreach ((string argName, string argValue) in _queryParams) {
-				paramsArr [i++] = argName + "=" + Uri.EscapeDataString (argValue);
+				paramsArr [i++] = $"{argName}={Uri.EscapeDataString (argValue)}";
 			}
 
@@ -218,5 +216,5 @@
 			NameValueCollection nvc = _req.QueryString;
 			if (nvc [_name] == null) {
-				throw new MissingMemberException ("[OpenID] OpenID parameter \"" + _name + "\" missing");
+				throw new MissingMemberException ($"[OpenID] OpenID parameter \"{_name}\" missing");
 			}
 
@@ -227,5 +225,5 @@
 			NameValueCollection nvc = _req.QueryString;
 			for (int i = 0; i < nvc.Count; i++) {
-				Log.Out ("   " + nvc.GetKey (i) + " = " + nvc [i]);
+				Log.Out ($"   {nvc.GetKey (i)} = {nvc [i]}");
 			}
 		}
Index: binary-improvements2/WebServer/src/SSE/AbsEvent.cs
===================================================================
--- binary-improvements2/WebServer/src/SSE/AbsEvent.cs	(revision 401)
+++ binary-improvements2/WebServer/src/SSE/AbsEvent.cs	(revision 402)
@@ -4,5 +4,4 @@
 using System.Net.Sockets;
 using System.Text;
-using AllocsFixes.JSON;
 using Webserver.UrlHandlers;
 using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
@@ -20,6 +19,6 @@
 		private readonly List<HttpListenerResponse> openStreams = new List<HttpListenerResponse> ();
 
-		private readonly BlockingQueue<(string _eventName, object _data)> sendQueue =
-			new BlockingQueue<(string _eventName, object _data)> ();
+		private readonly BlockingQueue<(string _eventName, string _data)> sendQueue =
+			new BlockingQueue<(string _eventName, string _data)> ();
 
 		private int currentlyOpen;
@@ -42,32 +41,20 @@
 		}
 
-		protected void SendData (string _eventName, object _data) {
+		protected void SendData (string _eventName, string _data) {
 			sendQueue.Enqueue ((_eventName, _data));
 			Parent.SignalSendQueue ();
 		}
 
-
 		public void ProcessSendQueue () {
 			while (sendQueue.HasData ()) {
-				(string eventName, object data) = sendQueue.Dequeue ();
+				(string eventName, string data) = sendQueue.Dequeue ();
 				
 				stringBuilder.Append ("event: ");
 				stringBuilder.AppendLine (eventName);
 				stringBuilder.Append ("data: ");
-				
-				switch (data) {
-					case string dataString:
-						stringBuilder.AppendLine (dataString);
-						break;
-					case JsonNode dataJson:
-						dataJson.ToString (stringBuilder);
-						stringBuilder.AppendLine ("");
-						break;
-					default:
-						logError ("Data is neither string nor JSON.", false);
-						continue;
-				}
+				stringBuilder.AppendLine (data);
 				
 				stringBuilder.AppendLine ("");
+
 				string output = stringBuilder.ToString ();
 				stringBuilder.Clear ();
@@ -111,5 +98,5 @@
 						if (e.InnerException is SocketException se) {
 							if (se.SocketErrorCode != SocketError.ConnectionAborted && se.SocketErrorCode != SocketError.Shutdown) {
-								logError ($"SocketError ({se.SocketErrorCode}) while trying to write", true);
+								logError ($"SocketError ({se.SocketErrorCode.ToStringCached ()}) while trying to write", true);
 							}
 						} else {
Index: binary-improvements2/WebServer/src/SSE/EventLog.cs
===================================================================
--- binary-improvements2/WebServer/src/SSE/EventLog.cs	(revision 401)
+++ binary-improvements2/WebServer/src/SSE/EventLog.cs	(revision 402)
@@ -1,6 +1,6 @@
 using System;
-using AllocsFixes.JSON;
 using JetBrains.Annotations;
 using UnityEngine;
+using Utf8Json;
 using Webserver.UrlHandlers;
 
@@ -12,16 +12,34 @@
 		}
 
+		private static readonly byte[] jsonMsgKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("msg");
+		private static readonly byte[] jsonTypeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("type");
+		private static readonly byte[] jsonTraceKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("trace");
+		private static readonly byte[] jsonIsotimeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("isotime");
+		private static readonly byte[] jsonUptimeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("uptime");
+
 		private void LogCallback (string _formattedMsg, string _plainMsg, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
 			string isotime = _timestamp.ToString ("o");
 			string uptime = _uptime.ToString ();
 
-			JsonObject data = new JsonObject ();
-			data.Add ("msg", new JsonString (_plainMsg));
-			data.Add ("type", new JsonString (_type.ToStringCached ()));
-			data.Add ("trace", new JsonString (_trace));
-			data.Add ("isotime", new JsonString (isotime));
-			data.Add ("uptime", new JsonString (uptime));
-
-			SendData ("logLine", data);
+			JsonWriter writer = new JsonWriter ();
+			
+			writer.WriteRaw (jsonMsgKey);
+			writer.WriteString (_plainMsg);
+			
+			writer.WriteRaw (jsonTypeKey);
+			writer.WriteString (_type.ToStringCached ());
+			
+			writer.WriteRaw (jsonTraceKey);
+			writer.WriteString (_trace);
+			
+			writer.WriteRaw (jsonIsotimeKey);
+			writer.WriteString (isotime);
+			
+			writer.WriteRaw (jsonUptimeKey);
+			writer.WriteString (uptime);
+			
+			writer.WriteEndObject ();
+			
+			SendData ("logLine", writer.ToString ());
 		}
 	}
Index: binary-improvements2/WebServer/src/UrlHandlers/ApiHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/ApiHandler.cs	(revision 401)
+++ binary-improvements2/WebServer/src/UrlHandlers/ApiHandler.cs	(revision 402)
@@ -45,5 +45,5 @@
 		private void addApi (AbsWebAPI _api) {
 			apis.Add (_api.Name, _api);
-			WebPermissions.Instance.AddKnownModule ("webapi." + _api.Name, _api.DefaultPermissionLevel ());
+			WebPermissions.Instance.AddKnownModule ($"webapi.{_api.Name}", _api.DefaultPermissionLevel ());
 		}
 
@@ -92,5 +92,5 @@
 
 		private bool IsAuthorizedForApi (string _apiName, int _permissionLevel) {
-			return WebPermissions.Instance.ModuleAllowedWithLevel ("webapi." + _apiName, _permissionLevel);
+			return WebPermissions.Instance.ModuleAllowedWithLevel ($"webapi.{_apiName}", _permissionLevel);
 		}
 	}
Index: binary-improvements2/WebServer/src/UrlHandlers/ItemIconHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/ItemIconHandler.cs	(revision 401)
+++ binary-improvements2/WebServer/src/UrlHandlers/ItemIconHandler.cs	(revision 402)
@@ -3,5 +3,4 @@
 using System.IO;
 using System.Net;
-using AllocsFixes;
 using UnityEngine;
 using Object = UnityEngine.Object;
@@ -45,7 +44,16 @@
 				_context.Response.StatusCode = (int) HttpStatusCode.NotFound;
 				if (logMissingFiles) {
-					Log.Out ("[Web] IconHandler: FileNotFound: \"" + _context.RequestPath + "\" ");
+					Log.Out ($"[Web] IconHandler: FileNotFound: \"{_context.RequestPath}\" ");
 				}
 			}
+		}
+
+		private class LoadingStats {
+			public int Files;
+			public int Tints;
+			public readonly MicroStopwatch MswTotal = new MicroStopwatch (false);
+			public readonly MicroStopwatch MswLoading = new MicroStopwatch (false);
+			public readonly MicroStopwatch MswEncoding = new MicroStopwatch (false);
+			public readonly MicroStopwatch MswTinting = new MicroStopwatch (false);
 		}
 
@@ -57,5 +65,6 @@
 				}
 
-				MicroStopwatch microStopwatch = new MicroStopwatch ();
+				LoadingStats stats = new LoadingStats ();
+				stats?.MswTotal.Start ();
 
 				// Get list of used tints for all items
@@ -72,14 +81,14 @@
 
 					string name = ic.GetIconName ();
-					if (!tintedIcons.ContainsKey (name)) {
-						tintedIcons.Add (name, new List<Color> ());
+					if (!tintedIcons.TryGetValue (name, out List<Color> tintsList)) {
+						tintsList = new List<Color> ();
+						tintedIcons.Add (name, tintsList);
 					}
 
-					List<Color> list = tintedIcons [name];
-					list.Add (tintColor);
+					tintsList.Add (tintColor);
 				}
 
 				try {
-					loadIconsFromFolder (GameIO.GetGameDir ("Data/ItemIcons"), tintedIcons);
+					loadIconsFromFolder (GameIO.GetGameDir ("Data/ItemIcons"), tintedIcons, stats);
 				} catch (Exception e) {
 					Log.Error ("[Web] Failed loading icons from base game");
@@ -90,14 +99,28 @@
 				foreach (Mod mod in ModManager.GetLoadedMods ()) {
 					try {
-						string modIconsPath = mod.Path + "/ItemIcons";
-						loadIconsFromFolder (modIconsPath, tintedIcons);
+						string modIconsPath = $"{mod.Path}/ItemIcons";
+						loadIconsFromFolder (modIconsPath, tintedIcons, stats);
 					} catch (Exception e) {
-						Log.Error ("[Web] Failed loading icons from mod " + mod.ModInfo.Name.Value);
+						Log.Error ($"[Web] Failed loading icons from mod {mod.Name}");
 						Log.Exception (e);
 					}
 				}
+				
+				loaded = true;
 
-				loaded = true;
-				Log.Out ("[Web] IconHandler: Icons loaded - {0} ms", microStopwatch.ElapsedMilliseconds);
+				if (stats == null) {
+					Log.Out ($"[Web] IconHandler: Loaded {icons.Count} icons");
+				} else {
+					stats?.MswTotal.Stop ();
+					Log.Out ($"[Web] IconHandler: Loaded {icons.Count} icons ({stats.Files} source images with {stats.Tints} tints applied)");
+					Log.Out ($"[Web] IconHandler: Total time {stats.MswTotal.ElapsedMilliseconds} ms, loading files {stats.MswLoading.ElapsedMilliseconds} ms, tinting files {stats.MswTinting.ElapsedMilliseconds} ms, encoding files {stats.MswEncoding.ElapsedMilliseconds} ms");
+
+					int totalSize = 0;
+					foreach ((string _, byte[] iconData) in icons) {
+						totalSize += iconData.Length;
+					}
+					
+					Log.Out ($"[Web] IconHandler: Cached {totalSize / 1024} KiB");
+				}
 
 				return true;
@@ -105,5 +128,5 @@
 		}
 
-		private void loadIconsFromFolder (string _path, Dictionary<string, List<Color>> _tintedIcons) {
+		private void loadIconsFromFolder (string _path, Dictionary<string, List<Color>> _tintedIcons, LoadingStats _stats) {
 			if (!Directory.Exists (_path)) {
 				return;
@@ -118,9 +141,14 @@
 					string name = Path.GetFileNameWithoutExtension (file);
 					Texture2D tex = new Texture2D (1, 1, TextureFormat.ARGB32, false);
-					if (!tex.LoadImage (File.ReadAllBytes (file))) {
+					
+					_stats?.MswLoading.Start ();
+					byte[] sourceBytes = File.ReadAllBytes (file);
+					if (!tex.LoadImage (sourceBytes)) {
+						_stats?.MswLoading.Stop ();
 						continue;
 					}
+					_stats?.MswLoading.Stop ();
 
-					AddIcon (name, tex, _tintedIcons);
+					AddIcon (name, sourceBytes, tex, _tintedIcons, _stats);
 
 					Object.Destroy (tex);
@@ -131,13 +159,19 @@
 		}
 
-		private void AddIcon (string _name, Texture2D _tex, Dictionary<string, List<Color>> _tintedIcons) {
-			icons [_name + "__FFFFFF"] = _tex.EncodeToPNG ();
+		private void AddIcon (string _name, byte[] _sourceBytes, Texture2D _tex, Dictionary<string, List<Color>> _tintedIcons, LoadingStats _stats) {
+			_stats?.MswEncoding.Start ();
+			icons [$"{_name}__FFFFFF"] = _sourceBytes;
+			_stats?.MswEncoding.Stop ();
 
-			if (!_tintedIcons.ContainsKey (_name)) {
+			if (_stats != null) {
+				_stats.Files++;
+			}
+
+			if (!_tintedIcons.TryGetValue (_name, out List<Color> tintsList)) {
 				return;
 			}
 
-			foreach (Color c in _tintedIcons [_name]) {
-				string tintedName = _name + "__" + AllocsUtils.ColorToHex (c);
+			foreach (Color c in tintsList) {
+				string tintedName = $"{_name}__{c.ToHexCode ()}";
 				if (icons.ContainsKey (tintedName)) {
 					continue;
@@ -146,15 +180,20 @@
 				Texture2D tintedTex = new Texture2D (_tex.width, _tex.height, TextureFormat.ARGB32, false);
 
-				for (int x = 0; x < _tex.width; x++) {
-					for (int y = 0; y < _tex.height; y++) {
-						tintedTex.SetPixel (x, y, _tex.GetPixel (x, y) * c);
-					}
-				}
+				_stats?.MswTinting.Start ();
+				TextureUtils.ApplyTint (_tex, tintedTex, c);
+				_stats?.MswTinting.Stop ();
 
+				_stats?.MswEncoding.Start ();
 				icons [tintedName] = tintedTex.EncodeToPNG ();
+				_stats?.MswEncoding.Stop ();
 
 				Object.Destroy (tintedTex);
+
+				if (_stats != null) {
+					_stats.Tints++;
+				}
 			}
 		}
+		
 	}
 }
Index: binary-improvements2/WebServer/src/UrlHandlers/RewriteHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/RewriteHandler.cs	(revision 401)
+++ binary-improvements2/WebServer/src/UrlHandlers/RewriteHandler.cs	(revision 402)
@@ -10,5 +10,5 @@
 
 		public override void HandleRequest (RequestContext _context) {
-			_context.RequestPath = fixedTarget ? target : target + _context.RequestPath.Remove (0, urlBasePath.Length);
+			_context.RequestPath = fixedTarget ? target : $"{target}{_context.RequestPath.Remove (0, urlBasePath.Length)}";
 			parent.ApplyPathHandler (_context);
 		}
Index: binary-improvements2/WebServer/src/UrlHandlers/SessionHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/SessionHandler.cs	(revision 401)
+++ binary-improvements2/WebServer/src/UrlHandlers/SessionHandler.cs	(revision 402)
@@ -1,4 +1,8 @@
 using System;
+using System.Collections.Generic;
+using System.IO;
 using System.Net;
+using Platform.Steam;
+using Utf8Json;
 
 namespace Webserver.UrlHandlers {
@@ -6,6 +10,8 @@
 		private const string pageBasePath = "/app";
 		private const string pageErrorPath = "/app/error/";
+		
 		private const string steamOpenIdVerifyUrl = "verifysteamopenid";
 		private const string steamLoginUrl = "loginsteam";
+		private const string userPassLoginUrl = "login";
 
 		private readonly ConnectionHandler connectionHandler;
@@ -37,11 +43,83 @@
 				return;
 			}
+			
+			if (subpath.StartsWith (userPassLoginUrl)) {
+				HandleUserPassLogin (_context);
+				return;
+			}
 
 			_context.Response.Redirect (pageErrorPath + "InvalidSessionsCommand");
 		}
 
+		private void HandleUserPassLogin (RequestContext _context) {
+			if (!_context.Request.HasEntityBody) {
+				_context.Response.Redirect (pageErrorPath + "NoLoginData");
+				return;
+			}
+
+			Stream requestInputStream = _context.Request.InputStream;
+
+			byte[] jsonInputData = new byte[_context.Request.ContentLength64];
+			requestInputStream.Read (jsonInputData, 0, (int)_context.Request.ContentLength64);
+
+			IDictionary<string, object> inputJson;
+			try {
+				inputJson = JsonSerializer.Deserialize<IDictionary<string, object>> (jsonInputData);
+			} catch (Exception e) {
+				Log.Error ("Error deserializing JSON from user/password login:");
+				Log.Exception (e);
+				_context.Response.Redirect (pageErrorPath + "InvalidLoginJson");
+				return;
+			}
+
+			if (!inputJson.TryGetValue ("username", out object fieldNode) || fieldNode is not string username) {
+				_context.Response.Redirect (pageErrorPath + "InvalidLoginJson");
+				return;
+			}
+
+			if (!inputJson.TryGetValue ("password", out fieldNode) || fieldNode is not string password) {
+				_context.Response.Redirect (pageErrorPath + "InvalidLoginJson");
+				return;
+			}
+
+			// TODO: Apply login
+
+			string remoteEndpointString = _context.Request.RemoteEndPoint!.ToString ();
+
+			if (username != "test" || password != "123") {
+				// TODO: failed login
+				Log.Out ($"[Web] User/pass login failed from {remoteEndpointString}");
+				_context.Response.Redirect (pageErrorPath + "UserPassInvalid");
+				return;
+			}
+			
+			try {
+				// TODO: Match username/password to UserIdentifierAbs / serveradmins.xml
+				
+				WebConnection con = connectionHandler.LogIn (new UserIdentifierSteam (76561198066968172ul), _context.Request.RemoteEndPoint.Address);
+				int level = GameManager.Instance.adminTools.GetUserPermissionLevel (con.UserId);
+				Log.Out ($"[Web] User/pass login from {remoteEndpointString} with ID {con.UserId}, permission level {level}");
+
+				Cookie cookie = new Cookie ("sid", con.SessionID, "/") {
+					Expired = false,
+					Expires = DateTime.MinValue,
+					HttpOnly = true,
+					Secure = false
+				};
+				_context.Response.AppendCookie (cookie);
+				_context.Response.Redirect (pageBasePath);
+
+				return;
+			} catch (Exception e) {
+				Log.Error ("[Web] Error during user/pass login:");
+				Log.Exception (e);
+			}
+
+			_context.Response.Redirect (pageErrorPath + "UserPassLoginFailed");
+		}
+
 		private void HandleSteamLogin (RequestContext _context) {
-			string host = (WebUtils.IsSslRedirected (_context.Request) ? "https://" : "http://") + _context.Request.UserHostName;
-			string url = OpenID.GetOpenIdLoginUrl (host, host + urlBasePath + steamOpenIdVerifyUrl);
+			string host = $"{(WebUtils.IsSslRedirected (_context.Request) ? "https://" : "http://")}{_context.Request.UserHostName}";
+			string url = OpenID.GetOpenIdLoginUrl (host, $"{host}{urlBasePath}{steamOpenIdVerifyUrl}");
 			_context.Response.Redirect (url);
 		}
@@ -68,8 +146,7 @@
 				ulong id = OpenID.Validate (_context.Request);
 				if (id > 0) {
-					WebConnection con = connectionHandler.LogIn (id, _context.Request.RemoteEndPoint.Address);
+					WebConnection con = connectionHandler.LogIn (new UserIdentifierSteam (id), _context.Request.RemoteEndPoint.Address);
 					int level = GameManager.Instance.adminTools.GetUserPermissionLevel (con.UserId);
-					Log.Out ("[Web] Steam OpenID login from {0} with ID {1}, permission level {2}",
-						remoteEndpointString, con.UserId, level);
+					Log.Out ($"[Web] Steam OpenID login from {remoteEndpointString} with ID {con.UserId}, permission level {level}");
 
 					Cookie cookie = new Cookie ("sid", con.SessionID, "/") {
Index: binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs	(revision 401)
+++ binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs	(revision 402)
@@ -39,5 +39,5 @@
 			base.SetBasePathAndParent (_parent, _relativePath);
 
-			queueThead = ThreadManager.StartThread ("SSE-Processing_" + urlBasePath, QueueProcessThread, ThreadPriority.BelowNormal,
+			queueThead = ThreadManager.StartThread ($"SSE-Processing_{urlBasePath}", QueueProcessThread, ThreadPriority.BelowNormal,
 				_useRealThread: true);
 		}
@@ -49,7 +49,8 @@
 		}
 
+		// ReSharper disable once MemberCanBePrivate.Global
 		public void AddEvent (string _eventName, AbsEvent _eventInstance) {
 			events.Add (_eventName, _eventInstance);
-			WebPermissions.Instance.AddKnownModule ("webevent." + _eventName, _eventInstance.DefaultPermissionLevel ());
+			WebPermissions.Instance.AddKnownModule ($"webevent.{_eventName}", _eventInstance.DefaultPermissionLevel ());
 		}
 
@@ -88,5 +89,5 @@
 
 		private bool IsAuthorizedForEvent (string _eventName, int _permissionLevel) {
-			return WebPermissions.Instance.ModuleAllowedWithLevel ("webevent." + _eventName, _permissionLevel);
+			return WebPermissions.Instance.ModuleAllowedWithLevel ($"webevent.{_eventName}", _permissionLevel);
 		}
 
Index: binary-improvements2/WebServer/src/UrlHandlers/StaticHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/StaticHandler.cs	(revision 401)
+++ binary-improvements2/WebServer/src/UrlHandlers/StaticHandler.cs	(revision 402)
@@ -1,5 +1,5 @@
 using System.IO;
 using System.Net;
-using AllocsFixes.FileCache;
+using Webserver.FileCache;
 
 namespace Webserver.UrlHandlers {
@@ -11,5 +11,5 @@
 		public StaticHandler (string _filePath, AbstractCache _cache, bool _logMissingFiles,
 			string _moduleName = null) : base (_moduleName) {
-			datapath = _filePath + (_filePath [_filePath.Length - 1] == '/' ? "" : "/");
+			datapath = $"{_filePath}{(_filePath [^1] == '/' ? "" : "/")}";
 			cache = _cache;
 			logMissingFiles = _logMissingFiles;
@@ -19,5 +19,5 @@
 			string fn = _context.RequestPath.Remove (0, urlBasePath.Length);
 
-			byte[] content = cache.GetFileContent (datapath + fn);
+			byte[] content = cache.GetFileContent ($"{datapath}{fn}");
 
 			if (content != null) {
@@ -28,5 +28,5 @@
 				_context.Response.StatusCode = (int) HttpStatusCode.NotFound;
 				if (logMissingFiles) {
-					Log.Warning ("[Web] Static: FileNotFound: \"" + _context.RequestPath + "\" @ \"" + datapath + fn + "\"");
+					Log.Warning ($"[Web] Static: FileNotFound: \"{_context.RequestPath}\" @ \"{datapath}{fn}\"");
 				}
 			}
Index: binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs	(revision 401)
+++ binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs	(revision 402)
@@ -1,3 +1,3 @@
-using AllocsFixes.JSON;
+using Utf8Json;
 
 namespace Webserver.UrlHandlers {
@@ -6,21 +6,45 @@
 		}
 
+		private static readonly byte[] jsonLoggedInKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("loggedIn");
+		private static readonly byte[] jsonUsernameKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("username");
+		private static readonly byte[] jsonPermissionsKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("permissions");
+
+		private static readonly byte[] jsonModuleKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("module");
+		private static readonly byte[] jsonAllowedKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("allowed");
+
 		public override void HandleRequest (RequestContext _context) {
-			JsonObject result = new JsonObject ();
+			WebUtils.PrepareEnvelopedResult (out JsonWriter writer);
+			
+			writer.WriteRaw (jsonLoggedInKey);
+			writer.WriteBoolean (_context.Connection != null);
+			
+			writer.WriteRaw (jsonUsernameKey);
+			writer.WriteString (_context.Connection != null ? _context.Connection.UserId.ToString () : string.Empty);
+			
+			writer.WriteRaw (jsonPermissionsKey);
+			writer.WriteBeginArray ();
 
-			result.Add ("loggedin", new JsonBoolean (_context.Connection != null));
-			result.Add ("username", new JsonString (_context.Connection != null ? _context.Connection.UserId.ToString () : string.Empty));
+			bool first = true;
+			foreach (WebPermissions.WebModulePermission perm in WebPermissions.Instance.GetModules ()) {
+				if (!first) {
+					writer.WriteValueSeparator ();
+				}
 
-			JsonArray perms = new JsonArray ();
-			foreach (WebPermissions.WebModulePermission perm in WebPermissions.Instance.GetModules ()) {
-				JsonObject permObj = new JsonObject ();
-				permObj.Add ("module", new JsonString (perm.module));
-				permObj.Add ("allowed", new JsonBoolean (perm.permissionLevel >= _context.PermissionLevel));
-				perms.Add (permObj);
+				first = false;
+				
+				writer.WriteRaw (jsonModuleKey);
+				writer.WriteString (perm.module);
+				
+				writer.WriteRaw (jsonAllowedKey);
+				writer.WriteBoolean (perm.permissionLevel >= _context.PermissionLevel);
+				
+				writer.WriteEndObject ();
 			}
 
-			result.Add ("permissions", perms);
-
-			WebUtils.WriteJson (_context.Response, result);
+			writer.WriteEndArray ();
+			
+			writer.WriteEndObject ();
+			
+			WebUtils.SendEnvelopedResult (_context, ref writer);
 		}
 	}
Index: binary-improvements2/WebServer/src/Web.cs
===================================================================
--- binary-improvements2/WebServer/src/Web.cs	(revision 401)
+++ binary-improvements2/WebServer/src/Web.cs	(revision 402)
@@ -3,8 +3,7 @@
 using System.IO;
 using System.Net.Sockets;
-using AllocsFixes.FileCache;
-using MapRendering;
 using SpaceWizards.HttpListener;
 using UnityEngine;
+using Webserver.FileCache;
 using Webserver.UrlHandlers;
 using Cookie = System.Net.Cookie;
@@ -14,4 +13,6 @@
 namespace Webserver {
 	public class Web : IConsoleServer {
+		public static event Action<Web> ServerInitialized;
+		
 		private const int guestPermissionLevel = 2000;
 		private const string indexPageUrl = "/app";
@@ -26,5 +27,5 @@
 		public Web (string _modInstancePath) {
 			try {
-				int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
+				int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> (nameof (EnumGamePrefs.ControlPanelPort)));
 				if (webPort < 1 || webPort > 65533) {
 					Log.Out ("[Web] Webserver not started (ControlPanelPort not within 1-65533)");
@@ -44,5 +45,5 @@
 
 				string webfilesFolder = DetectWebserverFolder (_modInstancePath);
-				string webfilesFolderLegacy = _modInstancePath + "/weblegacy";
+				string webfilesFolderLegacy = $"{_modInstancePath}/weblegacy";
 
 				connectionHandler = new ConnectionHandler ();
@@ -56,5 +57,5 @@
 				RegisterPathHandler ("/weblegacy", new StaticHandler (
 					webfilesFolderLegacy,
-					useStaticCache ? (AbstractCache)new SimpleCache () : new DirectAccess (),
+					useStaticCache ? new SimpleCache () : new DirectAccess (),
 					false)
 				);
@@ -68,15 +69,12 @@
 				RegisterPathHandler ("/files/", new StaticHandler (
 					webfilesFolder,
-					useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
+					useStaticCache ? new SimpleCache () : new DirectAccess (),
 					false)
 				);
 				RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
-				RegisterPathHandler ("/map/", new StaticHandler (
-					GameIO.GetSaveGameDir () + "/map",
-					MapRenderer.GetTileCache (),
-					false,
-					"web.map")
-				);
 				RegisterPathHandler ("/api/", new ApiHandler ());
+				
+				// Allow other code to add their stuff
+				ServerInitialized?.Invoke (this);
 
 				listener.Prefixes.Add ($"http://+:{webPort}/");
@@ -87,5 +85,5 @@
 				SdtdConsole.Instance.RegisterServer (this);
 
-				Log.Out ("[Web] Started Webserver on port " + webPort);
+				Log.Out ($"[Web] Started Webserver on port {webPort}");
 			} catch (Exception e) {
 				Log.Error ("[Web] Error in Web.ctor: ");
@@ -95,8 +93,8 @@
 
 		private static string DetectWebserverFolder (string _modInstancePath) {
-			string webserverFolder = _modInstancePath + "/webserver";
+			string webserverFolder = $"{_modInstancePath}/webserver";
 
 			foreach (Mod mod in ModManager.GetLoadedMods ()) {
-				string modServerFolder = mod.Path + "/webserver";
+				string modServerFolder = $"{mod.Path}/webserver";
 				
 				if (Directory.Exists (modServerFolder)) {
@@ -127,5 +125,5 @@
 			foreach (Mod mod in ModManager.GetLoadedMods ()) {
 				try {
-					string webModPath = mod.Path + "/WebMod";
+					string webModPath = $"{mod.Path}/WebMod";
 					if (!Directory.Exists (webModPath)) {
 						continue;
@@ -136,8 +134,8 @@
 						webMods.Add (webMod);
 					} catch (InvalidDataException e) {
-						Log.Error ($"[Web] Could not load webmod from mod {mod.ModInfo.Name.Value}: {e.Message}");
+						Log.Error ($"[Web] Could not load webmod from mod {mod.Name}: {e.Message}");
 					}
 				} catch (Exception e) {
-					Log.Error ("[Web] Failed loading web mods from mod " + mod.ModInfo.Name.Value);
+					Log.Error ($"[Web] Failed loading web mods from mod {mod.Name}");
 					Log.Exception (e);
 				}
@@ -150,5 +148,5 @@
 				listener.Close ();
 			} catch (Exception e) {
-				Log.Out ("[Web] Error in Web.Disconnect: " + e);
+				Log.Out ($"[Web] Error in Web.Disconnect: {e}");
 			}
 		}
@@ -238,7 +236,7 @@
 			} catch (IOException e) {
 				if (e.InnerException is SocketException) {
-					Log.Out ("[Web] Error in Web.HandleRequest(): Remote host closed connection: " + e.InnerException.Message);
+					Log.Out ($"[Web] Error in Web.HandleRequest(): Remote host closed connection: {e.InnerException.Message}");
 				} else {
-					Log.Out ("[Web] Error (IO) in Web.HandleRequest(): " + e);
+					Log.Out ($"[Web] Error (IO) in Web.HandleRequest(): {e}");
 				}
 			} catch (Exception e) {
@@ -313,5 +311,5 @@
 			}
 
-			Log.Warning ("[Web] Invalid Admintoken used from " + reqRemoteEndPoint);
+			Log.Warning ($"[Web] Invalid Admintoken used from {reqRemoteEndPoint}");
 
 			return guestPermissionLevel;
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Animal.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Animal.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Animal.cs	(revision 402)
@@ -0,0 +1,47 @@
+﻿using System.Collections.Generic;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.LiveData;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	internal class Animal : AbsRestApi {
+		private readonly List<EntityAnimal> entities = new List<EntityAnimal> ();
+
+		private static readonly byte[] jsonKeyId = JsonWriter.GetEncodedPropertyNameWithBeginObject ("id");
+		private static readonly byte[] jsonKeyName = JsonWriter.GetEncodedPropertyNameWithBeginObject ("name");
+		private static readonly byte[] jsonKeyPosition = JsonWriter.GetEncodedPropertyNameWithBeginObject ("position");
+
+		protected override void HandleRestGet (RequestContext _context) {
+			PrepareEnvelopedResult (out JsonWriter writer);
+			writer.WriteBeginArray ();
+			
+			lock (entities) {
+				Animals.Instance.Get (entities);
+				
+				for (int i = 0; i < entities.Count; i++) {
+					if (i > 0) {
+						writer.WriteValueSeparator ();
+					}
+					
+					EntityAlive entity = entities [i];
+					Vector3i position = new Vector3i (entity.GetPosition ());
+					
+					writer.WriteRaw (jsonKeyId);
+					writer.WriteInt32 (entity.entityId);
+					
+					writer.WriteRaw (jsonKeyName);
+					writer.WriteString (!string.IsNullOrEmpty (entity.EntityName) ? entity.EntityName : $"animal class #{entity.entityClass}");
+					
+					writer.WriteRaw (jsonKeyPosition);
+					JsonCommons.WritePositionObject (writer, position);
+
+					writer.WriteEndObject ();
+				}
+			}
+			
+			writer.WriteEndArray ();
+			SendEnvelopedResult (_context, ref writer);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Command.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Command.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Command.cs	(revision 402)
@@ -0,0 +1,135 @@
+using System.Collections.Generic;
+using System.Net;
+using JetBrains.Annotations;
+using Utf8Json;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class Command : AbsRestApi {
+		private static readonly byte[] jsonCommandsKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("commands");
+
+		private static readonly byte[] jsonOverloadsKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("overloads");
+		private static readonly byte[] jsonCommandKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("command");
+		private static readonly byte[] jsonDescriptionKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("description");
+		private static readonly byte[] jsonHelpKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("help");
+		private static readonly byte[] jsonAllowedKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("allowed");
+		
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+			int permissionLevel = _context.PermissionLevel;
+			
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			writer.WriteRaw (jsonCommandsKey);
+			writer.WriteBeginArray ();
+
+			if (string.IsNullOrEmpty (id)) {
+				bool first = true;
+				foreach (IConsoleCommand cc in SdtdConsole.Instance.GetCommands ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeCommandJson (ref writer, cc, permissionLevel);
+				}
+			} else if (SdtdConsole.Instance.GetCommand (id) is { } command) {
+				writeCommandJson (ref writer, command, permissionLevel);
+			} else {
+				writer.WriteEndArray ();
+				writer.WriteEndObject ();
+				SendEnvelopedResult (_context, ref writer, HttpStatusCode.NotFound);
+				return;
+			}
+
+			writer.WriteEndArray ();
+			writer.WriteEndObject ();
+
+			SendEnvelopedResult (_context, ref writer);
+		}
+
+		private void writeCommandJson (ref JsonWriter _writer, IConsoleCommand _command, int _userPermissionLevel) {
+			_writer.WriteRaw (jsonOverloadsKey);
+			_writer.WriteBeginArray ();
+
+			string cmd = string.Empty;
+				
+			bool firstOverload = true;
+			foreach (string s in _command.GetCommands ()) {
+				if (!firstOverload) {
+					_writer.WriteValueSeparator ();
+				}
+				firstOverload = false;
+					
+				_writer.WriteString (s);
+					
+				if (s.Length > cmd.Length) {
+					cmd = s;
+				}
+			}
+				
+			_writer.WriteEndArray ();
+
+			_writer.WriteRaw (jsonCommandKey);
+			_writer.WriteString (cmd);
+				
+			_writer.WriteRaw (jsonDescriptionKey);
+			_writer.WriteString (_command.GetDescription ());
+				
+			_writer.WriteRaw (jsonHelpKey);
+			_writer.WriteString (_command.GetHelp ());
+				
+			int commandPermissionLevel = GameManager.Instance.adminTools.GetCommandPermissionLevel (_command.GetCommands ());
+			_writer.WriteRaw (jsonAllowedKey);
+			_writer.WriteBoolean (_userPermissionLevel <= commandPermissionLevel);
+
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			if (!TryGetJsonField (_jsonInput, "command", out string commandString)) {
+				SendErrorResult (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_COMMAND");
+				return;
+			}
+
+			WebCommandResult.ResultType responseType = WebCommandResult.ResultType.Full;
+
+			if (TryGetJsonField (_jsonInput, "format", out string formatString)) {
+				if (formatString.EqualsCaseInsensitive ("raw")) {
+					responseType = WebCommandResult.ResultType.Raw;
+				} else if (formatString.EqualsCaseInsensitive ("simple")) {
+					responseType = WebCommandResult.ResultType.ResultOnly;
+				}
+			}
+
+			int commandSepIndex = commandString.IndexOf (' ');
+			string commandPart = commandSepIndex > 0 ? commandString.Substring (0, commandSepIndex) : commandString;
+			string argumentsPart = commandSepIndex > 0
+				? commandString.Substring (commandPart.Length + 1)
+				: "";
+
+			IConsoleCommand command = SdtdConsole.Instance.GetCommand (commandPart, true);
+
+			if (command == null) {
+				SendErrorResult (_context, HttpStatusCode.NotFound, _jsonInputData, "UNKNOWN_COMMAND");
+				return;
+			}
+
+			int commandPermissionLevel = GameManager.Instance.adminTools.GetCommandPermissionLevel (command.GetCommands ());
+
+			if (_context.PermissionLevel > commandPermissionLevel) {
+				SendErrorResult (_context, HttpStatusCode.Forbidden, _jsonInputData, "NO_PERMISSION");
+				return;
+			}
+
+			_context.Response.SendChunked = true;
+			WebCommandResult wcr = new WebCommandResult (commandPart, argumentsPart, responseType, _context);
+			SdtdConsole.Instance.ExecuteAsync (commandString, wcr);
+		}
+
+		public override int DefaultPermissionLevel () {
+			return 2000;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/GetLandClaims.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/GetLandClaims.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/GetLandClaims.cs	(revision 402)
@@ -0,0 +1,78 @@
+// using System.Collections.Generic;
+// using System.Net;
+// using AllocsFixes;
+// using AllocsFixes.PersistentData;
+// using JetBrains.Annotations;
+//
+// namespace Webserver.WebAPI.APIs {
+// 	[UsedImplicitly]
+// 	public class GetLandClaims : AbsWebAPI {
+// 		public override void HandleRequest (RequestContext _context) {
+// 			PlatformUserIdentifierAbs requestedUserId = null;
+// 			if (_context.Request.QueryString ["userid"] != null) {
+// 				if (!PlatformUserIdentifierAbs.TryFromCombinedString (_context.Request.QueryString ["userid"], out requestedUserId)) {
+// 					WebUtils.WriteText (_context.Response, "Invalid user id given", HttpStatusCode.BadRequest);
+// 					return;
+// 				}
+// 			}
+//
+// 			// default user, cheap way to avoid 'null reference exception'
+// 			PlatformUserIdentifierAbs userId = _context.Connection?.UserId;
+//
+// 			bool bViewAll = WebConnection.CanViewAllClaims (_context.PermissionLevel);
+//
+// 			JsonObject result = new JsonObject ();
+// 			result.Add ("claimsize",
+// 				new JsonNumber (GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> (nameof (EnumGamePrefs.LandClaimSize)))));
+//
+// 			JsonArray claimOwners = new JsonArray ();
+// 			result.Add ("claimowners", claimOwners);
+//
+// 			LandClaimList.OwnerFilter[] ownerFilters = null;
+// 			if (requestedUserId != null || !bViewAll) {
+// 				if (requestedUserId != null && !bViewAll) {
+// 					ownerFilters = new[] {
+// 						LandClaimList.UserIdFilter (userId),
+// 						LandClaimList.UserIdFilter (requestedUserId)
+// 					};
+// 				} else if (!bViewAll) {
+// 					ownerFilters = new[] {LandClaimList.UserIdFilter (userId)};
+// 				} else {
+// 					ownerFilters = new[] {LandClaimList.UserIdFilter (requestedUserId)};
+// 				}
+// 			}
+//
+// 			LandClaimList.PositionFilter[] posFilters = null;
+//
+// 			Dictionary<Player, List<Vector3i>> claims = LandClaimList.GetLandClaims (ownerFilters, posFilters);
+//
+// 			foreach ((Player player, List<Vector3i> claimPositions) in claims) {
+// 				JsonObject owner = new JsonObject ();
+// 				claimOwners.Add (owner);
+//
+// 				owner.Add ("steamid", new JsonString (player.PlatformId.CombinedString));
+// 				owner.Add ("claimactive", new JsonBoolean (player.LandProtectionActive));
+//
+// 				if (player.Name.Length > 0) {
+// 					owner.Add ("playername", new JsonString (player.Name));
+// 				} else {
+// 					owner.Add ("playername", new JsonNull ());
+// 				}
+//
+// 				JsonArray claimsJson = new JsonArray ();
+// 				owner.Add ("claims", claimsJson);
+//
+// 				foreach (Vector3i v in claimPositions) {
+// 					JsonObject claim = new JsonObject ();
+// 					claim.Add ("x", new JsonNumber (v.x));
+// 					claim.Add ("y", new JsonNumber (v.y));
+// 					claim.Add ("z", new JsonNumber (v.z));
+//
+// 					claimsJson.Add (claim);
+// 				}
+// 			}
+//
+// 			WebUtils.WriteJson (_context.Response, result);
+// 		}
+// 	}
+// }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerInventories.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerInventories.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerInventories.cs	(revision 402)
@@ -0,0 +1,25 @@
+// using AllocsFixes.PersistentData;
+// using JetBrains.Annotations;
+//
+// namespace Webserver.WebAPI.APIs {
+// 	[UsedImplicitly]
+// 	public class GetPlayerInventories : AbsWebAPI {
+// 		public override void HandleRequest (RequestContext _context) {
+// 			GetPlayerInventory.GetInventoryArguments (_context.Request, out bool showIconColor, out bool showIconName);
+//
+// 			JsonArray allInventoriesResult = new JsonArray ();
+//
+// 			foreach ((PlatformUserIdentifierAbs userId, Player player) in PersistentContainer.Instance.Players.Dict) {
+// 				if (player == null) {
+// 					continue;
+// 				}
+//
+// 				if (player.IsOnline) {
+// 					allInventoriesResult.Add (GetPlayerInventory.DoPlayer (userId.CombinedString, player, showIconColor, showIconName));
+// 				}
+// 			}
+//
+// 			WebUtils.WriteJson (_context.Response, allInventoriesResult);
+// 		}
+// 	}
+// }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerInventory.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerInventory.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerInventory.cs	(revision 402)
@@ -0,0 +1,128 @@
+// using System.Collections.Generic;
+// using System.Net;
+// using AllocsFixes.PersistentData;
+// using JetBrains.Annotations;
+// using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+//
+// namespace Webserver.WebAPI.APIs {
+// 	[UsedImplicitly]
+// 	public class GetPlayerInventory : AbsWebAPI {
+// 		public override void HandleRequest (RequestContext _context) {
+// 			if (_context.Request.QueryString ["userid"] == null) {
+// 				WebUtils.WriteText (_context.Response, "No user id given", HttpStatusCode.BadRequest);
+// 				return;
+// 			}
+//
+// 			string userIdString = _context.Request.QueryString ["userid"];
+// 			if (!PlatformUserIdentifierAbs.TryFromCombinedString (userIdString, out PlatformUserIdentifierAbs userId)) {
+// 				WebUtils.WriteText (_context.Response, "Invalid user id given", HttpStatusCode.BadRequest);
+// 				return;
+// 			}
+//
+// 			Player p = PersistentContainer.Instance.Players [userId, false];
+// 			if (p == null) {
+// 				WebUtils.WriteText (_context.Response, "Unknown user id given", HttpStatusCode.NotFound);
+// 				return;
+// 			}
+//
+// 			GetInventoryArguments (_context.Request, out bool showIconColor, out bool showIconName);
+//
+// 			JsonObject result = DoPlayer (userIdString, p, showIconColor, showIconName);
+//
+// 			WebUtils.WriteJson (_context.Response, result);
+// 		}
+//
+// 		internal static void GetInventoryArguments (HttpListenerRequest _req, out bool _showIconColor, out bool _showIconName) {
+// 			if (_req.QueryString ["showiconcolor"] == null || !bool.TryParse (_req.QueryString ["showiconcolor"], out _showIconColor)) {
+// 				_showIconColor = true;
+// 			}
+// 			
+// 			if (_req.QueryString ["showiconname"] == null || !bool.TryParse (_req.QueryString ["showiconname"], out _showIconName)) {
+// 				_showIconName = true;
+// 			}
+// 		}
+//
+// 		internal static JsonObject DoPlayer (string _steamId, Player _player, bool _showIconColor, bool _showIconName) {
+// 			AllocsFixes.PersistentData.Inventory inv = _player.Inventory;
+//
+// 			JsonObject result = new JsonObject ();
+//
+// 			JsonArray bag = new JsonArray ();
+// 			JsonArray belt = new JsonArray ();
+// 			JsonObject equipment = new JsonObject ();
+// 			result.Add ("userid", new JsonString (_steamId));
+// 			result.Add ("entityid", new JsonNumber (_player.EntityID));
+// 			result.Add ("playername", new JsonString (_player.Name));
+// 			result.Add ("bag", bag);
+// 			result.Add ("belt", belt);
+// 			result.Add ("equipment", equipment);
+//
+// 			DoInventory (belt, inv.belt, _showIconColor, _showIconName);
+// 			DoInventory (bag, inv.bag, _showIconColor, _showIconName);
+//
+// 			AddEquipment (equipment, "head", inv.equipment, EquipmentSlots.Headgear, _showIconColor, _showIconName);
+// 			AddEquipment (equipment, "eyes", inv.equipment, EquipmentSlots.Eyewear, _showIconColor, _showIconName);
+// 			AddEquipment (equipment, "face", inv.equipment, EquipmentSlots.Face, _showIconColor, _showIconName);
+//
+// 			AddEquipment (equipment, "armor", inv.equipment, EquipmentSlots.ChestArmor, _showIconColor, _showIconName);
+// 			AddEquipment (equipment, "jacket", inv.equipment, EquipmentSlots.Jacket, _showIconColor, _showIconName);
+// 			AddEquipment (equipment, "shirt", inv.equipment, EquipmentSlots.Shirt, _showIconColor, _showIconName);
+//
+// 			AddEquipment (equipment, "legarmor", inv.equipment, EquipmentSlots.LegArmor, _showIconColor, _showIconName);
+// 			AddEquipment (equipment, "pants", inv.equipment, EquipmentSlots.Legs, _showIconColor, _showIconName);
+// 			AddEquipment (equipment, "boots", inv.equipment, EquipmentSlots.Feet, _showIconColor, _showIconName);
+//
+// 			AddEquipment (equipment, "gloves", inv.equipment, EquipmentSlots.Hands, _showIconColor, _showIconName);
+//
+// 			return result;
+// 		}
+//
+// 		private static void DoInventory (JsonArray _jsonRes, List<InvItem> _inv, bool _showIconColor, bool _showIconName) {
+// 			for (int i = 0; i < _inv.Count; i++) {
+// 				_jsonRes.Add (GetJsonForItem (_inv [i], _showIconColor, _showIconName));
+// 			}
+// 		}
+//
+// 		private static void AddEquipment (JsonObject _eq, string _slotname, InvItem[] _items, EquipmentSlots _slot, bool _showIconColor, bool _showIconName) {
+// 			int[] slotindices = XUiM_PlayerEquipment.GetSlotIndicesByEquipmentSlot (_slot);
+//
+// 			for (int i = 0; i < slotindices.Length; i++) {
+// 				if (_items? [slotindices [i]] == null) {
+// 					continue;
+// 				}
+//
+// 				InvItem item = _items [slotindices [i]];
+// 				_eq.Add (_slotname, GetJsonForItem (item, _showIconColor, _showIconName));
+// 				return;
+// 			}
+//
+// 			_eq.Add (_slotname, new JsonNull ());
+// 		}
+//
+// 		private static JsonNode GetJsonForItem (InvItem _item, bool _showIconColor, bool _showIconName) {
+// 			if (_item == null) {
+// 				return new JsonNull ();
+// 			}
+//
+// 			JsonObject jsonItem = new JsonObject ();
+// 			jsonItem.Add ("count", new JsonNumber (_item.count));
+// 			jsonItem.Add ("name", new JsonString (_item.itemName));
+// 			
+// 			if (_showIconName) {
+// 				jsonItem.Add ("icon", new JsonString (_item.icon));
+// 			}
+//
+// 			if (_showIconColor) {
+// 				jsonItem.Add ("iconcolor", new JsonString (_item.iconcolor));
+// 			}
+//
+// 			jsonItem.Add ("quality", new JsonNumber (_item.quality));
+// 			if (_item.quality >= 0) {
+// 				jsonItem.Add ("qualitycolor", new JsonString (QualityInfo.GetQualityColorHex (_item.quality)));
+// 			}
+//
+// 			return jsonItem;
+//
+// 		}
+// 	}
+// }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerList.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerList.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayerList.cs	(revision 402)
@@ -0,0 +1,263 @@
+// using System;
+// using System.Collections.Generic;
+// using System.Linq;
+// using System.Text.RegularExpressions;
+// using AllocsFixes.PersistentData;
+// using JetBrains.Annotations;
+//
+// namespace Webserver.WebAPI.APIs {
+// 	[UsedImplicitly]
+// 	public class GetPlayerList : AbsWebAPI {
+// 		private static readonly Regex numberFilterMatcher =
+// 			new Regex (@"^(>=|=>|>|<=|=<|<|==|=)?\s*([0-9]+(\.[0-9]*)?)$");
+//
+// 		private static readonly UnityEngine.Profiling.CustomSampler jsonSerializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Build");
+//
+// 		public override void HandleRequest (RequestContext _context) {
+// 			AdminTools admTools = GameManager.Instance.adminTools;
+// 			PlatformUserIdentifierAbs userId = _context.Connection?.UserId;
+//
+// 			bool bViewAll = WebConnection.CanViewAllPlayers (_context.PermissionLevel);
+//
+// 			// TODO: Sort (and filter?) prior to converting to JSON ... hard as how to get the correct column's data? (i.e. column name matches JSON object field names, not source data)
+//
+// 			int rowsPerPage = 25;
+// 			if (_context.Request.QueryString ["rowsperpage"] != null) {
+// 				int.TryParse (_context.Request.QueryString ["rowsperpage"], out rowsPerPage);
+// 			}
+//
+// 			int page = 0;
+// 			if (_context.Request.QueryString ["page"] != null) {
+// 				int.TryParse (_context.Request.QueryString ["page"], out page);
+// 			}
+//
+// 			int firstEntry = page * rowsPerPage;
+//
+// 			Players playersList = PersistentContainer.Instance.Players;
+//
+// 			
+// 			List<JsonObject> playerList = new List<JsonObject> ();
+//
+// 			jsonSerializeSampler.Begin ();
+//
+// 			foreach (KeyValuePair<PlatformUserIdentifierAbs, Player> kvp in playersList.Dict) {
+// 				Player p = kvp.Value;
+//
+// 				if (bViewAll || p.PlatformId.Equals (userId)) {
+// 					JsonObject pos = new JsonObject ();
+// 					pos.Add ("x", new JsonNumber (p.LastPosition.x));
+// 					pos.Add ("y", new JsonNumber (p.LastPosition.y));
+// 					pos.Add ("z", new JsonNumber (p.LastPosition.z));
+//
+// 					JsonObject pJson = new JsonObject ();
+// 					pJson.Add ("steamid", new JsonString (kvp.Key.CombinedString));
+// 					pJson.Add ("entityid", new JsonNumber (p.EntityID));
+// 					pJson.Add ("ip", new JsonString (p.IP));
+// 					pJson.Add ("name", new JsonString (p.Name));
+// 					pJson.Add ("online", new JsonBoolean (p.IsOnline));
+// 					pJson.Add ("position", pos);
+//
+// 					pJson.Add ("totalplaytime", new JsonNumber (p.TotalPlayTime));
+// 					pJson.Add ("lastonline",
+// 						new JsonString (p.LastOnline.ToUniversalTime ().ToString ("yyyy-MM-ddTHH:mm:ssZ")));
+// 					pJson.Add ("ping", new JsonNumber (p.IsOnline ? p.ClientInfo.ping : -1));
+//
+// 					JsonBoolean banned = admTools != null ? new JsonBoolean (admTools.IsBanned (kvp.Key, out _, out _)) : new JsonBoolean (false);
+//
+// 					pJson.Add ("banned", banned);
+//
+// 					playerList.Add (pJson);
+// 				}
+// 			}
+//
+// 			jsonSerializeSampler.End ();
+//
+// 			IEnumerable<JsonObject> list = playerList;
+//
+// 			foreach (string key in _context.Request.QueryString.AllKeys) {
+// 				if (!string.IsNullOrEmpty (key) && key.StartsWith ("filter[")) {
+// 					string filterCol = key.Substring (key.IndexOf ('[') + 1);
+// 					filterCol = filterCol.Substring (0, filterCol.Length - 1);
+// 					string filterVal = _context.Request.QueryString.Get (key).Trim ();
+//
+// 					list = ExecuteFilter (list, filterCol, filterVal);
+// 				}
+// 			}
+//
+// 			int totalAfterFilter = list.Count ();
+//
+// 			foreach (string key in _context.Request.QueryString.AllKeys) {
+// 				if (!string.IsNullOrEmpty (key) && key.StartsWith ("sort[")) {
+// 					string sortCol = key.Substring (key.IndexOf ('[') + 1);
+// 					sortCol = sortCol.Substring (0, sortCol.Length - 1);
+// 					string sortVal = _context.Request.QueryString.Get (key);
+//
+// 					list = ExecuteSort (list, sortCol, sortVal == "0");
+// 				}
+// 			}
+//
+// 			list = list.Skip (firstEntry);
+// 			list = list.Take (rowsPerPage);
+//
+//
+// 			JsonArray playersJsResult = new JsonArray ();
+// 			foreach (JsonObject jsO in list) {
+// 				playersJsResult.Add (jsO);
+// 			}
+//
+// 			JsonObject result = new JsonObject ();
+// 			result.Add ("total", new JsonNumber (totalAfterFilter));
+// 			result.Add ("totalUnfiltered", new JsonNumber (playerList.Count));
+// 			result.Add ("firstResult", new JsonNumber (firstEntry));
+// 			result.Add ("players", playersJsResult);
+//
+// 			WebUtils.WriteJson (_context.Response, result);
+// 		}
+//
+// 		private IEnumerable<JsonObject> ExecuteFilter (IEnumerable<JsonObject> _list, string _filterCol,
+// 			string _filterVal) {
+// 			if (!_list.Any()) {
+// 				return _list;
+// 			}
+//
+// 			if (_list.First ().ContainsKey (_filterCol)) {
+// 				Type colType = _list.First () [_filterCol].GetType ();
+// 				if (colType == typeof (JsonNumber)) {
+// 					return ExecuteNumberFilter (_list, _filterCol, _filterVal);
+// 				}
+//
+// 				if (colType == typeof (JsonBoolean)) {
+// 					bool value = StringParsers.ParseBool (_filterVal);
+// 					return _list.Where (_line => ((JsonBoolean) _line [_filterCol]).GetBool () == value);
+// 				}
+//
+// 				if (colType == typeof (JsonString)) {
+// 					// regex-match whole ^string$, replace * by .*, ? by .?, + by .+
+// 					_filterVal = _filterVal.Replace ("*", ".*").Replace ("?", ".?").Replace ("+", ".+");
+// 					_filterVal = "^" + _filterVal + "$";
+//
+// 					//Log.Out ("GetPlayerList: Filter on String with Regex '" + _filterVal + "'");
+// 					Regex matcher = new Regex (_filterVal, RegexOptions.IgnoreCase);
+// 					return _list.Where (_line => matcher.IsMatch (((JsonString) _line [_filterCol]).GetString ()));
+// 				}
+// 			}
+//
+// 			return _list;
+// 		}
+//
+//
+// 		private IEnumerable<JsonObject> ExecuteNumberFilter (IEnumerable<JsonObject> _list, string _filterCol,
+// 			string _filterVal) {
+// 			// allow value (exact match), =, ==, >=, >, <=, <
+// 			Match filterMatch = numberFilterMatcher.Match (_filterVal);
+// 			if (filterMatch.Success) {
+// 				double value = StringParsers.ParseDouble (filterMatch.Groups [2].Value);
+// 				NumberMatchType matchType;
+// 				double epsilon = value / 100000;
+// 				switch (filterMatch.Groups [1].Value) {
+// 					case "":
+// 					case "=":
+// 					case "==":
+// 						matchType = NumberMatchType.Equal;
+// 						break;
+// 					case ">":
+// 						matchType = NumberMatchType.Greater;
+// 						break;
+// 					case ">=":
+// 					case "=>":
+// 						matchType = NumberMatchType.GreaterEqual;
+// 						break;
+// 					case "<":
+// 						matchType = NumberMatchType.Lesser;
+// 						break;
+// 					case "<=":
+// 					case "=<":
+// 						matchType = NumberMatchType.LesserEqual;
+// 						break;
+// 					default:
+// 						matchType = NumberMatchType.Equal;
+// 						break;
+// 				}
+//
+// 				return _list.Where (delegate (JsonObject _line) {
+// 					double objVal = ((JsonNumber) _line [_filterCol]).GetDouble ();
+// 					switch (matchType) {
+// 						case NumberMatchType.Greater:
+// 							return objVal > value;
+// 						case NumberMatchType.GreaterEqual:
+// 							return objVal >= value;
+// 						case NumberMatchType.Lesser:
+// 							return objVal < value;
+// 						case NumberMatchType.LesserEqual:
+// 							return objVal <= value;
+// 						case NumberMatchType.Equal:
+// 						default:
+// 							return NearlyEqual (objVal, value, epsilon);
+// 					}
+// 				});
+// 			}
+//
+// 			global::Log.Out ("[Web] GetPlayerList: ignoring invalid filter for number-column '{0}': '{1}'", _filterCol, _filterVal);
+// 			return _list;
+// 		}
+//
+//
+// 		private IEnumerable<JsonObject> ExecuteSort (IEnumerable<JsonObject> _list, string _sortCol, bool _ascending) {
+// 			if (_list.Count () == 0) {
+// 				return _list;
+// 			}
+//
+// 			if (_list.First ().ContainsKey (_sortCol)) {
+// 				Type colType = _list.First () [_sortCol].GetType ();
+// 				if (colType == typeof (JsonNumber)) {
+// 					if (_ascending) {
+// 						return _list.OrderBy (_line => ((JsonNumber) _line [_sortCol]).GetDouble ());
+// 					}
+//
+// 					return _list.OrderByDescending (_line => ((JsonNumber) _line [_sortCol]).GetDouble ());
+// 				}
+//
+// 				if (colType == typeof (JsonBoolean)) {
+// 					if (_ascending) {
+// 						return _list.OrderBy (_line => ((JsonBoolean) _line [_sortCol]).GetBool ());
+// 					}
+//
+// 					return _list.OrderByDescending (_line => ((JsonBoolean) _line [_sortCol]).GetBool ());
+// 				}
+//
+// 				if (_ascending) {
+// 					return _list.OrderBy (_line => _line [_sortCol].ToString ());
+// 				}
+//
+// 				return _list.OrderByDescending (_line => _line [_sortCol].ToString ());
+// 			}
+//
+// 			return _list;
+// 		}
+//
+//
+// 		private bool NearlyEqual (double _a, double _b, double _epsilon) {
+// 			double absA = Math.Abs (_a);
+// 			double absB = Math.Abs (_b);
+// 			double diff = Math.Abs (_a - _b);
+//
+// 			if (_a == _b) {
+// 				return true;
+// 			}
+//
+// 			if (_a == 0 || _b == 0 || diff < double.Epsilon) {
+// 				return diff < _epsilon;
+// 			}
+//
+// 			return diff / (absA + absB) < _epsilon;
+// 		}
+//
+// 		private enum NumberMatchType {
+// 			Equal,
+// 			Greater,
+// 			GreaterEqual,
+// 			Lesser,
+// 			LesserEqual
+// 		}
+// 	}
+// }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayersLocation.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayersLocation.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayersLocation.cs	(revision 402)
@@ -0,0 +1,61 @@
+// using AllocsFixes.PersistentData;
+// using JetBrains.Annotations;
+//
+// namespace Webserver.WebAPI.APIs {
+// 	[UsedImplicitly]
+// 	public class GetPlayersLocation : AbsWebAPI {
+// 		public override void HandleRequest (RequestContext _context) {
+// 			AdminTools admTools = GameManager.Instance.adminTools;
+// 			PlatformUserIdentifierAbs reqUserId = _context.Connection?.UserId;
+//
+// 			bool listOffline = false;
+// 			if (_context.Request.QueryString ["offline"] != null) {
+// 				bool.TryParse (_context.Request.QueryString ["offline"], out listOffline);
+// 			}
+//
+// 			bool bViewAll = WebConnection.CanViewAllPlayers (_context.PermissionLevel);
+//
+// 			JsonArray playersJsResult = new JsonArray ();
+//
+// 			Players playersList = PersistentContainer.Instance.Players;
+//
+// 			foreach ((PlatformUserIdentifierAbs userId, Player player) in playersList.Dict) {
+// 				if (admTools != null) {
+// 					if (admTools.IsBanned (userId, out _, out _)) {
+// 						continue;
+// 					}
+// 				}
+//
+// 				if (!listOffline && !player.IsOnline) {
+// 					continue;
+// 				}
+//
+// 				if (!bViewAll && !player.PlatformId.Equals (reqUserId)) {
+// 					continue;
+// 				}
+//
+// 				JsonObject pos = new JsonObject ();
+// 				pos.Add ("x", new JsonNumber (player.LastPosition.x));
+// 				pos.Add ("y", new JsonNumber (player.LastPosition.y));
+// 				pos.Add ("z", new JsonNumber (player.LastPosition.z));
+//
+// 				JsonObject pJson = new JsonObject ();
+// 				pJson.Add ("steamid", new JsonString (userId.CombinedString));
+//
+// 				//					pJson.Add("entityid", new JSONNumber (p.EntityID));
+// 				//                    pJson.Add("ip", new JSONString (p.IP));
+// 				pJson.Add ("name", new JsonString (player.Name));
+// 				pJson.Add ("online", new JsonBoolean (player.IsOnline));
+// 				pJson.Add ("position", pos);
+//
+// 				//					pJson.Add ("totalplaytime", new JSONNumber (p.TotalPlayTime));
+// 				//					pJson.Add ("lastonline", new JSONString (p.LastOnline.ToString ("s")));
+// 				//					pJson.Add ("ping", new JSONNumber (p.IsOnline ? p.ClientInfo.ping : -1));
+//
+// 				playersJsResult.Add (pJson);
+// 			}
+//
+// 			WebUtils.WriteJson (_context.Response, playersJsResult);
+// 		}
+// 	}
+// }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayersOnline.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayersOnline.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/GetPlayersOnline.cs	(revision 402)
@@ -0,0 +1,46 @@
+// using AllocsFixes.PersistentData;
+// using JetBrains.Annotations;
+//
+// namespace Webserver.WebAPI.APIs {
+// 	[UsedImplicitly]
+// 	public class GetPlayersOnline : AbsWebAPI {
+// 		public override void HandleRequest (RequestContext _context) {
+// 			JsonArray players = new JsonArray ();
+//
+// 			World w = GameManager.Instance.World;
+// 			foreach ((int entityId, EntityPlayer entityPlayer) in w.Players.dict) {
+// 				ClientInfo ci = ConnectionManager.Instance.Clients.ForEntityId (entityId);
+// 				Player player = PersistentContainer.Instance.Players [ci.InternalId, false];
+//
+// 				JsonObject pos = new JsonObject ();
+// 				pos.Add ("x", new JsonNumber ((int) entityPlayer.GetPosition ().x));
+// 				pos.Add ("y", new JsonNumber ((int) entityPlayer.GetPosition ().y));
+// 				pos.Add ("z", new JsonNumber ((int) entityPlayer.GetPosition ().z));
+//
+// 				JsonObject p = new JsonObject ();
+// 				p.Add ("steamid", new JsonString (ci.PlatformId.CombinedString));
+// 				p.Add ("entityid", new JsonNumber (ci.entityId));
+// 				p.Add ("ip", new JsonString (ci.ip));
+// 				p.Add ("name", new JsonString (entityPlayer.EntityName));
+// 				p.Add ("online", new JsonBoolean (true));
+// 				p.Add ("position", pos);
+//
+// 				p.Add ("level", new JsonNumber (player?.Level ?? -1));
+// 				p.Add ("health", new JsonNumber (entityPlayer.Health));
+// 				p.Add ("stamina", new JsonNumber (entityPlayer.Stamina));
+// 				p.Add ("zombiekills", new JsonNumber (entityPlayer.KilledZombies));
+// 				p.Add ("playerkills", new JsonNumber (entityPlayer.KilledPlayers));
+// 				p.Add ("playerdeaths", new JsonNumber (entityPlayer.Died));
+// 				p.Add ("score", new JsonNumber (entityPlayer.Score));
+//
+// 				p.Add ("totalplaytime", new JsonNumber (player?.TotalPlayTime ?? -1));
+// 				p.Add ("lastonline", new JsonString (player != null ? player.LastOnline.ToString ("s") : string.Empty));
+// 				p.Add ("ping", new JsonNumber (ci.ping));
+//
+// 				players.Add (p);
+// 			}
+//
+// 			WebUtils.WriteJson (_context.Response, players);
+// 		}
+// 	}
+// }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Hostile.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Hostile.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Hostile.cs	(revision 402)
@@ -0,0 +1,47 @@
+﻿using System.Collections.Generic;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.LiveData;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	internal class Hostile : AbsRestApi {
+		private readonly List<EntityEnemy> entities = new List<EntityEnemy> ();
+
+		private static readonly byte[] jsonKeyId = JsonWriter.GetEncodedPropertyNameWithBeginObject ("id");
+		private static readonly byte[] jsonKeyName = JsonWriter.GetEncodedPropertyNameWithBeginObject ("name");
+		private static readonly byte[] jsonKeyPosition = JsonWriter.GetEncodedPropertyNameWithBeginObject ("position");
+
+		protected override void HandleRestGet (RequestContext _context) {
+			PrepareEnvelopedResult (out JsonWriter writer);
+			writer.WriteBeginArray ();
+			
+			lock (entities) {
+				Hostiles.Instance.Get (entities);
+				
+				for (int i = 0; i < entities.Count; i++) {
+					if (i > 0) {
+						writer.WriteValueSeparator ();
+					}
+					
+					EntityAlive entity = entities [i];
+					Vector3i position = new Vector3i (entity.GetPosition ());
+					
+					writer.WriteRaw (jsonKeyId);
+					writer.WriteInt32 (entity.entityId);
+					
+					writer.WriteRaw (jsonKeyName);
+					writer.WriteString (!string.IsNullOrEmpty (entity.EntityName) ? entity.EntityName : $"enemy class #{entity.entityClass}");
+					
+					writer.WriteRaw (jsonKeyPosition);
+					JsonCommons.WritePositionObject (writer, position);
+
+					writer.WriteEndObject ();
+				}
+			}
+			
+			writer.WriteEndArray ();
+			SendEnvelopedResult (_context, ref writer);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Log.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Log.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Log.cs	(revision 402)
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using Utf8Json;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class Log : AbsRestApi {
+		private const int maxCount = 1000;
+
+		private static readonly byte[] jsonKeyEntries = JsonWriter.GetEncodedPropertyNameWithBeginObject ("entries");
+		private static readonly byte[] jsonKeyFirstLine = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("firstLine");
+		private static readonly byte[] jsonKeyLastLine = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("lastLine");
+
+		private static readonly byte[] jsonMsgKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("msg");
+		private static readonly byte[] jsonTypeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("type");
+		private static readonly byte[] jsonTraceKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("trace");
+		private static readonly byte[] jsonIsotimeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("isotime");
+		private static readonly byte[] jsonUptimeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("uptime");
+		
+		protected override void HandleRestGet (RequestContext _context) {
+			if (_context.Request.QueryString ["count"] == null || !int.TryParse (_context.Request.QueryString ["count"], out int count)) {
+				count = 50;
+			}
+
+			if (count == 0) {
+				count = 1;
+			}
+
+			if (count > maxCount) {
+				count = maxCount;
+			}
+
+			if (count < -maxCount) {
+				count = -maxCount;
+			}
+
+			if (_context.Request.QueryString ["firstLine"] == null || !int.TryParse (_context.Request.QueryString ["firstLine"], out int firstLine)) {
+				firstLine = count > 0 ? LogBuffer.Instance.OldestLine : LogBuffer.Instance.LatestLine;
+			}
+			
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			writer.WriteRaw (jsonKeyEntries);
+			
+			List<LogBuffer.LogEntry> logEntries = LogBuffer.Instance.GetRange (ref firstLine, count, out int lastLine);
+
+			writer.WriteBeginArray ();
+
+			bool first = true;
+			foreach (LogBuffer.LogEntry logEntry in logEntries) {
+				if (!first) {
+					writer.WriteValueSeparator ();
+				}
+
+				first = false;
+				
+				writer.WriteRaw (jsonMsgKey);
+				writer.WriteString (logEntry.message);
+				
+				writer.WriteRaw (jsonTypeKey);
+				writer.WriteString (logEntry.type.ToStringCached ());
+				
+				writer.WriteRaw (jsonTraceKey);
+				writer.WriteString (logEntry.trace);
+				
+				writer.WriteRaw (jsonIsotimeKey);
+				writer.WriteString (logEntry.isoTime);
+				
+				writer.WriteRaw (jsonUptimeKey);
+				writer.WriteString (logEntry.uptime.ToString ());
+				
+				writer.WriteEndObject ();
+			}
+			writer.WriteEndArray ();
+
+			writer.WriteRaw (jsonKeyFirstLine);
+			writer.WriteInt32 (firstLine);
+			
+			writer.WriteRaw (jsonKeyLastLine);
+			writer.WriteInt32 (lastLine);
+			
+			writer.WriteEndObject ();
+
+			SendEnvelopedResult (_context, ref writer);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/ServerInfo.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/ServerInfo.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/ServerInfo.cs	(revision 402)
@@ -0,0 +1,103 @@
+using JetBrains.Annotations;
+using Utf8Json;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class ServerInfo : AbsRestApi {
+		private static readonly UnityEngine.Profiling.CustomSampler buildSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_ServerInfo_BuildSampler");
+
+		private static readonly byte[] keyType = JsonWriter.GetEncodedPropertyNameWithBeginObject ("type");
+		private static readonly byte[] keyValue = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("value");
+
+		private int largestBuffer;
+
+		protected override void HandleRestGet (RequestContext _context) {
+			buildSampler.Begin ();
+
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			writer.EnsureCapacity (largestBuffer);
+			writer.WriteBeginObject ();
+			
+			GameServerInfo gsi = ConnectionManager.Instance.LocalServerInfo;
+
+			bool first = true;
+			
+			
+
+			foreach (GameInfoString stringGamePref in EnumUtils.Values<GameInfoString> ()) {
+				string value = gsi.GetValue (stringGamePref);
+
+				if (!first) {
+					writer.WriteValueSeparator ();
+				}
+
+				first = false;
+				
+				writer.WriteString (stringGamePref.ToStringCached ());
+				writer.WriteNameSeparator ();
+				
+				writer.WriteRaw (keyType);
+				writer.WriteString ("string");
+				
+				writer.WriteRaw (keyValue);
+				writer.WriteString (value);
+				
+				writer.WriteEndObject ();
+			}
+
+			foreach (GameInfoInt intGamePref in EnumUtils.Values<GameInfoInt> ()) {
+				int value = gsi.GetValue (intGamePref);
+
+				if (!first) {
+					writer.WriteValueSeparator ();
+				}
+
+				first = false;
+				
+				writer.WriteString (intGamePref.ToStringCached ());
+				writer.WriteNameSeparator ();
+				
+				writer.WriteRaw (keyType);
+				writer.WriteString ("int");
+				
+				writer.WriteRaw (keyValue);
+				writer.WriteInt32 (value);
+				
+				writer.WriteEndObject ();
+			}
+
+			foreach (GameInfoBool boolGamePref in EnumUtils.Values<GameInfoBool> ()) {
+				bool value = gsi.GetValue (boolGamePref);
+
+				if (!first) {
+					writer.WriteValueSeparator ();
+				}
+
+				first = false;
+				
+				writer.WriteString (boolGamePref.ToStringCached ());
+				writer.WriteNameSeparator ();
+				
+				writer.WriteRaw (keyType);
+				writer.WriteString ("bool");
+				
+				writer.WriteRaw (keyValue);
+				writer.WriteBoolean (value);
+				
+				writer.WriteEndObject ();
+			}
+			
+			writer.WriteEndObject ();
+			
+			buildSampler.End ();
+
+			int bufferContentSize = writer.CurrentOffset + 128;
+			if (bufferContentSize > largestBuffer) {
+				largestBuffer = bufferContentSize;
+			}
+			
+			SendEnvelopedResult (_context, ref writer);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/ServerStats.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/ServerStats.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/ServerStats.cs	(revision 402)
@@ -0,0 +1,53 @@
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.LiveData;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class ServerStats : AbsRestApi {
+		private static readonly byte[] jsonKeyGameTime = JsonWriter.GetEncodedPropertyNameWithBeginObject ("gameTime");
+		private static readonly byte[] jsonKeyPlayers = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("players");
+		private static readonly byte[] jsonKeyHostiles = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("hostiles");
+		private static readonly byte[] jsonKeyAnimals = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("animals");
+		
+		private static readonly byte[] jsonKeyDays = JsonWriter.GetEncodedPropertyNameWithBeginObject ("days");
+		private static readonly byte[] jsonKeyHours = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("hours");
+		private static readonly byte[] jsonKeyMinutes = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("minutes");
+
+		protected override void HandleRestGet (RequestContext _context) {
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			writer.WriteRaw (jsonKeyGameTime);
+
+			(int days, int hours, int minutes) = GameUtils.WorldTimeToElements (GameManager.Instance.World.worldTime);
+			
+			writer.WriteRaw (jsonKeyDays);
+			writer.WriteInt32 (days);
+			
+			writer.WriteRaw (jsonKeyHours);
+			writer.WriteInt32 (hours);
+			
+			writer.WriteRaw (jsonKeyMinutes);
+			writer.WriteInt32 (minutes);
+			
+			writer.WriteEndObject ();
+
+			writer.WriteRaw (jsonKeyPlayers);
+			writer.WriteInt32 (GameManager.Instance.World.Players.Count);
+			
+			writer.WriteRaw (jsonKeyHostiles);
+			writer.WriteInt32 (Hostiles.Instance.GetCount ());
+			
+			writer.WriteRaw (jsonKeyAnimals);
+			writer.WriteInt32 (Animals.Instance.GetCount ());
+			
+			writer.WriteEndObject ();
+
+			SendEnvelopedResult (_context, ref writer);
+		}
+
+		public override int DefaultPermissionLevel () {
+			return 2000;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/WebMods.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/WebMods.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/WebMods.cs	(revision 402)
@@ -0,0 +1,60 @@
+using JetBrains.Annotations;
+using Utf8Json;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class WebMods : AbsRestApi {
+		private readonly byte[] loadedWebMods;
+
+		public WebMods (Web _parent) {
+			JsonWriter writer = new JsonWriter ();
+			writer.WriteBeginArray ();
+
+			bool first = true;
+			foreach (WebMod webMod in _parent.webMods) {
+				if (!first) {
+					writer.WriteValueSeparator ();
+				}
+				first = false;
+				
+				writer.WriteBeginObject ();
+				
+				writer.WriteString ("name");
+				writer.WriteNameSeparator ();
+				writer.WriteString (webMod.ParentMod.Name);
+
+				string webModReactBundle = webMod.ReactBundle;
+				if (webModReactBundle != null) {
+					writer.WriteValueSeparator ();
+					writer.WriteString ("bundle");
+					writer.WriteNameSeparator ();
+					writer.WriteString (webModReactBundle);
+				}
+				
+				string webModCssFile = webMod.CssPath;
+				if (webModCssFile != null) {
+					writer.WriteValueSeparator ();
+					writer.WriteString ("css");
+					writer.WriteNameSeparator ();
+					writer.WriteString (webModCssFile);
+				}
+				
+				writer.WriteEndObject ();
+			}
+			
+			writer.WriteEndArray ();
+
+			loadedWebMods = writer.ToUtf8ByteArray ();
+		}
+
+		protected override void HandleRestGet (RequestContext _context) {
+			PrepareEnvelopedResult (out JsonWriter writer);
+			writer.WriteRaw (loadedWebMods);
+			SendEnvelopedResult (_context, ref writer);
+		}
+
+		public override int DefaultPermissionLevel () {
+			return 2000;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/WebUiUpdates.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/WebUiUpdates.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/WebUiUpdates.cs	(revision 402)
@@ -0,0 +1,63 @@
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.LiveData;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class WebUiUpdates : AbsRestApi {
+		private static readonly byte[] jsonKeyGameTime = JsonWriter.GetEncodedPropertyNameWithBeginObject ("gameTime");
+		private static readonly byte[] jsonKeyPlayers = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("players");
+		private static readonly byte[] jsonKeyHostiles = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("hostiles");
+		private static readonly byte[] jsonKeyAnimals = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("animals");
+		private static readonly byte[] jsonKeyNewLogs = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("newLogs");
+		
+		private static readonly byte[] jsonKeyDays = JsonWriter.GetEncodedPropertyNameWithBeginObject ("days");
+		private static readonly byte[] jsonKeyHours = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("hours");
+		private static readonly byte[] jsonKeyMinutes = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("minutes");
+
+		
+		protected override void HandleRestGet (RequestContext _context) {
+			if (_context.Request.QueryString ["latestLine"] == null ||
+			    !int.TryParse (_context.Request.QueryString ["latestLine"], out int latestLine)) {
+				latestLine = 0;
+			}
+			
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			writer.WriteRaw (jsonKeyGameTime);
+
+			(int days, int hours, int minutes) = GameUtils.WorldTimeToElements (GameManager.Instance.World.worldTime);
+			
+			writer.WriteRaw (jsonKeyDays);
+			writer.WriteInt32 (days);
+			
+			writer.WriteRaw (jsonKeyHours);
+			writer.WriteInt32 (hours);
+			
+			writer.WriteRaw (jsonKeyMinutes);
+			writer.WriteInt32 (minutes);
+			
+			writer.WriteEndObject ();
+
+			writer.WriteRaw (jsonKeyPlayers);
+			writer.WriteInt32 (GameManager.Instance.World.Players.Count);
+			
+			writer.WriteRaw (jsonKeyHostiles);
+			writer.WriteInt32 (Hostiles.Instance.GetCount ());
+			
+			writer.WriteRaw (jsonKeyAnimals);
+			writer.WriteInt32 (Animals.Instance.GetCount ());
+			
+			writer.WriteRaw (jsonKeyNewLogs);
+			writer.WriteInt32 (LogBuffer.Instance.LatestLine - latestLine);
+
+			writer.WriteEndObject ();
+
+			SendEnvelopedResult (_context, ref writer);
+		}
+
+		public override int DefaultPermissionLevel () {
+			return 2000;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs	(revision 401)
+++ binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs	(revision 402)
@@ -1,6 +1,7 @@
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Net;
-using AllocsFixes.JSON;
+using Utf8Json;
 
 namespace Webserver.WebAPI {
@@ -9,20 +10,28 @@
 
 		public sealed override void HandleRequest (RequestContext _context) {
-			JsonNode jsonBody = null;
+			IDictionary<string, object> inputJson = null;
+			byte[] jsonInputData = null;
+			
+			if (_context.Request.HasEntityBody) {
+				Stream requestInputStream = _context.Request.InputStream;
+				
+				jsonInputData = new byte[_context.Request.ContentLength64];
+				requestInputStream.Read (jsonInputData, 0, (int)_context.Request.ContentLength64);
+				
+				try {
+					jsonDeserializeSampler.Begin ();
+					inputJson = JsonSerializer.Deserialize<IDictionary<string, object>> (jsonInputData);
+					
+					// Log.Out ("JSON body:");
+					// foreach ((string key, object value) in inputJson) {
+					// 	Log.Out ($" - {key} = {value} ({value.GetType ()})");
+					// }
+					
+					jsonDeserializeSampler.End ();
+				} catch (Exception e) {
+					jsonDeserializeSampler.End ();
 
-			if (_context.Request.HasEntityBody) {
-				string body = new StreamReader (_context.Request.InputStream).ReadToEnd ();
-
-				if (!string.IsNullOrEmpty (body)) {
-					try {
-						jsonDeserializeSampler.Begin ();
-						jsonBody = Parser.Parse (body);
-						jsonDeserializeSampler.End ();
-					} catch (Exception e) {
-						jsonDeserializeSampler.End ();
-
-						SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "INVALID_BODY", e);
-						return;
-					}
+					SendErrorResult (_context, HttpStatusCode.BadRequest, null, "INVALID_BODY", e);
+					return;
 				}
 			}
@@ -31,6 +40,6 @@
 				switch (_context.Request.HttpMethod) {
 					case "GET":
-						if (jsonBody != null) {
-							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, jsonBody, "GET_WITH_BODY");
+						if (inputJson != null) {
+							SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "GET_WITH_BODY");
 							return;
 						}
@@ -40,36 +49,36 @@
 					case "POST":
 						if (!string.IsNullOrEmpty (_context.RequestPath)) {
-							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, jsonBody, "POST_WITH_ID");
+							SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "POST_WITH_ID");
 							return;
 						}
 
-						if (jsonBody == null) {
-							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "POST_WITHOUT_BODY");
+						if (inputJson == null) {
+							SendErrorResult (_context, HttpStatusCode.BadRequest, null, "POST_WITHOUT_BODY");
 							return;
 						}
 
-						HandleRestPost (_context, jsonBody);
+						HandleRestPost (_context, inputJson, jsonInputData);
 						return;
 					case "PUT":
 						if (string.IsNullOrEmpty (_context.RequestPath)) {
-							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, jsonBody, "PUT_WITHOUT_ID");
+							SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "PUT_WITHOUT_ID");
 							return;
 						}
 
-						if (jsonBody == null) {
-							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "PUT_WITHOUT_BODY");
+						if (inputJson == null) {
+							SendErrorResult (_context, HttpStatusCode.BadRequest, null, "PUT_WITHOUT_BODY");
 							return;
 						}
 
-						HandleRestPut (_context, jsonBody);
+						HandleRestPut (_context, inputJson, jsonInputData);
 						return;
 					case "DELETE":
 						if (string.IsNullOrEmpty (_context.RequestPath)) {
-							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, jsonBody, "DELETE_WITHOUT_ID");
+							SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "DELETE_WITHOUT_ID");
 							return;
 						}
 
-						if (jsonBody != null) {
-							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "DELETE_WITH_BODY");
+						if (inputJson != null) {
+							SendErrorResult (_context, HttpStatusCode.BadRequest, null, "DELETE_WITH_BODY");
 							return;
 						}
@@ -78,50 +87,50 @@
 						return;
 					default:
-						SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "INVALID_METHOD");
+						SendErrorResult (_context, HttpStatusCode.BadRequest, null, "INVALID_METHOD");
 						return;
 				}
 			} catch (Exception e) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.InternalServerError, jsonBody, "ERROR_PROCESSING", e);
+				SendErrorResult (_context, HttpStatusCode.InternalServerError, jsonInputData, "ERROR_PROCESSING", e);
 			}
 		}
 
-		private static readonly JsonArray emptyData = new JsonArray ();
-
-		protected void SendEnvelopedResult (RequestContext _context, JsonNode _resultData, HttpStatusCode _statusCode = HttpStatusCode.OK,
-			JsonNode _jsonInputBody = null, string _errorCode = null, Exception _exception = null) {
-			JsonObject meta = new JsonObject ();
-
-			meta.Add ("serverTime", new JsonString (DateTime.Now.ToString ("o")));
-			if (!string.IsNullOrEmpty (_errorCode)) {
-				meta.Add ("requestMethod", new JsonString (_context.Request.HttpMethod));
-				meta.Add ("requestSubpath", new JsonString (_context.RequestPath));
-				meta.Add ("requestBody", new JsonString (_jsonInputBody?.ToString () ?? "-empty-"));
-				meta.Add ("errorCode", new JsonString (_errorCode));
-				if (_exception != null) {
-					meta.Add ("exceptionMessage", new JsonString (_exception.Message));
-					meta.Add ("exceptionTrace", new JsonString (_exception.StackTrace));
-				}
-			}
-
-			JsonObject envelope = new JsonObject ();
-			envelope.Add ("meta", meta);
-			envelope.Add ("data", _resultData ?? emptyData);
-
-			WebUtils.WriteJson (_context.Response, envelope, _statusCode);
+		protected void SendErrorResult (RequestContext _context, HttpStatusCode _statusCode, byte[] _jsonInputData = null, string _errorCode = null, Exception _exception = null) {
+			PrepareEnvelopedResult (out JsonWriter writer);
+			writer.WriteRaw (JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, _statusCode, _jsonInputData, _errorCode, _exception);
 		}
 
-		protected bool TryGetJsonField (JsonObject _jsonObject, string _fieldName, out int _value) {
+		static AbsRestApi () {
+			JsonWriter writer = new JsonWriter ();
+			writer.WriteBeginArray ();
+			writer.WriteEndArray ();
+			JsonEmptyData = writer.ToUtf8ByteArray ();
+		}
+
+		protected static readonly byte[] JsonEmptyData;
+		
+		protected void PrepareEnvelopedResult (out JsonWriter _writer) {
+			WebUtils.PrepareEnvelopedResult (out _writer);
+		}
+
+		protected void SendEnvelopedResult (RequestContext _context, ref JsonWriter _writer, HttpStatusCode _statusCode = HttpStatusCode.OK,
+			byte[] _jsonInputData = null, string _errorCode = null, Exception _exception = null) {
+			
+			WebUtils.SendEnvelopedResult (_context, ref _writer, _statusCode, _jsonInputData, _errorCode, _exception);
+		}
+
+		protected bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out int _value) {
 			_value = default;
 			
-			if (!_jsonObject.TryGetValue (_fieldName, out JsonNode fieldNode)) {
+			if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
 				return false;
 			}
 
-			if (!(fieldNode is JsonValue valueField)) {
+			if (fieldNode is not double value) {
 				return false;
 			}
 
 			try {
-				_value = valueField.AsInt;
+				_value = (int)value;
 				return true;
 			} catch (Exception) {
@@ -130,17 +139,17 @@
 		}
 
-		protected bool TryGetJsonField (JsonObject _jsonObject, string _fieldName, out double _value) {
+		protected bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out double _value) {
 			_value = default;
 			
-			if (!_jsonObject.TryGetValue (_fieldName, out JsonNode fieldNode)) {
+			if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
 				return false;
 			}
 
-			if (!(fieldNode is JsonValue valueField)) {
+			if (fieldNode is not double value) {
 				return false;
 			}
 
 			try {
-				_value = valueField.AsDouble;
+				_value = value;
 				return true;
 			} catch (Exception) {
@@ -149,17 +158,17 @@
 		}
 
-		protected bool TryGetJsonField (JsonObject _jsonObject, string _fieldName, out string _value) {
+		protected bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out string _value) {
 			_value = default;
 			
-			if (!_jsonObject.TryGetValue (_fieldName, out JsonNode fieldNode)) {
+			if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
 				return false;
 			}
 
-			if (!(fieldNode is JsonValue valueField)) {
+			if (fieldNode is not string value) {
 				return false;
 			}
 
 			try {
-				_value = valueField.AsString;
+				_value = value;
 				return true;
 			} catch (Exception) {
@@ -168,11 +177,19 @@
 		}
 
-		protected abstract void HandleRestGet (RequestContext _context);
+		protected virtual void HandleRestGet (RequestContext _context) {
+			SendErrorResult (_context, HttpStatusCode.MethodNotAllowed, null, "Unsupported");
+		}
 
-		protected abstract void HandleRestPost (RequestContext _context, JsonNode _jsonBody);
+		protected virtual void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			SendErrorResult (_context, HttpStatusCode.MethodNotAllowed, _jsonInputData, "Unsupported");
+		}
 
-		protected abstract void HandleRestPut (RequestContext _context, JsonNode _jsonBody);
+		protected virtual void HandleRestPut (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			SendErrorResult (_context, HttpStatusCode.MethodNotAllowed, _jsonInputData, "Unsupported");
+		}
 
-		protected abstract void HandleRestDelete (RequestContext _context);
+		protected virtual void HandleRestDelete (RequestContext _context) {
+			SendErrorResult (_context, HttpStatusCode.MethodNotAllowed, null, "Unsupported");
+		}
 	}
 }
Index: binary-improvements2/WebServer/src/WebAPI/ExecuteConsoleCommand.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/ExecuteConsoleCommand.cs	(revision 401)
+++ 	(revision )
@@ -1,52 +1,0 @@
-using System;
-using System.Net;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class ExecuteConsoleCommand : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			if (string.IsNullOrEmpty (_context.Request.QueryString ["command"])) {
-				WebUtils.WriteText (_context.Response, "No command given", HttpStatusCode.BadRequest);
-				return;
-			}
-
-			WebCommandResult.ResultType responseType = WebCommandResult.ResultType.Full;
-
-			string formatArg = _context.Request.QueryString ["format"];
-			if (formatArg != null) {
-				if (formatArg.EqualsCaseInsensitive ("raw")) {
-					responseType = WebCommandResult.ResultType.Raw;
-				} else if (formatArg.EqualsCaseInsensitive ("simple")) {
-					responseType = WebCommandResult.ResultType.ResultOnly;
-				}
-			}
-			
-			string commandline = _context.Request.QueryString ["command"];
-			string commandPart = commandline.Split (' ') [0];
-			string argumentsPart = commandline.Substring (Math.Min (commandline.Length, commandPart.Length + 1));
-
-			IConsoleCommand command = SdtdConsole.Instance.GetCommand (commandline);
-
-			if (command == null) {
-				WebUtils.WriteText (_context.Response, "Unknown command", HttpStatusCode.NotFound);
-				return;
-			}
-
-			int commandPermissionLevel = GameManager.Instance.adminTools.GetCommandPermissionLevel (command.GetCommands ());
-
-			if (_context.PermissionLevel > commandPermissionLevel) {
-				WebUtils.WriteText (_context.Response, "You are not allowed to execute this command", HttpStatusCode.Forbidden);
-				return;
-			}
-
-			_context.Response.SendChunked = true;
-			WebCommandResult wcr = new WebCommandResult (commandPart, argumentsPart, responseType, _context.Response);
-			SdtdConsole.Instance.ExecuteAsync (commandline, wcr);
-		}
-
-		public override int DefaultPermissionLevel () {
-			return 2000;
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetAllowedCommands.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetAllowedCommands.cs	(revision 401)
+++ 	(revision )
@@ -1,43 +1,0 @@
-using AllocsFixes.JSON;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetAllowedCommands : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			JsonObject result = new JsonObject ();
-			JsonArray entries = new JsonArray ();
-			foreach (IConsoleCommand cc in SdtdConsole.Instance.GetCommands ()) {
-				int commandPermissionLevel = GameManager.Instance.adminTools.GetCommandPermissionLevel (cc.GetCommands ());
-				if (_context.PermissionLevel > commandPermissionLevel) {
-					continue;
-				}
-
-				JsonArray cmdOverloads = new JsonArray ();
-
-				string cmd = string.Empty;
-				foreach (string s in cc.GetCommands ()) {
-					cmdOverloads.Add (new JsonString (s));
-					if (s.Length > cmd.Length) {
-						cmd = s;
-					}
-				}
-
-				JsonObject cmdObj = new JsonObject ();
-				cmdObj.Add ("command", new JsonString (cmd));
-				cmdObj.Add ("overloads", cmdOverloads);
-				cmdObj.Add ("description", new JsonString (cc.GetDescription ()));
-				cmdObj.Add ("help", new JsonString (cc.GetHelp ()));
-				entries.Add (cmdObj);
-			}
-
-			result.Add ("commands", entries);
-
-			WebUtils.WriteJson (_context.Response, result);
-		}
-
-		public override int DefaultPermissionLevel () {
-			return 2000;
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetAnimalsLocation.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetAnimalsLocation.cs	(revision 401)
+++ 	(revision )
@@ -1,41 +1,0 @@
-﻿using System.Collections.Generic;
-using AllocsFixes.JSON;
-using AllocsFixes.LiveData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	internal class GetAnimalsLocation : AbsWebAPI {
-		private readonly List<EntityAnimal> animals = new List<EntityAnimal> ();
-
-		public override void HandleRequest (RequestContext _context) {
-			JsonArray animalsJsResult = new JsonArray ();
-
-			Animals.Instance.Get (animals);
-			for (int i = 0; i < animals.Count; i++) {
-				EntityAnimal entity = animals [i];
-				Vector3i position = new Vector3i (entity.GetPosition ());
-
-				JsonObject jsonPOS = new JsonObject ();
-				jsonPOS.Add ("x", new JsonNumber (position.x));
-				jsonPOS.Add ("y", new JsonNumber (position.y));
-				jsonPOS.Add ("z", new JsonNumber (position.z));
-
-				JsonObject pJson = new JsonObject ();
-				pJson.Add ("id", new JsonNumber (entity.entityId));
-
-				if (!string.IsNullOrEmpty (entity.EntityName)) {
-					pJson.Add ("name", new JsonString (entity.EntityName));
-				} else {
-					pJson.Add ("name", new JsonString ("animal class #" + entity.entityClass));
-				}
-
-				pJson.Add ("position", jsonPOS);
-
-				animalsJsResult.Add (pJson);
-			}
-
-			WebUtils.WriteJson (_context.Response, animalsJsResult);
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetHostileLocation.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetHostileLocation.cs	(revision 401)
+++ 	(revision )
@@ -1,41 +1,0 @@
-﻿using System.Collections.Generic;
-using AllocsFixes.JSON;
-using AllocsFixes.LiveData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	internal class GetHostileLocation : AbsWebAPI {
-		private readonly List<EntityEnemy> enemies = new List<EntityEnemy> ();
-
-		public override void HandleRequest (RequestContext _context) {
-			JsonArray hostilesJsResult = new JsonArray ();
-
-			Hostiles.Instance.Get (enemies);
-			for (int i = 0; i < enemies.Count; i++) {
-				EntityEnemy entity = enemies [i];
-				Vector3i position = new Vector3i (entity.GetPosition ());
-
-				JsonObject jsonPOS = new JsonObject ();
-				jsonPOS.Add ("x", new JsonNumber (position.x));
-				jsonPOS.Add ("y", new JsonNumber (position.y));
-				jsonPOS.Add ("z", new JsonNumber (position.z));
-
-				JsonObject pJson = new JsonObject ();
-				pJson.Add ("id", new JsonNumber (entity.entityId));
-
-				if (!string.IsNullOrEmpty (entity.EntityName)) {
-					pJson.Add ("name", new JsonString (entity.EntityName));
-				} else {
-					pJson.Add ("name", new JsonString ("enemy class #" + entity.entityClass));
-				}
-
-				pJson.Add ("position", jsonPOS);
-
-				hostilesJsResult.Add (pJson);
-			}
-
-			WebUtils.WriteJson (_context.Response, hostilesJsResult);
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetLandClaims.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetLandClaims.cs	(revision 401)
+++ 	(revision )
@@ -1,78 +1,0 @@
-using System.Collections.Generic;
-using System.Net;
-using AllocsFixes;
-using AllocsFixes.JSON;
-using AllocsFixes.PersistentData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetLandClaims : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			PlatformUserIdentifierAbs requestedUserId = null;
-			if (_context.Request.QueryString ["userid"] != null) {
-				if (!PlatformUserIdentifierAbs.TryFromCombinedString (_context.Request.QueryString ["userid"], out requestedUserId)) {
-					WebUtils.WriteText (_context.Response, "Invalid user id given", HttpStatusCode.BadRequest);
-					return;
-				}
-			}
-
-			// default user, cheap way to avoid 'null reference exception'
-			PlatformUserIdentifierAbs userId = _context.Connection?.UserId;
-
-			bool bViewAll = WebConnection.CanViewAllClaims (_context.PermissionLevel);
-
-			JsonObject result = new JsonObject ();
-			result.Add ("claimsize", new JsonNumber (GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("LandClaimSize"))));
-
-			JsonArray claimOwners = new JsonArray ();
-			result.Add ("claimowners", claimOwners);
-
-			LandClaimList.OwnerFilter[] ownerFilters = null;
-			if (requestedUserId != null || !bViewAll) {
-				if (requestedUserId != null && !bViewAll) {
-					ownerFilters = new[] {
-						LandClaimList.UserIdFilter (userId),
-						LandClaimList.UserIdFilter (requestedUserId)
-					};
-				} else if (!bViewAll) {
-					ownerFilters = new[] {LandClaimList.UserIdFilter (userId)};
-				} else {
-					ownerFilters = new[] {LandClaimList.UserIdFilter (requestedUserId)};
-				}
-			}
-
-			LandClaimList.PositionFilter[] posFilters = null;
-
-			Dictionary<Player, List<Vector3i>> claims = LandClaimList.GetLandClaims (ownerFilters, posFilters);
-
-			foreach ((Player player, List<Vector3i> claimPositions) in claims) {
-				JsonObject owner = new JsonObject ();
-				claimOwners.Add (owner);
-
-				owner.Add ("steamid", new JsonString (player.PlatformId.CombinedString));
-				owner.Add ("claimactive", new JsonBoolean (player.LandProtectionActive));
-
-				if (player.Name.Length > 0) {
-					owner.Add ("playername", new JsonString (player.Name));
-				} else {
-					owner.Add ("playername", new JsonNull ());
-				}
-
-				JsonArray claimsJson = new JsonArray ();
-				owner.Add ("claims", claimsJson);
-
-				foreach (Vector3i v in claimPositions) {
-					JsonObject claim = new JsonObject ();
-					claim.Add ("x", new JsonNumber (v.x));
-					claim.Add ("y", new JsonNumber (v.y));
-					claim.Add ("z", new JsonNumber (v.z));
-
-					claimsJson.Add (claim);
-				}
-			}
-
-			WebUtils.WriteJson (_context.Response, result);
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetLog.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetLog.cs	(revision 401)
+++ 	(revision )
@@ -1,53 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.JSON;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetLog : AbsWebAPI {
-		private const int MAX_COUNT = 1000;
-		
-		public override void HandleRequest (RequestContext _context) {
-			if (_context.Request.QueryString ["count"] == null || !int.TryParse (_context.Request.QueryString ["count"], out int count)) {
-				count = 50;
-			}
-
-			if (count == 0) {
-				count = 1;
-			}
-
-			if (count > MAX_COUNT) {
-				count = MAX_COUNT;
-			}
-
-			if (count < -MAX_COUNT) {
-				count = -MAX_COUNT;
-			}
-
-			if (_context.Request.QueryString ["firstLine"] == null || !int.TryParse (_context.Request.QueryString ["firstLine"], out int firstLine)) {
-				firstLine = count > 0 ? LogBuffer.Instance.OldestLine : LogBuffer.Instance.LatestLine;
-			}
-
-			JsonObject result = new JsonObject ();
-
-			List<LogBuffer.LogEntry> logEntries = LogBuffer.Instance.GetRange (ref firstLine, count, out int lastLine);
-
-			JsonArray entries = new JsonArray ();
-			foreach (LogBuffer.LogEntry logEntry in logEntries) {
-				JsonObject entry = new JsonObject ();
-				entry.Add ("isotime", new JsonString (logEntry.isoTime));
-				entry.Add ("uptime", new JsonString (logEntry.uptime.ToString ()));
-				entry.Add ("msg", new JsonString (logEntry.message));
-				entry.Add ("trace", new JsonString (logEntry.trace));
-				entry.Add ("type", new JsonString (logEntry.type.ToStringCached ()));
-				entries.Add (entry);
-			}
-
-			result.Add ("firstLine", new JsonNumber (firstLine));
-			result.Add ("lastLine", new JsonNumber (lastLine));
-			result.Add ("entries", entries);
-
-			WebUtils.WriteJson (_context.Response, result);
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetPlayerInventories.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetPlayerInventories.cs	(revision 401)
+++ 	(revision )
@@ -1,27 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.JSON;
-using AllocsFixes.PersistentData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetPlayerInventories : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			GetPlayerInventory.GetInventoryArguments (_context.Request, out bool showIconColor, out bool showIconName);
-
-			JsonArray allInventoriesResult = new JsonArray ();
-
-			foreach ((PlatformUserIdentifierAbs userId, Player player) in PersistentContainer.Instance.Players.Dict) {
-				if (player == null) {
-					continue;
-				}
-
-				if (player.IsOnline) {
-					allInventoriesResult.Add (GetPlayerInventory.DoPlayer (userId.CombinedString, player, showIconColor, showIconName));
-				}
-			}
-
-			WebUtils.WriteJson (_context.Response, allInventoriesResult);
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetPlayerInventory.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetPlayerInventory.cs	(revision 401)
+++ 	(revision )
@@ -1,129 +1,0 @@
-using System.Collections.Generic;
-using System.Net;
-using AllocsFixes.JSON;
-using AllocsFixes.PersistentData;
-using JetBrains.Annotations;
-using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetPlayerInventory : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			if (_context.Request.QueryString ["userid"] == null) {
-				WebUtils.WriteText (_context.Response, "No user id given", HttpStatusCode.BadRequest);
-				return;
-			}
-
-			string userIdString = _context.Request.QueryString ["userid"];
-			if (!PlatformUserIdentifierAbs.TryFromCombinedString (userIdString, out PlatformUserIdentifierAbs userId)) {
-				WebUtils.WriteText (_context.Response, "Invalid user id given", HttpStatusCode.BadRequest);
-				return;
-			}
-
-			Player p = PersistentContainer.Instance.Players [userId, false];
-			if (p == null) {
-				WebUtils.WriteText (_context.Response, "Unknown user id given", HttpStatusCode.NotFound);
-				return;
-			}
-
-			GetInventoryArguments (_context.Request, out bool showIconColor, out bool showIconName);
-
-			JsonObject result = DoPlayer (userIdString, p, showIconColor, showIconName);
-
-			WebUtils.WriteJson (_context.Response, result);
-		}
-
-		internal static void GetInventoryArguments (HttpListenerRequest _req, out bool _showIconColor, out bool _showIconName) {
-			if (_req.QueryString ["showiconcolor"] == null || !bool.TryParse (_req.QueryString ["showiconcolor"], out _showIconColor)) {
-				_showIconColor = true;
-			}
-			
-			if (_req.QueryString ["showiconname"] == null || !bool.TryParse (_req.QueryString ["showiconname"], out _showIconName)) {
-				_showIconName = true;
-			}
-		}
-
-		internal static JsonObject DoPlayer (string _steamId, Player _player, bool _showIconColor, bool _showIconName) {
-			AllocsFixes.PersistentData.Inventory inv = _player.Inventory;
-
-			JsonObject result = new JsonObject ();
-
-			JsonArray bag = new JsonArray ();
-			JsonArray belt = new JsonArray ();
-			JsonObject equipment = new JsonObject ();
-			result.Add ("userid", new JsonString (_steamId));
-			result.Add ("entityid", new JsonNumber (_player.EntityID));
-			result.Add ("playername", new JsonString (_player.Name));
-			result.Add ("bag", bag);
-			result.Add ("belt", belt);
-			result.Add ("equipment", equipment);
-
-			DoInventory (belt, inv.belt, _showIconColor, _showIconName);
-			DoInventory (bag, inv.bag, _showIconColor, _showIconName);
-
-			AddEquipment (equipment, "head", inv.equipment, EquipmentSlots.Headgear, _showIconColor, _showIconName);
-			AddEquipment (equipment, "eyes", inv.equipment, EquipmentSlots.Eyewear, _showIconColor, _showIconName);
-			AddEquipment (equipment, "face", inv.equipment, EquipmentSlots.Face, _showIconColor, _showIconName);
-
-			AddEquipment (equipment, "armor", inv.equipment, EquipmentSlots.ChestArmor, _showIconColor, _showIconName);
-			AddEquipment (equipment, "jacket", inv.equipment, EquipmentSlots.Jacket, _showIconColor, _showIconName);
-			AddEquipment (equipment, "shirt", inv.equipment, EquipmentSlots.Shirt, _showIconColor, _showIconName);
-
-			AddEquipment (equipment, "legarmor", inv.equipment, EquipmentSlots.LegArmor, _showIconColor, _showIconName);
-			AddEquipment (equipment, "pants", inv.equipment, EquipmentSlots.Legs, _showIconColor, _showIconName);
-			AddEquipment (equipment, "boots", inv.equipment, EquipmentSlots.Feet, _showIconColor, _showIconName);
-
-			AddEquipment (equipment, "gloves", inv.equipment, EquipmentSlots.Hands, _showIconColor, _showIconName);
-
-			return result;
-		}
-
-		private static void DoInventory (JsonArray _jsonRes, List<InvItem> _inv, bool _showIconColor, bool _showIconName) {
-			for (int i = 0; i < _inv.Count; i++) {
-				_jsonRes.Add (GetJsonForItem (_inv [i], _showIconColor, _showIconName));
-			}
-		}
-
-		private static void AddEquipment (JsonObject _eq, string _slotname, InvItem[] _items, EquipmentSlots _slot, bool _showIconColor, bool _showIconName) {
-			int[] slotindices = XUiM_PlayerEquipment.GetSlotIndicesByEquipmentSlot (_slot);
-
-			for (int i = 0; i < slotindices.Length; i++) {
-				if (_items? [slotindices [i]] == null) {
-					continue;
-				}
-
-				InvItem item = _items [slotindices [i]];
-				_eq.Add (_slotname, GetJsonForItem (item, _showIconColor, _showIconName));
-				return;
-			}
-
-			_eq.Add (_slotname, new JsonNull ());
-		}
-
-		private static JsonNode GetJsonForItem (InvItem _item, bool _showIconColor, bool _showIconName) {
-			if (_item == null) {
-				return new JsonNull ();
-			}
-
-			JsonObject jsonItem = new JsonObject ();
-			jsonItem.Add ("count", new JsonNumber (_item.count));
-			jsonItem.Add ("name", new JsonString (_item.itemName));
-			
-			if (_showIconName) {
-				jsonItem.Add ("icon", new JsonString (_item.icon));
-			}
-
-			if (_showIconColor) {
-				jsonItem.Add ("iconcolor", new JsonString (_item.iconcolor));
-			}
-
-			jsonItem.Add ("quality", new JsonNumber (_item.quality));
-			if (_item.quality >= 0) {
-				jsonItem.Add ("qualitycolor", new JsonString (QualityInfo.GetQualityColorHex (_item.quality)));
-			}
-
-			return jsonItem;
-
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetPlayerList.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetPlayerList.cs	(revision 401)
+++ 	(revision )
@@ -1,264 +1,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.RegularExpressions;
-using AllocsFixes.JSON;
-using AllocsFixes.PersistentData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetPlayerList : AbsWebAPI {
-		private static readonly Regex numberFilterMatcher =
-			new Regex (@"^(>=|=>|>|<=|=<|<|==|=)?\s*([0-9]+(\.[0-9]*)?)$");
-
-		private static readonly UnityEngine.Profiling.CustomSampler jsonSerializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Build");
-
-		public override void HandleRequest (RequestContext _context) {
-			AdminTools admTools = GameManager.Instance.adminTools;
-			PlatformUserIdentifierAbs userId = _context.Connection?.UserId;
-
-			bool bViewAll = WebConnection.CanViewAllPlayers (_context.PermissionLevel);
-
-			// TODO: Sort (and filter?) prior to converting to JSON ... hard as how to get the correct column's data? (i.e. column name matches JSON object field names, not source data)
-
-			int rowsPerPage = 25;
-			if (_context.Request.QueryString ["rowsperpage"] != null) {
-				int.TryParse (_context.Request.QueryString ["rowsperpage"], out rowsPerPage);
-			}
-
-			int page = 0;
-			if (_context.Request.QueryString ["page"] != null) {
-				int.TryParse (_context.Request.QueryString ["page"], out page);
-			}
-
-			int firstEntry = page * rowsPerPage;
-
-			Players playersList = PersistentContainer.Instance.Players;
-
-			
-			List<JsonObject> playerList = new List<JsonObject> ();
-
-			jsonSerializeSampler.Begin ();
-
-			foreach (KeyValuePair<PlatformUserIdentifierAbs, Player> kvp in playersList.Dict) {
-				Player p = kvp.Value;
-
-				if (bViewAll || p.PlatformId.Equals (userId)) {
-					JsonObject pos = new JsonObject ();
-					pos.Add ("x", new JsonNumber (p.LastPosition.x));
-					pos.Add ("y", new JsonNumber (p.LastPosition.y));
-					pos.Add ("z", new JsonNumber (p.LastPosition.z));
-
-					JsonObject pJson = new JsonObject ();
-					pJson.Add ("steamid", new JsonString (kvp.Key.CombinedString));
-					pJson.Add ("entityid", new JsonNumber (p.EntityID));
-					pJson.Add ("ip", new JsonString (p.IP));
-					pJson.Add ("name", new JsonString (p.Name));
-					pJson.Add ("online", new JsonBoolean (p.IsOnline));
-					pJson.Add ("position", pos);
-
-					pJson.Add ("totalplaytime", new JsonNumber (p.TotalPlayTime));
-					pJson.Add ("lastonline",
-						new JsonString (p.LastOnline.ToUniversalTime ().ToString ("yyyy-MM-ddTHH:mm:ssZ")));
-					pJson.Add ("ping", new JsonNumber (p.IsOnline ? p.ClientInfo.ping : -1));
-
-					JsonBoolean banned = admTools != null ? new JsonBoolean (admTools.IsBanned (kvp.Key, out _, out _)) : new JsonBoolean (false);
-
-					pJson.Add ("banned", banned);
-
-					playerList.Add (pJson);
-				}
-			}
-
-			jsonSerializeSampler.End ();
-
-			IEnumerable<JsonObject> list = playerList;
-
-			foreach (string key in _context.Request.QueryString.AllKeys) {
-				if (!string.IsNullOrEmpty (key) && key.StartsWith ("filter[")) {
-					string filterCol = key.Substring (key.IndexOf ('[') + 1);
-					filterCol = filterCol.Substring (0, filterCol.Length - 1);
-					string filterVal = _context.Request.QueryString.Get (key).Trim ();
-
-					list = ExecuteFilter (list, filterCol, filterVal);
-				}
-			}
-
-			int totalAfterFilter = list.Count ();
-
-			foreach (string key in _context.Request.QueryString.AllKeys) {
-				if (!string.IsNullOrEmpty (key) && key.StartsWith ("sort[")) {
-					string sortCol = key.Substring (key.IndexOf ('[') + 1);
-					sortCol = sortCol.Substring (0, sortCol.Length - 1);
-					string sortVal = _context.Request.QueryString.Get (key);
-
-					list = ExecuteSort (list, sortCol, sortVal == "0");
-				}
-			}
-
-			list = list.Skip (firstEntry);
-			list = list.Take (rowsPerPage);
-
-
-			JsonArray playersJsResult = new JsonArray ();
-			foreach (JsonObject jsO in list) {
-				playersJsResult.Add (jsO);
-			}
-
-			JsonObject result = new JsonObject ();
-			result.Add ("total", new JsonNumber (totalAfterFilter));
-			result.Add ("totalUnfiltered", new JsonNumber (playerList.Count));
-			result.Add ("firstResult", new JsonNumber (firstEntry));
-			result.Add ("players", playersJsResult);
-
-			WebUtils.WriteJson (_context.Response, result);
-		}
-
-		private IEnumerable<JsonObject> ExecuteFilter (IEnumerable<JsonObject> _list, string _filterCol,
-			string _filterVal) {
-			if (!_list.Any()) {
-				return _list;
-			}
-
-			if (_list.First ().ContainsKey (_filterCol)) {
-				Type colType = _list.First () [_filterCol].GetType ();
-				if (colType == typeof (JsonNumber)) {
-					return ExecuteNumberFilter (_list, _filterCol, _filterVal);
-				}
-
-				if (colType == typeof (JsonBoolean)) {
-					bool value = StringParsers.ParseBool (_filterVal);
-					return _list.Where (_line => ((JsonBoolean) _line [_filterCol]).GetBool () == value);
-				}
-
-				if (colType == typeof (JsonString)) {
-					// regex-match whole ^string$, replace * by .*, ? by .?, + by .+
-					_filterVal = _filterVal.Replace ("*", ".*").Replace ("?", ".?").Replace ("+", ".+");
-					_filterVal = "^" + _filterVal + "$";
-
-					//Log.Out ("GetPlayerList: Filter on String with Regex '" + _filterVal + "'");
-					Regex matcher = new Regex (_filterVal, RegexOptions.IgnoreCase);
-					return _list.Where (_line => matcher.IsMatch (((JsonString) _line [_filterCol]).GetString ()));
-				}
-			}
-
-			return _list;
-		}
-
-
-		private IEnumerable<JsonObject> ExecuteNumberFilter (IEnumerable<JsonObject> _list, string _filterCol,
-			string _filterVal) {
-			// allow value (exact match), =, ==, >=, >, <=, <
-			Match filterMatch = numberFilterMatcher.Match (_filterVal);
-			if (filterMatch.Success) {
-				double value = StringParsers.ParseDouble (filterMatch.Groups [2].Value);
-				NumberMatchType matchType;
-				double epsilon = value / 100000;
-				switch (filterMatch.Groups [1].Value) {
-					case "":
-					case "=":
-					case "==":
-						matchType = NumberMatchType.Equal;
-						break;
-					case ">":
-						matchType = NumberMatchType.Greater;
-						break;
-					case ">=":
-					case "=>":
-						matchType = NumberMatchType.GreaterEqual;
-						break;
-					case "<":
-						matchType = NumberMatchType.Lesser;
-						break;
-					case "<=":
-					case "=<":
-						matchType = NumberMatchType.LesserEqual;
-						break;
-					default:
-						matchType = NumberMatchType.Equal;
-						break;
-				}
-
-				return _list.Where (delegate (JsonObject _line) {
-					double objVal = ((JsonNumber) _line [_filterCol]).GetDouble ();
-					switch (matchType) {
-						case NumberMatchType.Greater:
-							return objVal > value;
-						case NumberMatchType.GreaterEqual:
-							return objVal >= value;
-						case NumberMatchType.Lesser:
-							return objVal < value;
-						case NumberMatchType.LesserEqual:
-							return objVal <= value;
-						case NumberMatchType.Equal:
-						default:
-							return NearlyEqual (objVal, value, epsilon);
-					}
-				});
-			}
-
-			Log.Out ("[Web] GetPlayerList: ignoring invalid filter for number-column '{0}': '{1}'", _filterCol, _filterVal);
-			return _list;
-		}
-
-
-		private IEnumerable<JsonObject> ExecuteSort (IEnumerable<JsonObject> _list, string _sortCol, bool _ascending) {
-			if (_list.Count () == 0) {
-				return _list;
-			}
-
-			if (_list.First ().ContainsKey (_sortCol)) {
-				Type colType = _list.First () [_sortCol].GetType ();
-				if (colType == typeof (JsonNumber)) {
-					if (_ascending) {
-						return _list.OrderBy (_line => ((JsonNumber) _line [_sortCol]).GetDouble ());
-					}
-
-					return _list.OrderByDescending (_line => ((JsonNumber) _line [_sortCol]).GetDouble ());
-				}
-
-				if (colType == typeof (JsonBoolean)) {
-					if (_ascending) {
-						return _list.OrderBy (_line => ((JsonBoolean) _line [_sortCol]).GetBool ());
-					}
-
-					return _list.OrderByDescending (_line => ((JsonBoolean) _line [_sortCol]).GetBool ());
-				}
-
-				if (_ascending) {
-					return _list.OrderBy (_line => _line [_sortCol].ToString ());
-				}
-
-				return _list.OrderByDescending (_line => _line [_sortCol].ToString ());
-			}
-
-			return _list;
-		}
-
-
-		private bool NearlyEqual (double _a, double _b, double _epsilon) {
-			double absA = Math.Abs (_a);
-			double absB = Math.Abs (_b);
-			double diff = Math.Abs (_a - _b);
-
-			if (_a == _b) {
-				return true;
-			}
-
-			if (_a == 0 || _b == 0 || diff < double.Epsilon) {
-				return diff < _epsilon;
-			}
-
-			return diff / (absA + absB) < _epsilon;
-		}
-
-		private enum NumberMatchType {
-			Equal,
-			Greater,
-			GreaterEqual,
-			Lesser,
-			LesserEqual
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetPlayersLocation.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetPlayersLocation.cs	(revision 401)
+++ 	(revision )
@@ -1,63 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.JSON;
-using AllocsFixes.PersistentData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetPlayersLocation : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			AdminTools admTools = GameManager.Instance.adminTools;
-			PlatformUserIdentifierAbs reqUserId = _context.Connection?.UserId;
-
-			bool listOffline = false;
-			if (_context.Request.QueryString ["offline"] != null) {
-				bool.TryParse (_context.Request.QueryString ["offline"], out listOffline);
-			}
-
-			bool bViewAll = WebConnection.CanViewAllPlayers (_context.PermissionLevel);
-
-			JsonArray playersJsResult = new JsonArray ();
-
-			Players playersList = PersistentContainer.Instance.Players;
-
-			foreach ((PlatformUserIdentifierAbs userId, Player player) in playersList.Dict) {
-				if (admTools != null) {
-					if (admTools.IsBanned (userId, out _, out _)) {
-						continue;
-					}
-				}
-
-				if (!listOffline && !player.IsOnline) {
-					continue;
-				}
-
-				if (!bViewAll && !player.PlatformId.Equals (reqUserId)) {
-					continue;
-				}
-
-				JsonObject pos = new JsonObject ();
-				pos.Add ("x", new JsonNumber (player.LastPosition.x));
-				pos.Add ("y", new JsonNumber (player.LastPosition.y));
-				pos.Add ("z", new JsonNumber (player.LastPosition.z));
-
-				JsonObject pJson = new JsonObject ();
-				pJson.Add ("steamid", new JsonString (userId.CombinedString));
-
-				//					pJson.Add("entityid", new JSONNumber (p.EntityID));
-				//                    pJson.Add("ip", new JSONString (p.IP));
-				pJson.Add ("name", new JsonString (player.Name));
-				pJson.Add ("online", new JsonBoolean (player.IsOnline));
-				pJson.Add ("position", pos);
-
-				//					pJson.Add ("totalplaytime", new JSONNumber (p.TotalPlayTime));
-				//					pJson.Add ("lastonline", new JSONString (p.LastOnline.ToString ("s")));
-				//					pJson.Add ("ping", new JSONNumber (p.IsOnline ? p.ClientInfo.ping : -1));
-
-				playersJsResult.Add (pJson);
-			}
-
-			WebUtils.WriteJson (_context.Response, playersJsResult);
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetPlayersOnline.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetPlayersOnline.cs	(revision 401)
+++ 	(revision )
@@ -1,48 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.JSON;
-using AllocsFixes.PersistentData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetPlayersOnline : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			JsonArray players = new JsonArray ();
-
-			World w = GameManager.Instance.World;
-			foreach ((int entityId, EntityPlayer entityPlayer) in w.Players.dict) {
-				ClientInfo ci = ConnectionManager.Instance.Clients.ForEntityId (entityId);
-				Player player = PersistentContainer.Instance.Players [ci.InternalId, false];
-
-				JsonObject pos = new JsonObject ();
-				pos.Add ("x", new JsonNumber ((int) entityPlayer.GetPosition ().x));
-				pos.Add ("y", new JsonNumber ((int) entityPlayer.GetPosition ().y));
-				pos.Add ("z", new JsonNumber ((int) entityPlayer.GetPosition ().z));
-
-				JsonObject p = new JsonObject ();
-				p.Add ("steamid", new JsonString (ci.PlatformId.CombinedString));
-				p.Add ("entityid", new JsonNumber (ci.entityId));
-				p.Add ("ip", new JsonString (ci.ip));
-				p.Add ("name", new JsonString (entityPlayer.EntityName));
-				p.Add ("online", new JsonBoolean (true));
-				p.Add ("position", pos);
-
-				p.Add ("level", new JsonNumber (player?.Level ?? -1));
-				p.Add ("health", new JsonNumber (entityPlayer.Health));
-				p.Add ("stamina", new JsonNumber (entityPlayer.Stamina));
-				p.Add ("zombiekills", new JsonNumber (entityPlayer.KilledZombies));
-				p.Add ("playerkills", new JsonNumber (entityPlayer.KilledPlayers));
-				p.Add ("playerdeaths", new JsonNumber (entityPlayer.Died));
-				p.Add ("score", new JsonNumber (entityPlayer.Score));
-
-				p.Add ("totalplaytime", new JsonNumber (player?.TotalPlayTime ?? -1));
-				p.Add ("lastonline", new JsonString (player != null ? player.LastOnline.ToString ("s") : string.Empty));
-				p.Add ("ping", new JsonNumber (ci.ping));
-
-				players.Add (p);
-			}
-
-			WebUtils.WriteJson (_context.Response, players);
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetServerInfo.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetServerInfo.cs	(revision 401)
+++ 	(revision )
@@ -1,47 +1,0 @@
-using System;
-using AllocsFixes.JSON;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetServerInfo : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			JsonObject serverInfo = new JsonObject ();
-
-			GameServerInfo gsi = ConnectionManager.Instance.LocalServerInfo;
-
-			foreach (string stringGamePref in Enum.GetNames (typeof (GameInfoString))) {
-				string value = gsi.GetValue ((GameInfoString) Enum.Parse (typeof (GameInfoString), stringGamePref));
-
-				JsonObject singleStat = new JsonObject ();
-				singleStat.Add ("type", new JsonString ("string"));
-				singleStat.Add ("value", new JsonString (value));
-
-				serverInfo.Add (stringGamePref, singleStat);
-			}
-
-			foreach (string intGamePref in Enum.GetNames (typeof (GameInfoInt))) {
-				int value = gsi.GetValue ((GameInfoInt) Enum.Parse (typeof (GameInfoInt), intGamePref));
-
-				JsonObject singleStat = new JsonObject ();
-				singleStat.Add ("type", new JsonString ("int"));
-				singleStat.Add ("value", new JsonNumber (value));
-
-				serverInfo.Add (intGamePref, singleStat);
-			}
-
-			foreach (string boolGamePref in Enum.GetNames (typeof (GameInfoBool))) {
-				bool value = gsi.GetValue ((GameInfoBool) Enum.Parse (typeof (GameInfoBool), boolGamePref));
-
-				JsonObject singleStat = new JsonObject ();
-				singleStat.Add ("type", new JsonString ("bool"));
-				singleStat.Add ("value", new JsonBoolean (value));
-
-				serverInfo.Add (boolGamePref, singleStat);
-			}
-
-
-			WebUtils.WriteJson (_context.Response, serverInfo);
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetStats.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetStats.cs	(revision 401)
+++ 	(revision )
@@ -1,28 +1,0 @@
-using AllocsFixes.JSON;
-using AllocsFixes.LiveData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetStats : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			JsonObject result = new JsonObject ();
-
-			JsonObject time = new JsonObject ();
-			time.Add ("days", new JsonNumber (GameUtils.WorldTimeToDays (GameManager.Instance.World.worldTime)));
-			time.Add ("hours", new JsonNumber (GameUtils.WorldTimeToHours (GameManager.Instance.World.worldTime)));
-			time.Add ("minutes", new JsonNumber (GameUtils.WorldTimeToMinutes (GameManager.Instance.World.worldTime)));
-			result.Add ("gametime", time);
-
-			result.Add ("players", new JsonNumber (GameManager.Instance.World.Players.Count));
-			result.Add ("hostiles", new JsonNumber (Hostiles.Instance.GetCount ()));
-			result.Add ("animals", new JsonNumber (Animals.Instance.GetCount ()));
-
-			WebUtils.WriteJson (_context.Response, result);
-		}
-
-		public override int DefaultPermissionLevel () {
-			return 2000;
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetWebMods.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetWebMods.cs	(revision 401)
+++ 	(revision )
@@ -1,37 +1,0 @@
-using AllocsFixes.JSON;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetWebMods : AbsWebAPI {
-		private readonly JsonArray loadedWebMods = new JsonArray ();
-
-		public GetWebMods (Web _parent) {
-			foreach (WebMod webMod in _parent.webMods) {
-				JsonObject modJson = new JsonObject ();
-
-				modJson.Add ("name", new JsonString (webMod.ParentMod.ModInfo.Name.Value));
-				
-				string webModReactBundle = webMod.ReactBundle;
-				if (webModReactBundle != null) {
-					modJson.Add ("bundle", new JsonString (webModReactBundle));
-				}
-
-				string webModCssFile = webMod.CssPath;
-				if (webModCssFile != null) {
-					modJson.Add ("css", new JsonString (webModCssFile));
-				}
-
-				loadedWebMods.Add (modJson);
-			}
-		}
-
-		public override void HandleRequest (RequestContext _context) {
-			WebUtils.WriteJson (_context.Response, loadedWebMods);
-		}
-
-		public override int DefaultPermissionLevel () {
-			return 2000;
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/GetWebUIUpdates.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/GetWebUIUpdates.cs	(revision 401)
+++ 	(revision )
@@ -1,35 +1,0 @@
-using AllocsFixes.JSON;
-using AllocsFixes.LiveData;
-using JetBrains.Annotations;
-
-namespace Webserver.WebAPI {
-	[UsedImplicitly]
-	public class GetWebUIUpdates : AbsWebAPI {
-		public override void HandleRequest (RequestContext _context) {
-			if (_context.Request.QueryString ["latestLine"] == null ||
-			    !int.TryParse (_context.Request.QueryString ["latestLine"], out int latestLine)) {
-				latestLine = 0;
-			}
-
-			JsonObject result = new JsonObject ();
-
-			JsonObject time = new JsonObject ();
-			time.Add ("days", new JsonNumber (GameUtils.WorldTimeToDays (GameManager.Instance.World.worldTime)));
-			time.Add ("hours", new JsonNumber (GameUtils.WorldTimeToHours (GameManager.Instance.World.worldTime)));
-			time.Add ("minutes", new JsonNumber (GameUtils.WorldTimeToMinutes (GameManager.Instance.World.worldTime)));
-			result.Add ("gametime", time);
-
-			result.Add ("players", new JsonNumber (GameManager.Instance.World.Players.Count));
-			result.Add ("hostiles", new JsonNumber (Hostiles.Instance.GetCount ()));
-			result.Add ("animals", new JsonNumber (Animals.Instance.GetCount ()));
-
-			result.Add ("newlogs", new JsonNumber (LogBuffer.Instance.LatestLine - latestLine));
-
-			WebUtils.WriteJson (_context.Response, result);
-		}
-
-		public override int DefaultPermissionLevel () {
-			return 2000;
-		}
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/JsonCommons.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/JsonCommons.cs	(revision 402)
+++ binary-improvements2/WebServer/src/WebAPI/JsonCommons.cs	(revision 402)
@@ -0,0 +1,23 @@
+using Utf8Json;
+
+namespace Webserver.WebAPI {
+	public static class JsonCommons {
+		private static readonly byte[] jsonKeyPositionX = JsonWriter.GetEncodedPropertyNameWithBeginObject ("x");
+		private static readonly byte[] jsonKeyPositionY = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("y");
+		private static readonly byte[] jsonKeyPositionZ = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("z");
+
+		public static void WritePositionObject (JsonWriter _writer, Vector3i _position) {
+			_writer.WriteRaw (jsonKeyPositionX);
+			_writer.WriteInt32 (_position.x);
+			
+			_writer.WriteRaw (jsonKeyPositionY);
+			_writer.WriteInt32 (_position.y);
+			
+			_writer.WriteRaw (jsonKeyPositionZ);
+			_writer.WriteInt32 (_position.z);
+			
+			_writer.WriteEndObject ();
+		}
+		
+	}
+}
Index: binary-improvements2/WebServer/src/WebCommandResult.cs
===================================================================
--- binary-improvements2/WebServer/src/WebCommandResult.cs	(revision 401)
+++ binary-improvements2/WebServer/src/WebCommandResult.cs	(revision 402)
@@ -4,7 +4,6 @@
 using System.Net.Sockets;
 using System.Text;
-using AllocsFixes.JSON;
 using UnityEngine;
-using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
+using Utf8Json;
 
 namespace Webserver {
@@ -19,9 +18,9 @@
 		private readonly string parameters;
 
-		private readonly HttpListenerResponse response;
+		private readonly RequestContext context;
 		private readonly ResultType responseType;
 
-		public WebCommandResult (string _command, string _parameters, ResultType _responseType, HttpListenerResponse _response) {
-			response = _response;
+		public WebCommandResult (string _command, string _parameters, ResultType _responseType, RequestContext _context) {
+			context = _context;
 			command = _command;
 			parameters = _parameters;
@@ -29,4 +28,10 @@
 		}
 
+		private static readonly byte[] jsonRawKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("resultRaw");
+		
+		private static readonly byte[] jsonCommandKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("command");
+		private static readonly byte[] jsonParametersKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("parameters");
+		private static readonly byte[] jsonResultKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("result");
+		
 		public void SendLines (List<string> _output) {
 			StringBuilder sb = new StringBuilder ();
@@ -35,35 +40,41 @@
 			}
 
+			string commandOutput = sb.ToString ();
+
 			try {
-				response.SendChunked = false;
-
 				if (responseType == ResultType.Raw) {
-					WebUtils.WriteText (response, sb.ToString ());
+					WebUtils.WriteText (context.Response, commandOutput);
 				} else {
-					JsonNode result;
+					WebUtils.PrepareEnvelopedResult (out JsonWriter writer);
+			
 					if (responseType == ResultType.ResultOnly) {
-						result = new JsonString (sb.ToString ());
+						writer.WriteRaw (jsonRawKey);
+						writer.WriteString (commandOutput);
+						writer.WriteEndObject ();
 					} else {
-						JsonObject resultObj = new JsonObject ();
-
-						resultObj.Add ("command", new JsonString (command));
-						resultObj.Add ("parameters", new JsonString (parameters));
-						resultObj.Add ("result", new JsonString (sb.ToString ()));
-
-						result = resultObj;
+						writer.WriteRaw (jsonCommandKey);
+						writer.WriteString (command);
+						
+						writer.WriteRaw (jsonParametersKey);
+						writer.WriteString (parameters);
+						
+						writer.WriteRaw (jsonResultKey);
+						writer.WriteString (commandOutput);
+						
+						writer.WriteEndObject ();
 					}
 
-					WebUtils.WriteJson (response, result);
+					WebUtils.SendEnvelopedResult (context, ref writer);
 				}
 			} catch (IOException e) {
 				if (e.InnerException is SocketException) {
-					Log.Warning ("[Web] Error in WebCommandResult.SendLines(): Remote host closed connection: " + e.InnerException.Message);
+					Log.Warning ($"[Web] Error in WebCommandResult.SendLines(): Remote host closed connection: {e.InnerException.Message}");
 				} else {
-					Log.Warning ("[Web] Error (IO) in WebCommandResult.SendLines(): " + e);
+					Log.Warning ($"[Web] Error (IO) in WebCommandResult.SendLines(): {e}");
 				}
 			} catch (Exception e) {
-				Log.Warning ("[Web] Error in WebCommandResult.SendLines(): " + e);
+				Log.Warning ($"[Web] Error in WebCommandResult.SendLines(): {e}");
 			} finally {
-				response?.Close ();
+				context?.Response?.Close ();
 			}
 		}
@@ -82,5 +93,5 @@
 
 		public string GetDescription () {
-			return "WebCommandResult_for_" + command;
+			return $"WebCommandResult_for_{command}";
 		}
 	}
Index: binary-improvements2/WebServer/src/WebConnection.cs
===================================================================
--- binary-improvements2/WebServer/src/WebConnection.cs	(revision 401)
+++ binary-improvements2/WebServer/src/WebConnection.cs	(revision 402)
@@ -25,5 +25,5 @@
 			login = DateTime.Now;
 			lastAction = login;
-			conDescription = "WebPanel from " + Endpoint;
+			conDescription = $"WebPanel from {Endpoint}";
 		}
 
Index: binary-improvements2/WebServer/src/WebMod.cs
===================================================================
--- binary-improvements2/WebServer/src/WebMod.cs	(revision 401)
+++ binary-improvements2/WebServer/src/WebMod.cs	(revision 402)
@@ -1,4 +1,4 @@
 using System.IO;
-using AllocsFixes.FileCache;
+using Webserver.FileCache;
 using Webserver.UrlHandlers;
 
@@ -14,5 +14,5 @@
 
 		public WebMod (Web _parentWeb, Mod _parentMod, bool _useStaticCache) {
-			string folder = _parentMod.Path + "/WebMod";
+			string folder = $"{_parentMod.Path}/WebMod";
 			if (!Directory.Exists (folder)) {
 				throw new InvalidDataException("No WebMod folder in mod");
@@ -21,17 +21,9 @@
 			string urlWebModBase = $"{modsBaseUrl}{_parentMod.FolderName}/";
 
-			ReactBundle = folder + "/" + reactBundleName;
-			if (File.Exists (ReactBundle)) {
-				ReactBundle = urlWebModBase + reactBundleName;
-			} else {
-				ReactBundle = null;
-			}
+			ReactBundle = $"{folder}/{reactBundleName}";
+			ReactBundle = File.Exists (ReactBundle) ? $"{urlWebModBase}{reactBundleName}" : null;
 
-			CssPath = folder + "/" + stylingFileName;
-			if (File.Exists (CssPath)) {
-				CssPath = urlWebModBase + stylingFileName;
-			} else {
-				CssPath = null;
-			}
+			CssPath = $"{folder}/{stylingFileName}";
+			CssPath = File.Exists (CssPath) ? $"{urlWebModBase}{stylingFileName}" : null;
 
 			if (ReactBundle == null && CssPath == null) {
@@ -43,5 +35,5 @@
 			_parentWeb.RegisterPathHandler (urlWebModBase, new StaticHandler (
 				folder,
-				_useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
+				_useStaticCache ? new SimpleCache () : new DirectAccess (),
 				false)
 			);
Index: binary-improvements2/WebServer/src/WebPermissions.cs
===================================================================
--- binary-improvements2/WebServer/src/WebPermissions.cs	(revision 401)
+++ binary-improvements2/WebServer/src/WebPermissions.cs	(revision 402)
@@ -36,7 +36,7 @@
 		private readonly ReadOnlyCollection<WebModulePermission> allModulesListRo;
 
-		private static string SettingsFilePath => GamePrefs.GetString (EnumUtils.Parse<EnumGamePrefs> ("SaveGameFolder"));
+		private static string SettingsFilePath => GamePrefs.GetString (EnumUtils.Parse<EnumGamePrefs> (nameof (EnumGamePrefs.SaveGameFolder)));
 		private static string SettingsFileName => permissionsFileName;
-		private static string SettingsFullPath => SettingsFilePath + "/" + SettingsFileName;
+		private static string SettingsFullPath => $"{SettingsFilePath}/{SettingsFileName}";
 
 		private WebPermissions () {
@@ -184,5 +184,5 @@
 
 		private void OnFileChanged (object _source, FileSystemEventArgs _e) {
-			Log.Out ("[Web] [Perms] Reloading " + SettingsFileName);
+			Log.Out ($"[Web] [Perms] Reloading {SettingsFileName}");
 			Load ();
 		}
@@ -205,5 +205,5 @@
 				xmlDoc.Load (SettingsFullPath);
 			} catch (XmlException e) {
-				Log.Error ("[Web] [Perms] Failed loading permissions file: " + e.Message);
+				Log.Error ($"[Web] [Perms] Failed loading permissions file: {e.Message}");
 				return;
 			}
Index: binary-improvements2/WebServer/src/WebUtils.cs
===================================================================
--- binary-improvements2/WebServer/src/WebUtils.cs	(revision 401)
+++ binary-improvements2/WebServer/src/WebUtils.cs	(revision 402)
@@ -1,6 +1,7 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Net;
 using System.Text;
-using AllocsFixes.JSON;
+using Utf8Json;
 using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
 using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
@@ -8,20 +9,12 @@
 namespace Webserver {
 	public static class WebUtils {
+		// ReSharper disable once MemberCanBePrivate.Global
 		public const string MimePlain = "text/plain";
 		public const string MimeHtml = "text/html";
+		// ReSharper disable once MemberCanBePrivate.Global
 		public const string MimeJson = "application/json";
 		
-		private static readonly UnityEngine.Profiling.CustomSampler jsonSerializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Serialize");
+		private static readonly UnityEngine.Profiling.CustomSampler envelopeBuildSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_EnvelopeBuilding");
 		private static readonly UnityEngine.Profiling.CustomSampler netWriteSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Write");
-
-		public static void WriteJson (HttpListenerResponse _resp, JsonNode _root, HttpStatusCode _statusCode = HttpStatusCode.OK) {
-			jsonSerializeSampler.Begin ();
-			StringBuilder sb = new StringBuilder ();
-			_root.ToString (sb);
-			jsonSerializeSampler.End ();
-			netWriteSampler.Begin ();
-			WriteText (_resp, sb.ToString(), _statusCode, MimeJson);
-			netWriteSampler.End ();
-		}
 
 		public static void WriteText (HttpListenerResponse _resp, string _text, HttpStatusCode _statusCode = HttpStatusCode.OK, string _mimeType = null) {
@@ -43,4 +36,78 @@
 			return Guid.NewGuid ().ToString ();
 		}
+
+		[SuppressMessage ("ReSharper", "MemberCanBePrivate.Global")]
+		public static void WriteJsonData (HttpListenerResponse _resp, ref JsonWriter _jsonWriter, HttpStatusCode _statusCode = HttpStatusCode.OK) {
+			ArraySegment<byte> jsonData = _jsonWriter.GetBuffer ();
+
+			netWriteSampler.Begin ();
+			_resp.StatusCode = (int)_statusCode;
+			_resp.ContentType = MimeJson;
+			_resp.ContentEncoding = Encoding.UTF8;
+			_resp.ContentLength64 = jsonData.Count;
+			_resp.OutputStream.Write (jsonData.Array!, 0, jsonData.Count);
+			netWriteSampler.End ();
+		}
+		
+		
+		private static readonly byte[] jsonRawDataKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("data"); // {"data":
+
+		private static readonly byte[] jsonRawMetaKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("meta"); // ,"meta":
+		private static readonly byte[] jsonRawMetaServertimeKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("serverTime"); // {"serverTime":
+		private static readonly byte[] jsonRawMetaRequestMethodKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("requestMethod"); // ,"requestMethod":
+		private static readonly byte[] jsonRawMetaRequestSubpathKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("requestSubpath"); // ,"requestSubpath":
+		private static readonly byte[] jsonRawMetaRequestBodyKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("requestBody"); // ,"requestBody":
+		private static readonly byte[] jsonRawMetaErrorCodeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("errorCode"); // ,"errorCode":
+		private static readonly byte[] jsonRawMetaExceptionMessageKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("exceptionMessage"); // ,"exceptionMessage":
+		private static readonly byte[] jsonRawMetaExceptionTraceKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("exceptionTrace"); // ,"exceptionTrace":
+		
+		public static void PrepareEnvelopedResult (out JsonWriter _writer) {
+			_writer = new JsonWriter ();
+			_writer.WriteRaw (jsonRawDataKey);
+		}
+
+		public static void SendEnvelopedResult (RequestContext _context, ref JsonWriter _writer, HttpStatusCode _statusCode = HttpStatusCode.OK,
+			byte[] _jsonInputData = null, string _errorCode = null, Exception _exception = null) {
+			
+			envelopeBuildSampler.Begin ();
+			
+			_writer.WriteRaw (jsonRawMetaKey);
+
+			_writer.WriteRaw (jsonRawMetaServertimeKey);
+			_writer.WriteString (DateTime.Now.ToString ("o"));
+
+			if (!string.IsNullOrEmpty (_errorCode)) {
+				_writer.WriteRaw (jsonRawMetaRequestMethodKey);
+				_writer.WriteString (_context.Request.HttpMethod);
+
+				_writer.WriteRaw (jsonRawMetaRequestSubpathKey);
+				_writer.WriteString (_context.RequestPath);
+
+				_writer.WriteRaw (jsonRawMetaRequestBodyKey);
+				if (_jsonInputData != null) {
+					_writer.WriteRaw (_jsonInputData);
+				} else {
+					_writer.WriteNull ();
+				}
+
+				_writer.WriteRaw (jsonRawMetaErrorCodeKey);
+				_writer.WriteString (_errorCode);
+
+				if (_exception != null) {
+					_writer.WriteRaw (jsonRawMetaExceptionMessageKey);
+					_writer.WriteString (_exception.Message);
+
+					_writer.WriteRaw (jsonRawMetaExceptionTraceKey);
+					_writer.WriteString (_exception.StackTrace);
+				}
+			}
+			
+			_writer.WriteEndObject (); // End of "meta" object
+			_writer.WriteEndObject (); // End of overall result object
+			
+			envelopeBuildSampler.End ();
+
+			WriteJsonData (_context.Response, ref _writer, _statusCode);
+		}
 	}
 }
