Index: binary-improvements2/7dtd-binaries/README.txt
===================================================================
--- binary-improvements2/7dtd-binaries/README.txt	(revision 401)
+++ binary-improvements2/7dtd-binaries/README.txt	(revision 402)
@@ -2,4 +2,5 @@
 - Assembly-CSharp.dll
 - Assembly-CSharp-firstpass.dll
+- com.rlabrecque.steamworks.net.dll
 - LogLibrary.dll
 - mscorlib.dll
@@ -9,2 +10,3 @@
 - UnityEngine.dll
 - UnityEngine.ImageConversionModule.dll
+- Utf8Json.dll
Index: binary-improvements2/7dtd-server-fixes/7dtd-server-fixes.csproj
===================================================================
--- binary-improvements2/7dtd-server-fixes/7dtd-server-fixes.csproj	(revision 401)
+++ binary-improvements2/7dtd-server-fixes/7dtd-server-fixes.csproj	(revision 402)
@@ -89,8 +89,4 @@
   <ItemGroup>
     <Compile Include="src\AssemblyInfo.cs" />
-    <Compile Include="src\FileCache\InvalidateCachesCmd.cs" />
-    <Compile Include="src\JSON\JsonManualBuilder.cs" />
-    <Compile Include="src\LiveData\Animals.cs" />
-    <Compile Include="src\LiveData\Hostiles.cs" />
     <Compile Include="src\PersistentData\PersistentContainer.cs" />
     <Compile Include="src\PersistentData\InvItem.cs" />
@@ -98,23 +94,7 @@
     <Compile Include="src\PersistentData\Players.cs" />
     <Compile Include="src\PersistentData\Player.cs" />
-    <Compile Include="src\JSON\JsonNode.cs" />
-    <Compile Include="src\JSON\JsonArray.cs" />
-    <Compile Include="src\JSON\JsonObject.cs" />
-    <Compile Include="src\JSON\JsonNumber.cs" />
-    <Compile Include="src\JSON\JsonString.cs" />
-    <Compile Include="src\JSON\JsonBoolean.cs" />
-    <Compile Include="src\JSON\Parser.cs" />
-    <Compile Include="src\JSON\JsonNull.cs" />
-    <Compile Include="src\JSON\MalformedJSONException.cs" />
-    <Compile Include="src\FileCache\AbstractCache.cs" />
-    <Compile Include="src\FileCache\DirectAccess.cs" />
-    <Compile Include="src\FileCache\SimpleCache.cs" />
-    <Compile Include="src\FileCache\MapTileCache.cs" />
     <Compile Include="src\ModApi.cs" />
-    <Compile Include="src\AllocsUtils.cs" />
     <Compile Include="src\LandClaimList.cs" />
     <Compile Include="src\PersistentData\Attributes.cs" />
-    <Compile Include="src\JSON\JsonValue.cs" />
-    <Compile Include="src\LiveData\EntityFilterList.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
@@ -122,5 +102,4 @@
     <Folder Include="src\" />
     <Folder Include="src\PersistentData\" />
-    <Folder Include="src\FileCache\" />
   </ItemGroup>
   <ItemGroup>
Index: binary-improvements2/7dtd-server-fixes/ModInfo.xml
===================================================================
--- binary-improvements2/7dtd-server-fixes/ModInfo.xml	(revision 401)
+++ binary-improvements2/7dtd-server-fixes/ModInfo.xml	(revision 402)
@@ -1,10 +1,9 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <xml>
-	<ModInfo>
-		<Name value="Server extensions" />
-		<Description value="Common functions" />
-		<Author value="The Fun Pimps LLC" />
-		<Version value="1" />
-		<Website value="" />
-	</ModInfo>
+	<Name value="TFP_ServerExtensions" />
+	<DisplayName value="Server Extensions (base)" />
+	<Description value="Common functions for other mods" />
+	<Author value="The Fun Pimps LLC" />
+	<Version value="21.0" />
+	<Website value="" />
 </xml>
Index: binary-improvements2/7dtd-server-fixes/src/AllocsUtils.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/AllocsUtils.cs	(revision 401)
+++ 	(revision )
@@ -1,9 +1,0 @@
-using UnityEngine;
-
-namespace AllocsFixes {
-	public static class AllocsUtils {
-		public static string ColorToHex (Color _color) {
-			return $"{(int)(_color.r * 255):X02}{(int)(_color.g * 255):X02}{(int)(_color.b * 255):X02}";
-		}
-	}
-}
Index: binary-improvements2/7dtd-server-fixes/src/ModApi.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/ModApi.cs	(revision 401)
+++ binary-improvements2/7dtd-server-fixes/src/ModApi.cs	(revision 402)
@@ -30,11 +30,6 @@
 			}
 
-			Log.Out ("Player connected" +
-			         ", entityid=" + _cInfo.entityId +
-			         ", name=" + _cInfo.playerName +
-			         ", pltfmid=" + (_cInfo.PlatformId?.CombinedString ?? "<unknown>") +
-			         ", crossid=" + (_cInfo.CrossplatformId?.CombinedString ?? "<unknown/none>") +
-			         ", steamOwner=" + (owner ?? "<unknown/none>") +
-			         ", ip=" + _cInfo.ip
+			Log.Out (
+				$"Player connected, entityid={_cInfo.entityId}, name={_cInfo.playerName}, pltfmid={_cInfo.PlatformId?.CombinedString ?? "<unknown>"}, crossid={_cInfo.CrossplatformId?.CombinedString ?? "<unknown/none>"}, steamOwner={owner ?? "<unknown/none>"}, ip={_cInfo.ip}"
 			);
 		}
@@ -66,8 +61,8 @@
 
 			if (_cInfo != null) {
-				Log.Out ("Sent chat hook reply to {0}", _cInfo.InternalId);
+				Log.Out ($"Sent chat hook reply to {_cInfo.InternalId}");
 				_cInfo.SendPackage (NetPackageManager.GetPackage<NetPackageChat> ().Setup (EChatType.Whisper, -1, testChatAnswer, "", false, null));
 			} else {
-				Log.Error ("ChatHookExample: Argument _cInfo null on message: {0}", _msg);
+				Log.Error ($"ChatHookExample: Argument _cInfo null on message: {_msg}");
 			}
 
Index: binary-improvements2/7dtd-server-fixes/src/PersistentData/Inventory.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/PersistentData/Inventory.cs	(revision 401)
+++ binary-improvements2/7dtd-server-fixes/src/PersistentData/Inventory.cs	(revision 402)
@@ -67,6 +67,5 @@
 
 			if (_count > maxAllowed) {
-				Log.Out ("Player with ID " + _playerId + " has stack for \"" + name + "\" greater than allowed (" +
-				         _count + " > " + maxAllowed + ")");
+				Log.Out ($"Player with ID {_playerId} has stack for \"{name}\" greater than allowed ({_count} > {maxAllowed})");
 			}
 
@@ -80,5 +79,5 @@
 			item.icon = itemClass.GetIconName ();
 
-			item.iconcolor = AllocsUtils.ColorToHex (itemClass.GetIconTint ());
+			item.iconcolor = itemClass.GetIconTint ().ToHexCode ();
 
 			return item;
Index: binary-improvements2/7dtd-server-fixes/src/PersistentData/PersistentContainer.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/PersistentData/PersistentContainer.cs	(revision 401)
+++ binary-improvements2/7dtd-server-fixes/src/PersistentData/PersistentContainer.cs	(revision 402)
@@ -22,5 +22,5 @@
 
 		public void Save () {
-			Stream stream = File.Open (GameIO.GetSaveGameDir () + "/AllocsPeristentData.bin", FileMode.Create);
+			Stream stream = File.Open ($"{GameIO.GetSaveGameDir ()}/AllocsPeristentData.bin", FileMode.Create);
 			BinaryFormatter bFormatter = new BinaryFormatter ();
 			bFormatter.Serialize (stream, this);
@@ -29,10 +29,10 @@
 
 		public static bool Load () {
-			if (!File.Exists (GameIO.GetSaveGameDir () + "/AllocsPeristentData.bin")) {
+			if (!File.Exists ($"{GameIO.GetSaveGameDir ()}/AllocsPeristentData.bin")) {
 				return false;
 			}
 
 			try {
-				Stream stream = File.Open (GameIO.GetSaveGameDir () + "/AllocsPeristentData.bin", FileMode.Open);
+				Stream stream = File.Open ($"{GameIO.GetSaveGameDir ()}/AllocsPeristentData.bin", FileMode.Open);
 				BinaryFormatter bFormatter = new BinaryFormatter ();
 				PersistentContainer obj = (PersistentContainer) bFormatter.Deserialize (stream);
Index: binary-improvements2/7dtd-server-fixes/src/PersistentData/Player.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/PersistentData/Player.cs	(revision 401)
+++ binary-improvements2/7dtd-server-fixes/src/PersistentData/Player.cs	(revision 402)
@@ -117,5 +117,5 @@
 			}
 
-			Log.Out ("Player set to offline: " + platformId);
+			Log.Out ($"Player set to offline: {platformId}");
 			lastOnline = DateTime.Now;
 			try {
@@ -133,5 +133,5 @@
 
 		public void SetOnline (ClientInfo _ci) {
-			Log.Out ("Player set to online: " + platformId);
+			Log.Out ($"Player set to online: {platformId}");
 			clientInfo = _ci;
             entityId = _ci.entityId;
Index: binary-improvements2/7dtd-server-fixes/src/PersistentData/Players.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/PersistentData/Players.cs	(revision 401)
+++ binary-improvements2/7dtd-server-fixes/src/PersistentData/Players.cs	(revision 402)
@@ -22,5 +22,5 @@
 				}
 
-				Log.Out ("Created new player entry for ID: " + _platformId);
+				Log.Out ($"Created new player entry for ID: {_platformId}");
 				Player p = new Player (_platformId);
 				Dict.Add (_platformId, p);
Index: binary-improvements2/CommandExtensions/CommandExtensions.csproj
===================================================================
--- binary-improvements2/CommandExtensions/CommandExtensions.csproj	(revision 401)
+++ binary-improvements2/CommandExtensions/CommandExtensions.csproj	(revision 402)
@@ -67,4 +67,5 @@
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="src\Commands\TestLogSpam.cs" />
     <Compile Include="src\ModApi.cs" />
     <Compile Include="src\AssemblyInfo.cs" />
Index: binary-improvements2/CommandExtensions/ModInfo.xml
===================================================================
--- binary-improvements2/CommandExtensions/ModInfo.xml	(revision 401)
+++ binary-improvements2/CommandExtensions/ModInfo.xml	(revision 402)
@@ -1,10 +1,9 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <xml>
-	<ModInfo>
-		<Name value="Command extensions" />
-		<Description value="Additional commands for server operation" />
-		<Author value="The Fun Pimps LLC" />
-		<Version value="1" />
-		<Website value="" />
-	</ModInfo>
+	<Name value="TFP_CommandExtensions" />
+	<DisplayName value="Server Command Extensions" />
+	<Description value="Additional commands for server operation" />
+	<Author value="The Fun Pimps LLC" />
+	<Version value="21.0" />
+	<Website value="" />
 </xml>
Index: binary-improvements2/CommandExtensions/src/ChatHelpers.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/ChatHelpers.cs	(revision 401)
+++ binary-improvements2/CommandExtensions/src/ChatHelpers.cs	(revision 402)
@@ -10,8 +10,9 @@
 			}
 
-			_receiver.SendPackage (NetPackageManager.GetPackage<NetPackageChat> ().Setup (EChatType.Whisper, -1, _message, senderName + " (PM)", false, null));
+			_receiver.SendPackage (NetPackageManager.GetPackage<NetPackageChat> ().Setup (EChatType.Whisper, -1, _message,
+				$"{senderName} (PM)", false, null));
 			string receiverName = _receiver.playerName;
-			SdtdConsole.Instance.Output (
-				$"Message to player {(receiverName != null ? "\"" + receiverName + "\"" : "unknownName")} sent with sender \"{senderName}\"");
+			receiverName = receiverName != null ? $"\"{receiverName}\"" : "unknownName";
+			SdtdConsole.Instance.Output ($"Message to player {receiverName} sent with sender \"{senderName}\"");
 		}
 	}
Index: binary-improvements2/CommandExtensions/src/Commands/Give.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/Give.cs	(revision 401)
+++ binary-improvements2/CommandExtensions/src/Commands/Give.cs	(revision 402)
@@ -27,6 +27,5 @@
 		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
 			if (_params.Count != 3 && _params.Count != 4) {
-				SdtdConsole.Instance.Output ("Wrong number of arguments, expected 3 or 4, found " + _params.Count +
-				                             ".");
+				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 3 or 4, found {_params.Count}.");
 				return;
 			}
Index: binary-improvements2/CommandExtensions/src/Commands/ListItems.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/ListItems.cs	(revision 401)
+++ binary-improvements2/CommandExtensions/src/Commands/ListItems.cs	(revision 402)
@@ -39,9 +39,9 @@
 				}
 
-				SdtdConsole.Instance.Output ("    " + s);
+				SdtdConsole.Instance.Output ($"    {s}");
 				listed++;
 			}
 
-			SdtdConsole.Instance.Output ("Listed " + listed + " matching items.");
+			SdtdConsole.Instance.Output ($"Listed {listed} matching items.");
 		}
 	}
Index: binary-improvements2/CommandExtensions/src/Commands/ListLandProtection.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/ListLandProtection.cs	(revision 401)
+++ binary-improvements2/CommandExtensions/src/Commands/ListLandProtection.cs	(revision 402)
@@ -60,5 +60,5 @@
 						userIdFilter = ci.InternalId;
 					} else {
-						SdtdConsole.Instance.Output ("Player name or entity id \"" + _params [0] + "\" not found.");
+						SdtdConsole.Instance.Output ($"Player name or entity id \"{_params [0]}\" not found.");
 						return;
 					}
@@ -82,5 +82,5 @@
 					} catch (Exception e) {
 						SdtdConsole.Instance.Output ("Error getting current player's position");
-						Log.Out ("Error in ListLandProtection.Run: " + e);
+						Log.Out ($"Error in ListLandProtection.Run: {e}");
 						return;
 					}
@@ -114,8 +114,8 @@
 				foreach (Vector3i v in claimPositions) {
 					if (parseableOutput) {
-						SdtdConsole.Instance.Output ("LandProtectionOf: id=" + claimOwner.PlatformId +
-						                             ", playerName=" + claimOwner.Name + ", location=" + v);
+						SdtdConsole.Instance.Output (
+							$"LandProtectionOf: id={claimOwner.PlatformId}, playerName={claimOwner.Name}, location={v}");
 					} else {
-						SdtdConsole.Instance.Output ("   (" + v + ")");
+						SdtdConsole.Instance.Output ($"   ({v})");
 					}
 				}
@@ -123,5 +123,5 @@
 
 			if (userIdFilter == null) {
-				SdtdConsole.Instance.Output ("Total of " + ppl.m_lpBlockMap.Count + " keystones in the game");
+				SdtdConsole.Instance.Output ($"Total of {ppl.m_lpBlockMap.Count} keystones in the game");
 			}
 		}
Index: binary-improvements2/CommandExtensions/src/Commands/RemoveLandProtection.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/RemoveLandProtection.cs	(revision 401)
+++ binary-improvements2/CommandExtensions/src/Commands/RemoveLandProtection.cs	(revision 402)
@@ -50,11 +50,9 @@
 				GameManager.Instance.SetBlocksRPC (changes);
 
-				SdtdConsole.Instance.Output ("Tried to remove #" + changes.Count +
-				                             " land protection blocks for player \"" + _id + "\". Note " +
-				                             "that only blocks in chunks that are currently loaded (close to any player) could be removed. " +
-				                             "Please check for remaining blocks by running:");
-				SdtdConsole.Instance.Output ("  listlandprotection " + _id);
+				SdtdConsole.Instance.Output (
+					$"Tried to remove #{changes.Count} land protection blocks for player \"{_id}\". Note that only blocks in chunks that are currently loaded (close to any player) could be removed. Please check for remaining blocks by running:");
+				SdtdConsole.Instance.Output ($"  listlandprotection {_id}");
 			} catch (Exception e) {
-				Log.Out ("Error in RemoveLandProtection.removeById: " + e);
+				Log.Out ($"Error in RemoveLandProtection.removeById: {e}");
 			}
 		}
@@ -87,5 +85,5 @@
 			GameManager.Instance.SetBlocksRPC (changes);
 
-			SdtdConsole.Instance.Output ("Land protection block at (" + v + ") removed");
+			SdtdConsole.Instance.Output ($"Land protection block at ({v}) removed");
 		}
 
@@ -128,9 +126,9 @@
 					} catch (Exception e) {
 						SdtdConsole.Instance.Output ("Error removing claims");
-						Log.Out ("Error in RemoveLandProtection.Run: " + e);
+						Log.Out ($"Error in RemoveLandProtection.Run: {e}");
 					}
 				} catch (Exception e) {
 					SdtdConsole.Instance.Output ("Error getting current player's position");
-					Log.Out ("Error in RemoveLandProtection.Run: " + e);
+					Log.Out ($"Error in RemoveLandProtection.Run: {e}");
 				}
 			} else if (_params.Count == 1) {
Index: binary-improvements2/CommandExtensions/src/Commands/ShowInventory.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/ShowInventory.cs	(revision 401)
+++ binary-improvements2/CommandExtensions/src/Commands/ShowInventory.cs	(revision 402)
@@ -47,5 +47,5 @@
 
 			if (tag == null) {
-				SdtdConsole.Instance.Output ("Belt of player " + p.Name + ":");
+				SdtdConsole.Instance.Output ($"Belt of player {p.Name}:");
 			}
 
@@ -56,5 +56,5 @@
 
 			if (tag == null) {
-				SdtdConsole.Instance.Output ("Bagpack of player " + p.Name + ":");
+				SdtdConsole.Instance.Output ($"Bagpack of player {p.Name}:");
 			}
 
@@ -65,5 +65,5 @@
 
 			if (tag == null) {
-				SdtdConsole.Instance.Output ("Equipment of player " + p.Name + ":");
+				SdtdConsole.Instance.Output ($"Equipment of player {p.Name}:");
 			}
 
@@ -71,6 +71,5 @@
 
 			if (tag != null) {
-				SdtdConsole.Instance.Output ("tracker_item id=" + p.EntityID + ", tag=" + tag +
-				                             ", SHOWINVENTORY DONE");
+				SdtdConsole.Instance.Output ($"tracker_item id={p.EntityID}, tag={tag}, SHOWINVENTORY DONE");
 			}
 		}
@@ -94,7 +93,6 @@
 					// Tag defined -> parseable output
 					string partsMsg = DoParts (_inv [i].parts, 1, "");
-					string msg = "tracker_item id=" + _entityId + ", tag=" + _tag + ", location=" + _location +
-					             ", slot=" + i + ", item=" + _inv [i].itemName + ", qnty=" + _inv [i].count +
-					             ", quality=" + _inv [i].quality + ", parts=(" + partsMsg + ")";
+					string msg =
+						$"tracker_item id={_entityId}, tag={_tag}, location={_location}, slot={i}, item={_inv [i].itemName}, qnty={_inv [i].count}, quality={_inv [i].quality}, parts=({partsMsg})";
 					SdtdConsole.Instance.Output (msg);
 				}
@@ -140,7 +138,6 @@
 					// Tag defined -> parseable output
 					string partsMsg = DoParts (_items [slotindices [i]].parts, 1, "");
-					string msg = "tracker_item id=" + _entityId + ", tag=" + _tag + ", location=" + _location +
-					             ", slot=" + _slotname + ", item=" + item.itemName + ", qnty=1, quality=" +
-					             item.quality + ", parts=(" + partsMsg + ")";
+					string msg =
+						$"tracker_item id={_entityId}, tag={_tag}, location={_location}, slot={_slotname}, item={item.itemName}, qnty=1, quality={item.quality}, parts=({partsMsg})";
 					SdtdConsole.Instance.Output (msg);
 				}
@@ -176,5 +173,5 @@
 					}
 
-					_currentMessage += _parts [i].itemName + "@" + _parts [i].quality;
+					_currentMessage += $"{_parts [i].itemName}@{_parts [i].quality}";
 					_currentMessage = DoParts (_parts [i].parts, _indent + 1, _currentMessage);
 				}
Index: binary-improvements2/CommandExtensions/src/Commands/TestLogSpam.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/TestLogSpam.cs	(revision 402)
+++ binary-improvements2/CommandExtensions/src/Commands/TestLogSpam.cs	(revision 402)
@@ -0,0 +1,75 @@
+using System.Collections;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using UnityEngine;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class TestLogSpam : ConsoleCmdAbstract {
+		public override string[] GetCommands () {
+			return new[] { "tls" };
+		}
+
+		public override bool AllowedInMainMenu => true;
+
+		public override bool IsExecuteOnClient => true;
+
+		public override string GetDescription () {
+			return "Spams the log with until stopped";
+		}
+
+		public override string GetHelp () {
+			return @"
+			|Usage:
+			|  1. tls <N> ['second']
+			|  2. tls stop
+			|1. Start spamming with N messages per frame - or per second if the second argument is the word 'second'
+			|2. Stop spamming
+			".Unindent ();
+		}
+
+		private Coroutine spamCoroutine;
+		private WaitForSeconds waitObj;
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count != 1 && _params.Count != 2) {
+				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 1 or 2, found {_params.Count}.");
+				return;
+			}
+
+			if (_params [0].EqualsCaseInsensitive ("stop")) {
+				if (spamCoroutine == null) {
+					SdtdConsole.Instance.Output ("Not spamming.");
+					return;
+				}
+
+				ThreadManager.StopCoroutine (spamCoroutine);
+				spamCoroutine = null;
+				SdtdConsole.Instance.Output ("Spam stopped.");
+				return;
+			}
+
+			if (!int.TryParse (_params [0], out int count)) {
+				SdtdConsole.Instance.Output ("The given spam number is not a valid integer");
+				return;
+			}
+
+			bool perSecond = _params.Count > 1 && _params [1] == "second";
+
+			waitObj = perSecond ? new WaitForSeconds (1f) : null;
+
+			SdtdConsole.Instance.Output ($"Started spamming {count} messages per {(perSecond ? "second" : "frame")}");
+			spamCoroutine = ThreadManager.StartCoroutine (SpamCo (count));
+		}
+
+		private IEnumerator SpamCo (int _count) {
+			do {
+				for (int i = 0; i < _count; i++) {
+					Log.Out ("This is a spam log message.");
+				}
+
+				yield return waitObj;
+			} while (spamCoroutine != null);
+		}
+	}
+}
Index: binary-improvements2/MapRendering/MapRendering.csproj
===================================================================
--- binary-improvements2/MapRendering/MapRendering.csproj	(revision 401)
+++ binary-improvements2/MapRendering/MapRendering.csproj	(revision 402)
@@ -83,4 +83,8 @@
       <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>
@@ -88,4 +92,5 @@
     <Compile Include="src\MapRenderBlockBuffer.cs" />
     <Compile Include="src\MapRenderer.cs" />
+    <Compile Include="src\MapTileCache.cs" />
     <Compile Include="src\ModApi.cs" />
     <Compile Include="src\AssemblyInfo.cs" />
@@ -95,14 +100,14 @@
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <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>
-  </ItemGroup>
-  <ItemGroup>
     <None Include="ModInfo.xml">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
   </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\WebServer\WebServer.csproj">
+      <Project>{01b5f981-b9fd-4364-8f9e-9127130d2542}</Project>
+      <Name>WebServer</Name>
+      <Private>False</Private>
+    </ProjectReference>
+  </ItemGroup>
 </Project>
Index: binary-improvements2/MapRendering/ModInfo.xml
===================================================================
--- binary-improvements2/MapRendering/ModInfo.xml	(revision 401)
+++ binary-improvements2/MapRendering/ModInfo.xml	(revision 402)
@@ -1,10 +1,9 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <xml>
-	<ModInfo>
-		<Name value="TFP_MapRendering" />
-		<Description value="Render the game map to image map tiles as it is uncovered" />
-		<Author value="The Fun Pimps LLC" />
-		<Version value="1" />
-		<Website value="" />
-	</ModInfo>
+	<Name value="TFP_MapRendering" />
+	<DisplayName value="Map Renderer" />
+	<Description value="Render the game map to image map tiles as it is uncovered" />
+	<Author value="The Fun Pimps LLC" />
+	<Version value="21.0" />
+	<Website value="" />
 </xml>
Index: binary-improvements2/MapRendering/src/Commands/EnableRendering.cs
===================================================================
--- binary-improvements2/MapRendering/src/Commands/EnableRendering.cs	(revision 401)
+++ binary-improvements2/MapRendering/src/Commands/EnableRendering.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: " + MapRenderer.renderingEnabled);
+				SdtdConsole.Instance.Output ($"Current state: {MapRenderer.renderingEnabled}");
 				return;
 			}
 
 			MapRenderer.renderingEnabled = _params [0].Equals ("1");
-			SdtdConsole.Instance.Output ("Set live map rendering to " + _params [0].Equals ("1"));
+			SdtdConsole.Instance.Output ($"Set live map rendering to {_params [0].Equals ("1")}");
 		}
 	}
Index: binary-improvements2/MapRendering/src/MapRenderBlockBuffer.cs
===================================================================
--- binary-improvements2/MapRendering/src/MapRenderBlockBuffer.cs	(revision 401)
+++ binary-improvements2/MapRendering/src/MapRenderBlockBuffer.cs	(revision 402)
@@ -1,5 +1,4 @@
 using System;
 using System.IO;
-using AllocsFixes.FileCache;
 using Unity.Collections;
 using UnityEngine;
@@ -21,5 +20,5 @@
 			zoomLevel = _level;
 			cache = _cache;
-			folderBase = Constants.MapDirectory + "/" + zoomLevel + "/";
+			folderBase = $"{Constants.MapDirectory}/{zoomLevel}/";
 
 			{
@@ -52,5 +51,5 @@
 				saveTextureToFile ();
 			} catch (Exception e) {
-				Log.Warning ("Exception in MapRenderBlockBuffer.SaveBlock(): " + e);
+				Log.Warning ($"Exception in MapRenderBlockBuffer.SaveBlock(): {e}");
 			}
 			Profiler.EndSample ();
@@ -64,5 +63,5 @@
 					string folder;
 					if (currentBlockMapPos.x != _block.x) {
-						folder = folderBase + _block.x + '/';
+						folder = $"{folderBase}{_block.x}/";
 
 						Profiler.BeginSample ("LoadBlock.Directory");
@@ -73,5 +72,5 @@
 					}
 
-					string fileName = folder + _block.y + ".png";
+					string fileName = $"{folder}{_block.y}.png";
 					Profiler.EndSample ();
 					
@@ -105,5 +104,5 @@
 		public Color32[] GetHalfScaled () {
 			Profiler.BeginSample ("HalfScaled.ResizeBuffer");
-			zoomBuffer.Resize (Constants.MapBlockSize, Constants.MapBlockSize);
+			zoomBuffer.Reinitialize (Constants.MapBlockSize, Constants.MapBlockSize);
 			Profiler.EndSample ();
 
@@ -156,5 +155,5 @@
 			Profiler.BeginSample ("HalfScaledNative.ResizeBuffer");
 			if (zoomBuffer.format != blockMap.format || zoomBuffer.height != Constants.MapBlockSize / 2 || zoomBuffer.width != Constants.MapBlockSize / 2) {
-				zoomBuffer.Resize (Constants.MapBlockSize / 2, Constants.MapBlockSize / 2, blockMap.format, false);
+				zoomBuffer.Reinitialize (Constants.MapBlockSize / 2, Constants.MapBlockSize / 2, blockMap.format, false);
 			}
 			Profiler.EndSample ();
@@ -206,10 +205,10 @@
 
 			if (array != null) {
-				Log.Error ("Map image tile " + _fileName + " has been corrupted, recreating tile");
+				Log.Error ($"Map image tile {_fileName} has been corrupted, recreating tile");
 			}
 
 			if (blockMap.format != Constants.DefaultTextureFormat || blockMap.height != Constants.MapBlockSize ||
 			    blockMap.width != Constants.MapBlockSize) {
-				blockMap.Resize (Constants.MapBlockSize, Constants.MapBlockSize, Constants.DefaultTextureFormat,
+				blockMap.Reinitialize (Constants.MapBlockSize, Constants.MapBlockSize, Constants.DefaultTextureFormat,
 					false);
 			}
Index: binary-improvements2/MapRendering/src/MapRenderer.cs
===================================================================
--- binary-improvements2/MapRendering/src/MapRenderer.cs	(revision 401)
+++ binary-improvements2/MapRendering/src/MapRenderer.cs	(revision 402)
@@ -5,8 +5,8 @@
 using System.Text;
 using System.Threading;
-using AllocsFixes.FileCache;
-using AllocsFixes.JSON;
 using UnityEngine;
 using UnityEngine.Profiling;
+using Utf8Json;
+using Webserver.FileCache;
 using Object = UnityEngine.Object;
 
@@ -27,10 +27,8 @@
 
 		private MapRenderer () {
-			Constants.MapDirectory = GameIO.GetSaveGameDir () + "/map";
-
-			lock (lockObject) {
-				if (!LoadMapInfo ()) {
-					WriteMapInfo ();
-				}
+			Constants.MapDirectory = $"{GameIO.GetSaveGameDir ()}/map";
+
+			if (!LoadMapInfo ()) {
+				WriteMapInfo ();
 			}
 
@@ -47,5 +45,5 @@
 		public static MapRenderer Instance => instance ??= new MapRenderer ();
 
-		public static MapTileCache GetTileCache () {
+		public static AbstractCache GetTileCache () {
 			return Instance.cache;
 		}
@@ -95,5 +93,5 @@
 						}
 					} catch (Exception e) {
-						Log.Out ("Exception in MapRendering.RenderSingleChunk(): " + e);
+						Log.Out ($"Exception in MapRendering.RenderSingleChunk(): {e}");
 					}
 				}, _chunk);
@@ -161,5 +159,5 @@
 							}
 						} catch (Exception e) {
-							Log.Out ("Exception: " + e);
+							Log.Out ($"Exception: {e}");
 						}
 					}
@@ -177,5 +175,5 @@
 			if (fullMapTexture != null) {
 				byte[] array = fullMapTexture.EncodeToPNG ();
-				File.WriteAllBytes (Constants.MapDirectory + "/map.png", array);
+				File.WriteAllBytes ($"{Constants.MapDirectory}/map.png", array);
 				Object.Destroy (fullMapTexture);
 			}
@@ -183,6 +181,6 @@
 			renderingFullMap = false;
 
-			Log.Out ("Generating map took: " + microStopwatch.ElapsedMilliseconds + " ms");
-			Log.Out ("World extent: " + minPos + " - " + maxPos);
+			Log.Out ($"Generating map took: {microStopwatch.ElapsedMilliseconds} ms");
+			Log.Out ($"World extent: {minPos} - {maxPos}");
 		}
 
@@ -307,35 +305,42 @@
 
 		private void WriteMapInfo () {
-			JsonObject mapInfo = new JsonObject ();
-			mapInfo.Add ("blockSize", new JsonNumber (Constants.MapBlockSize));
-			mapInfo.Add ("maxZoom", new JsonNumber (Constants.Zoomlevels - 1));
+			JsonWriter writer = new JsonWriter ();
+			writer.WriteBeginObject ();
+			
+			writer.WriteString ("blockSize");
+			writer.WriteNameSeparator ();
+			writer.WriteInt32 (Constants.MapBlockSize);
+			
+			writer.WriteValueSeparator ();
+			writer.WriteString ("maxZoom");
+			writer.WriteNameSeparator ();
+			writer.WriteInt32 (Constants.Zoomlevels - 1);
+			
+			writer.WriteEndObject ();
 
 			Directory.CreateDirectory (Constants.MapDirectory);
-			File.WriteAllText (Constants.MapDirectory + "/mapinfo.json", mapInfo.ToString (), Encoding.UTF8);
+			File.WriteAllBytes ($"{Constants.MapDirectory}/mapinfo.json", writer.ToUtf8ByteArray ());
 		}
 
 		private bool LoadMapInfo () {
-			if (!File.Exists (Constants.MapDirectory + "/mapinfo.json")) {
+			if (!File.Exists ($"{Constants.MapDirectory}/mapinfo.json")) {
 				return false;
 			}
 
-			string json = File.ReadAllText (Constants.MapDirectory + "/mapinfo.json", Encoding.UTF8);
+			string json = File.ReadAllText ($"{Constants.MapDirectory}/mapinfo.json", Encoding.UTF8);
 			try {
-				JsonNode node = Parser.Parse (json);
-				if (node is JsonObject jo) {
-					if (jo.ContainsKey ("blockSize")) {
-						Constants.MapBlockSize = ((JsonNumber) jo ["blockSize"]).GetInt ();
-					}
-
-					if (jo.ContainsKey ("maxZoom")) {
-						Constants.Zoomlevels = ((JsonNumber) jo ["maxZoom"]).GetInt () + 1;
-					}
-
-					return true;
-				}
-			} catch (MalformedJsonException e) {
-				Log.Out ("Exception in LoadMapInfo: " + e);
-			} catch (InvalidCastException e) {
-				Log.Out ("Exception in LoadMapInfo: " + e);
+				IDictionary<string,object> inputJson = JsonSerializer.Deserialize<IDictionary<string, object>> (json);
+
+				if (inputJson.TryGetValue ("blockSize", out object fieldNode) && fieldNode is double value) {
+					Constants.MapBlockSize = (int)value;
+				}
+
+				if (inputJson.TryGetValue ("maxZoom", out fieldNode) && fieldNode is double value2) {
+					Constants.Zoomlevels = (int)value2 + 1;
+				}
+
+				return true;
+			} catch (Exception e) {
+				Log.Out ($"Exception in LoadMapInfo: {e}");
 			}
 
Index: binary-improvements2/MapRendering/src/MapTileCache.cs
===================================================================
--- binary-improvements2/MapRendering/src/MapTileCache.cs	(revision 402)
+++ binary-improvements2/MapRendering/src/MapTileCache.cs	(revision 402)
@@ -0,0 +1,149 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using UnityEngine;
+using UnityEngine.Profiling;
+using Webserver.FileCache;
+using Object = UnityEngine.Object;
+
+namespace MapRendering {
+	// Special "cache" for map tile folder as both map rendering and webserver access files in there.
+	// Only map rendering tiles are cached. Writing is done by WriteThrough.
+	public class MapTileCache : AbstractCache {
+		private readonly byte[] transparentTile;
+		private CurrentZoomFile[] cache;
+
+		public MapTileCache (int _tileSize) {
+			Texture2D tex = new Texture2D (_tileSize, _tileSize);
+			Color nullColor = new Color (0, 0, 0, 0);
+			for (int x = 0; x < _tileSize; x++) {
+				for (int y = 0; y < _tileSize; y++) {
+					tex.SetPixel (x, y, nullColor);
+				}
+			}
+
+			transparentTile = tex.EncodeToPNG ();
+			Object.Destroy (tex);
+		}
+
+		// SetZoomCount only called before processing happens in MapRenderer.ctor, no locking required
+		[SuppressMessage ("ReSharper", "InconsistentlySynchronizedField")]
+		public void SetZoomCount (int _count) {
+			cache = new CurrentZoomFile[_count];
+			for (int i = 0; i < cache.Length; i++) {
+				cache [i] = new CurrentZoomFile ();
+			}
+		}
+
+		public byte[] LoadTile (int _zoomlevel, string _filename) {
+			try {
+				lock (cache) {
+					CurrentZoomFile cacheEntry = cache [_zoomlevel];
+
+					if (cacheEntry.filename != null && cacheEntry.filename.Equals (_filename)) {
+						return cacheEntry.pngData;
+					}
+
+					cacheEntry.filename = _filename;
+
+					if (!File.Exists (_filename)) {
+						cacheEntry.pngData = null;
+						return null;
+					}
+
+					Profiler.BeginSample ("ReadPng");
+					cacheEntry.pngData = ReadAllBytes (_filename);
+					Profiler.EndSample ();
+
+					return cacheEntry.pngData;
+				}
+			} catch (Exception e) {
+				Log.Warning ($"Error in MapTileCache.LoadTile: {e}");
+			}
+
+			return null;
+		}
+
+		public void SaveTile (int _zoomlevel, byte[] _contentPng) {
+			try {
+				lock (cache) {
+					CurrentZoomFile cacheEntry = cache [_zoomlevel];
+
+					string file = cacheEntry.filename;
+					if (string.IsNullOrEmpty (file)) {
+						return;
+					}
+					
+					cacheEntry.pngData = _contentPng;
+
+					Profiler.BeginSample ("WritePng");
+					using (Stream stream = new FileStream (file, FileMode.Create, FileAccess.ReadWrite, FileShare.None,
+						4096)) {
+						stream.Write (_contentPng, 0, _contentPng.Length);
+					}
+					Profiler.EndSample ();
+				}
+			} catch (Exception e) {
+				Log.Warning ($"Error in MapTileCache.SaveTile: {e}");
+			}
+		}
+
+		public void ResetTile (int _zoomlevel) {
+			try {
+				lock (cache) {
+					cache [_zoomlevel].filename = null;
+					cache [_zoomlevel].pngData = null;
+				}
+			} catch (Exception e) {
+				Log.Warning ($"Error in MapTileCache.ResetTile: {e}");
+			}
+		}
+
+		public override byte[] GetFileContent (string _filename) {
+			try {
+				lock (cache) {
+					foreach (CurrentZoomFile czf in cache) {
+						if (czf.filename != null && czf.filename.Equals (_filename)) {
+							return czf.pngData;
+						}
+					}
+
+					return !File.Exists (_filename) ? transparentTile : ReadAllBytes (_filename);
+				}
+			} catch (Exception e) {
+				Log.Warning ($"Error in MapTileCache.GetFileContent: {e}");
+			}
+
+			return null;
+		}
+
+		public override (int, int) Invalidate () {
+			return (0, 0);
+		}
+
+		private static byte[] ReadAllBytes (string _path) {
+			using FileStream fileStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096);
+			
+			int bytesRead = 0;
+			int bytesLeft = (int) fileStream.Length;
+			byte[] result = new byte[bytesLeft];
+			while (bytesLeft > 0) {
+				int readThisTime = fileStream.Read (result, bytesRead, bytesLeft);
+				if (readThisTime == 0) {
+					throw new IOException ("Unexpected end of stream");
+				}
+
+				bytesRead += readThisTime;
+				bytesLeft -= readThisTime;
+			}
+
+			return result;
+		}
+
+
+		private class CurrentZoomFile {
+			public string filename;
+			public byte[] pngData;
+		}
+	}
+}
Index: binary-improvements2/MapRendering/src/ModApi.cs
===================================================================
--- binary-improvements2/MapRendering/src/ModApi.cs	(revision 401)
+++ binary-improvements2/MapRendering/src/ModApi.cs	(revision 402)
@@ -1,3 +1,5 @@
 using JetBrains.Annotations;
+using Webserver;
+using Webserver.UrlHandlers;
 
 namespace MapRendering {
@@ -7,4 +9,13 @@
 			ModEvents.GameShutdown.RegisterHandler (GameShutdown);
 			ModEvents.CalcChunkColorsDone.RegisterHandler (CalcChunkColorsDone);
+
+			Web.ServerInitialized += _web => {
+				_web.RegisterPathHandler ("/map/", new StaticHandler (
+					$"{GameIO.GetSaveGameDir ()}/map",
+					MapRenderer.GetTileCache (),
+					false,
+					"web.map")
+				);
+			};
 		}
 
Index: binary-improvements2/MarkersMod/MarkersMod.csproj
===================================================================
--- binary-improvements2/MarkersMod/MarkersMod.csproj	(revision 401)
+++ binary-improvements2/MarkersMod/MarkersMod.csproj	(revision 402)
@@ -59,4 +59,12 @@
       <Private>False</Private>
     </Reference>
+    <Reference Include="UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>..\7dtd-binaries\UnityEngine.CoreModule.dll</HintPath>
+      <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>
@@ -72,9 +80,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="..\WebServer\WebServer.csproj">
       <Project>{01b5f981-b9fd-4364-8f9e-9127130d2542}</Project>
Index: binary-improvements2/MarkersMod/ModInfo.xml
===================================================================
--- binary-improvements2/MarkersMod/ModInfo.xml	(revision 401)
+++ binary-improvements2/MarkersMod/ModInfo.xml	(revision 402)
@@ -1,10 +1,9 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <xml>
-	<ModInfo>
-		<Name value="Markers" />
-		<Description value="Allows placing custom markers on the web map" />
-		<Author value="Catalysm and Alloc" />
-		<Version value="1" />
-		<Website value="" />
-	</ModInfo>
+	<Name value="TFP_MarkersExample" />
+	<DisplayName value="Markers (Example Web Mod)" />
+	<Description value="Allows placing custom markers on the web map" />
+	<Author value="Catalysm and Alloc" />
+	<Version value="21.0" />
+	<Website value="" />
 </xml>
Index: binary-improvements2/MarkersMod/src/Markers.cs
===================================================================
--- binary-improvements2/MarkersMod/src/Markers.cs	(revision 401)
+++ binary-improvements2/MarkersMod/src/Markers.cs	(revision 402)
@@ -1,9 +1,11 @@
 using System.Collections.Generic;
 using System.Net;
-using AllocsFixes.JSON;
+using JetBrains.Annotations;
+using Utf8Json;
 using Webserver;
 using Webserver.WebAPI;
 
 namespace Examples {
+	[UsedImplicitly]
 	public class Markers : AbsRestApi {
 		private const int numRandomMarkers = 5;
@@ -11,6 +13,4 @@
 		private readonly Dictionary<string, (int, int)> markers = new Dictionary<string, (int, int)> ();
 
-		private static readonly JsonArray emptyResult = new JsonArray ();
-		
 		public Markers () {
 			GameRandom random = GameRandomManager.Instance.CreateGameRandom ();
@@ -24,51 +24,66 @@
 		}
 
+		private static readonly byte[] jsonKeyId = JsonWriter.GetEncodedPropertyNameWithBeginObject ("id");
+		private static readonly byte[] jsonKeyLat = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("lat");
+		private static readonly byte[] jsonKeyLng = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("lng");
+
 		protected override void HandleRestGet (RequestContext _context) {
 			string id = _context.RequestPath;
 			
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
 			if (string.IsNullOrEmpty (id)) {
-				JsonArray result = new JsonArray ();
+				writer.WriteBeginArray ();
 
+				bool first = true;
 				foreach ((string markerId, (int, int) coordinates) in markers) {
-					JsonObject marker = new JsonObject ();
-					marker.Add ("id", new JsonString (markerId));
-					marker.Add ("lat", new JsonNumber (coordinates.Item1));
-					marker.Add ("lng", new JsonNumber (coordinates.Item2));
-					result.Add (marker);
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+					
+					first = false;
+					
+					writeMarkerJson (ref writer, markerId, coordinates);
 				}
-				
-				SendEnvelopedResult (_context, result);
+
+				writer.WriteEndArray ();
+				SendEnvelopedResult (_context, ref writer);
 				return;
 			}
 
 			if (!markers.TryGetValue (id, out (int, int) location)) {
-				SendEnvelopedResult (_context, emptyResult, HttpStatusCode.NotFound);
+				writer.WriteRaw (JsonEmptyData);
+				SendEnvelopedResult (_context, ref writer, HttpStatusCode.NotFound);
 				return;
 			}
 
 			{
-				JsonArray result = new JsonArray ();
-				JsonObject marker = new JsonObject ();
-				marker.Add ("id", new JsonString (id));
-				marker.Add ("lat", new JsonNumber (location.Item1));
-				marker.Add ("lng", new JsonNumber (location.Item2));
-				result.Add (marker);
-				SendEnvelopedResult (_context, result);
+				writer.WriteBeginArray ();
+				
+				writeMarkerJson (ref writer, id, location);
+				
+				writer.WriteEndArray ();
+				SendEnvelopedResult (_context, ref writer);
 			}
 		}
 
-		protected override void HandleRestPost (RequestContext _context, JsonNode _jsonBody) {
-			if (!(_jsonBody is JsonObject bodyObject)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "BODY_NOT_OBJECT");
+		private void writeMarkerJson (ref JsonWriter _writer, string _markerId, (int, int) _coordinates) {
+			_writer.WriteRaw (jsonKeyId);
+			_writer.WriteString (_markerId);
+			_writer.WriteRaw (jsonKeyLat);
+			_writer.WriteInt32 (_coordinates.Item1);
+			_writer.WriteRaw (jsonKeyLng);
+			_writer.WriteInt32 (_coordinates.Item2);
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			if (!TryGetJsonField (_jsonInput, "lat", out int lat)) {
+				SendErrorResult (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_LAT");
 				return;
 			}
 
-			if (!TryGetJsonField (bodyObject, "lat", out int lat)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "NO_OR_INVALID_LAT");
-				return;
-			}
-
-			if (!TryGetJsonField (bodyObject, "lng", out int lng)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "NO_OR_INVALID_LNG");
+			if (!TryGetJsonField (_jsonInput, "lng", out int lng)) {
+				SendErrorResult (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_LNG");
 				return;
 			}
@@ -77,21 +92,17 @@
 			markers.Add (newId, (lat, lng));
 
-			JsonString result = new JsonString (newId);
-			SendEnvelopedResult (_context, result, HttpStatusCode.Created);
+			PrepareEnvelopedResult (out JsonWriter writer);
+			writer.WriteString (newId);
+			SendEnvelopedResult (_context, ref writer, HttpStatusCode.Created);
 		}
 
-		protected override void HandleRestPut (RequestContext _context, JsonNode _jsonBody) {
-			if (!(_jsonBody is JsonObject bodyObject)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "BODY_NOT_OBJECT");
+		protected override void HandleRestPut (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			if (!TryGetJsonField (_jsonInput, "lat", out int lat)) {
+				SendErrorResult (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_LAT");
 				return;
 			}
 
-			if (!TryGetJsonField (bodyObject, "lat", out int lat)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "NO_OR_INVALID_LAT");
-				return;
-			}
-
-			if (!TryGetJsonField (bodyObject, "lng", out int lng)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "NO_OR_INVALID_LNG");
+			if (!TryGetJsonField (_jsonInput, "lng", out int lng)) {
+				SendErrorResult (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_LNG");
 				return;
 			}
@@ -100,5 +111,5 @@
 
 			if (!markers.TryGetValue (id, out _)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.NotFound, _jsonBody, "ID_NOT_FOUND");
+				SendErrorResult (_context, HttpStatusCode.NotFound, _jsonInputData, "ID_NOT_FOUND");
 				return;
 			}
@@ -106,9 +117,13 @@
 			markers [id] = (lat, lng);
 
-			JsonObject result = new JsonObject ();
-			result.Add ("id", new JsonString (id));
-			result.Add ("lat", new JsonNumber (lat));
-			result.Add ("lng", new JsonNumber (lng));
-			SendEnvelopedResult (_context, result);
+			PrepareEnvelopedResult (out JsonWriter writer);
+			writer.WriteRaw (jsonKeyId);
+			writer.WriteString (id);
+			writer.WriteRaw (jsonKeyLat);
+			writer.WriteInt32 (lat);
+			writer.WriteRaw (jsonKeyLng);
+			writer.WriteInt32 (lng);
+			writer.WriteEndObject ();
+			SendEnvelopedResult (_context, ref writer);
 		}
 
@@ -116,5 +131,7 @@
 			string id = _context.RequestPath;
 
-			SendEnvelopedResult (_context, null, markers.Remove (id) ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+			PrepareEnvelopedResult (out JsonWriter writer);
+			writer.WriteRaw (JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, markers.Remove (id) ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
 		}
 	}
Index: binary-improvements2/MarkersMod/src/ModApi.cs
===================================================================
--- binary-improvements2/MarkersMod/src/ModApi.cs	(revision 401)
+++ binary-improvements2/MarkersMod/src/ModApi.cs	(revision 402)
@@ -1,3 +1,6 @@
+using JetBrains.Annotations;
+
 namespace Examples {
+	[UsedImplicitly]
 	public class ModApi : IModApi {
 		public void InitMod (Mod _modInstance) {
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);
+		}
 	}
 }
