Index: binary-improvements2/7dtd-server-fixes/7dtd-server-fixes.csproj
===================================================================
--- binary-improvements2/7dtd-server-fixes/7dtd-server-fixes.csproj	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/7dtd-server-fixes.csproj	(revision 391)
@@ -91,13 +91,12 @@
     <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\BlockingQueue.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\JsonNull.cs" />
     <Compile Include="src\JSON\MalformedJSONException.cs" />
     <Compile Include="src\FileCache\AbstractCache.cs" />
@@ -105,9 +104,9 @@
     <Compile Include="src\FileCache\SimpleCache.cs" />
     <Compile Include="src\FileCache\MapTileCache.cs" />
-    <Compile Include="src\API.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\JSON\JsonValue.cs" />
     <Compile Include="src\LiveData\EntityFilterList.cs" />
   </ItemGroup>
@@ -116,5 +115,4 @@
     <Folder Include="src\" />
     <Folder Include="src\PersistentData\" />
-    <Folder Include="src\JSON\Parser\" />
     <Folder Include="src\FileCache\" />
   </ItemGroup>
Index: binary-improvements2/7dtd-server-fixes/ModInfo.xml
===================================================================
--- binary-improvements2/7dtd-server-fixes/ModInfo.xml	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/ModInfo.xml	(revision 391)
@@ -2,9 +2,9 @@
 <xml>
 	<ModInfo>
-		<Name value="Allocs server fixes" />
+		<Name value="Server extensions" />
 		<Description value="Common functions" />
-		<Author value="Christian 'Alloc' Illy" />
-		<Version value="24" />
-		<Website value="http://7dtd.illy.bz" />
+		<Author value="The Fun Pimps LLC" />
+		<Version value="1" />
+		<Website value="" />
 	</ModInfo>
 </xml>
Index: binary-improvements2/7dtd-server-fixes/src/API.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/API.cs	(revision 390)
+++ 	(revision )
@@ -1,79 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.PersistentData;
-using Platform.Steam;
-
-namespace AllocsFixes {
-	public class API : IModApi {
-		public void InitMod (Mod _modInstance) {
-			ModEvents.GameStartDone.RegisterHandler (GameAwake);
-			ModEvents.GameShutdown.RegisterHandler (GameShutdown);
-			ModEvents.SavePlayerData.RegisterHandler (SavePlayerData);
-			ModEvents.PlayerSpawning.RegisterHandler (PlayerSpawning);
-			ModEvents.PlayerDisconnected.RegisterHandler (PlayerDisconnected);
-			ModEvents.PlayerSpawnedInWorld.RegisterHandler (PlayerSpawned);
-			ModEvents.ChatMessage.RegisterHandler (ChatMessage);
-		}
-
-		public void GameAwake () {
-			PersistentContainer.Load ();
-		}
-
-		public void GameShutdown () {
-		}
-
-		public void SavePlayerData (ClientInfo _cInfo, PlayerDataFile _playerDataFile) {
-			PersistentContainer.Instance.Players [_cInfo.InternalId, true].Update (_playerDataFile);
-		}
-
-		public void PlayerSpawning (ClientInfo _cInfo, int _chunkViewDim, PlayerProfile _playerProfile) {
-			string owner = null;
-			if (_cInfo.PlatformId is UserIdentifierSteam identifierSteam) {
-				owner = identifierSteam.OwnerId.ToString ();
-			}
-
-			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
-			);
-		}
-
-		public void PlayerDisconnected (ClientInfo _cInfo, bool _bShutdown) {
-			Player p = PersistentContainer.Instance.Players [_cInfo.InternalId, false];
-			if (p != null) {
-				p.SetOffline ();
-			} else {
-				Log.Out ("Disconnected player not found in client list...");
-			}
-
-			PersistentContainer.Instance.Save ();
-		}
-
-		public void PlayerSpawned (ClientInfo _cInfo, RespawnType _respawnReason, Vector3i _spawnPos) {
-			PersistentContainer.Instance.Players [_cInfo.InternalId, true].SetOnline (_cInfo);
-			PersistentContainer.Instance.Save ();
-		}
-
-		private const string ANSWER =
-			"     [ff0000]I[-] [ff7f00]W[-][ffff00]A[-][80ff00]S[-] [00ffff]H[-][0080ff]E[-][0000ff]R[-][8b00ff]E[-]";
-
-		public bool ChatMessage (ClientInfo _cInfo, EChatType _type, int _senderId, string _msg, string _mainName,
-			bool _localizeMain, List<int> _recipientEntityIds) {
-			if (string.IsNullOrEmpty (_msg) || !_msg.EqualsCaseInsensitive ("/alloc")) {
-				return true;
-			}
-
-			if (_cInfo != null) {
-				Log.Out ("Sent chat hook reply to {0}", _cInfo.InternalId);
-				_cInfo.SendPackage (NetPackageManager.GetPackage<NetPackageChat> ().Setup (EChatType.Whisper, -1, ANSWER, "", false, null));
-			} else {
-				Log.Error ("ChatHookExample: Argument _cInfo null on message: {0}", _msg);
-			}
-
-			return false;
-		}
-	}
-}
Index: binary-improvements2/7dtd-server-fixes/src/AllocsUtils.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/AllocsUtils.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/AllocsUtils.cs	(revision 391)
@@ -4,6 +4,5 @@
 	public static class AllocsUtils {
 		public static string ColorToHex (Color _color) {
-			return string.Format ("{0:X02}{1:X02}{2:X02}", (int) (_color.r * 255), (int) (_color.g * 255),
-				(int) (_color.b * 255));
+			return $"{(int)(_color.r * 255):X02}{(int)(_color.g * 255):X02}{(int)(_color.b * 255):X02}";
 		}
 	}
Index: binary-improvements2/7dtd-server-fixes/src/AssemblyInfo.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/AssemblyInfo.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/AssemblyInfo.cs	(revision 391)
@@ -7,7 +7,7 @@
 [assembly: AssemblyDescription ("")]
 [assembly: AssemblyConfiguration ("")]
-[assembly: AssemblyCompany ("")]
+[assembly: AssemblyCompany ("The Fun Pimps LLC")]
 [assembly: AssemblyProduct ("")]
-[assembly: AssemblyCopyright ("Alloc")]
+[assembly: AssemblyCopyright ("The Fun Pimps LLC")]
 [assembly: AssemblyTrademark ("")]
 [assembly: AssemblyCulture ("")]
Index: binary-improvements2/7dtd-server-fixes/src/BlockingQueue.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/BlockingQueue.cs	(revision 390)
+++ 	(revision )
@@ -1,37 +1,0 @@
-using System.Collections.Generic;
-using System.Threading;
-
-namespace AllocsFixes {
-	public class BlockingQueue<T> {
-		private readonly Queue<T> queue = new Queue<T> ();
-		private bool closing;
-
-		public void Enqueue (T _item) {
-			lock (queue) {
-				queue.Enqueue (_item);
-				Monitor.PulseAll (queue);
-			}
-		}
-
-		public T Dequeue () {
-			lock (queue) {
-				while (queue.Count == 0) {
-					if (closing) {
-						return default (T);
-					}
-
-					Monitor.Wait (queue);
-				}
-
-				return queue.Dequeue ();
-			}
-		}
-
-		public void Close () {
-			lock (queue) {
-				closing = true;
-				Monitor.PulseAll (queue);
-			}
-		}
-	}
-}
Index: binary-improvements2/7dtd-server-fixes/src/FileCache/DirectAccess.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/FileCache/DirectAccess.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/FileCache/DirectAccess.cs	(revision 391)
@@ -7,9 +7,5 @@
 		public override byte[] GetFileContent (string _filename) {
 			try {
-				if (!File.Exists (_filename)) {
-					return null;
-				}
-
-				return File.ReadAllBytes (_filename);
+				return File.Exists (_filename) ? File.ReadAllBytes (_filename) : null;
 			} catch (Exception e) {
 				Log.Out ("Error in DirectAccess.GetFileContent: " + e);
Index: binary-improvements2/7dtd-server-fixes/src/FileCache/MapTileCache.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/FileCache/MapTileCache.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/FileCache/MapTileCache.cs	(revision 391)
@@ -36,17 +36,19 @@
 				lock (cache) {
 					CurrentZoomFile cacheEntry = cache [_zoomlevel];
-					
-					if (cacheEntry.filename == null || !cacheEntry.filename.Equals (_filename)) {
-						cacheEntry.filename = _filename;
 
-						if (!File.Exists (_filename)) {
-							cacheEntry.pngData = null;
-							return null;
-						}
+					if (cacheEntry.filename != null && cacheEntry.filename.Equals (_filename)) {
+						return cacheEntry.pngData;
+					}
 
-						Profiler.BeginSample ("ReadPng");
-						cacheEntry.pngData = ReadAllBytes (_filename);
-						Profiler.EndSample ();
+					cacheEntry.filename = _filename;
+
+					if (!File.Exists (_filename)) {
+						cacheEntry.pngData = null;
+						return null;
 					}
+
+					Profiler.BeginSample ("ReadPng");
+					cacheEntry.pngData = ReadAllBytes (_filename);
+					Profiler.EndSample ();
 
 					return cacheEntry.pngData;
@@ -103,9 +105,5 @@
 					}
 
-					if (!File.Exists (_filename)) {
-						return transparentTile;
-					}
-
-					return ReadAllBytes (_filename);
+					return !File.Exists (_filename) ? transparentTile : ReadAllBytes (_filename);
 				}
 			} catch (Exception e) {
Index: binary-improvements2/7dtd-server-fixes/src/FileCache/SimpleCache.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/FileCache/SimpleCache.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/FileCache/SimpleCache.cs	(revision 391)
@@ -11,11 +11,13 @@
 			try {
 				lock (fileCache) {
-					if (!fileCache.ContainsKey (_filename)) {
-						if (!File.Exists (_filename)) {
-							return null;
-						}
+					if (fileCache.ContainsKey (_filename)) {
+						return fileCache [_filename];
+					}
 
-						fileCache.Add (_filename, File.ReadAllBytes (_filename));
+					if (!File.Exists (_filename)) {
+						return null;
 					}
+
+					fileCache.Add (_filename, File.ReadAllBytes (_filename));
 
 					return fileCache [_filename];
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JSONArray.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JSONArray.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JSONArray.cs	(revision 391)
@@ -3,17 +3,15 @@
 
 namespace AllocsFixes.JSON {
-	public class JSONArray : JSONNode {
-		private readonly List<JSONNode> nodes = new List<JSONNode> ();
+	public class JsonArray : JsonNode {
+		private readonly List<JsonNode> nodes = new List<JsonNode> ();
 
-		public JSONNode this [int _index] {
-			get { return nodes [_index]; }
-			set { nodes [_index] = value; }
+		public JsonNode this [int _index] {
+			get => nodes [_index];
+			set => nodes [_index] = value;
 		}
 
-		public int Count {
-			get { return nodes.Count; }
-		}
+		public int Count => nodes.Count;
 
-		public void Add (JSONNode _node) {
+		public void Add (JsonNode _node) {
 			nodes.Add (_node);
 		}
@@ -25,5 +23,5 @@
 			}
 
-			foreach (JSONNode n in nodes) {
+			foreach (JsonNode n in nodes) {
 				if (_prettyPrint) {
 					_stringBuilder.Append (new string ('\t', _currentLevel + 1));
@@ -48,7 +46,7 @@
 		}
 
-		public static JSONArray Parse (string _json, ref int _offset) {
+		public static JsonArray Parse (string _json, ref int _offset) {
 			//Log.Out ("ParseArray enter (" + offset + ")");
-			JSONArray arr = new JSONArray ();
+			JsonArray arr = new JsonArray ();
 
 			bool nextElemAllowed = true;
@@ -63,5 +61,5 @@
 							_offset++;
 						} else {
-							throw new MalformedJSONException (
+							throw new MalformedJsonException (
 								"Could not parse array, found a comma without a value first");
 						}
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JSONBoolean.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JSONBoolean.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JSONBoolean.cs	(revision 391)
@@ -2,8 +2,8 @@
 
 namespace AllocsFixes.JSON {
-	public class JSONBoolean : JSONValue {
+	public class JsonBoolean : JsonValue {
 		private readonly bool value;
 
-		public JSONBoolean (bool _value) {
+		public JsonBoolean (bool _value) {
 			value = _value;
 		}
@@ -17,5 +17,5 @@
 		}
 
-		public static JSONBoolean Parse (string _json, ref int _offset) {
+		public static JsonBoolean Parse (string _json, ref int _offset) {
 			//Log.Out ("ParseBool enter (" + offset + ")");
 
@@ -23,5 +23,5 @@
 				//Log.Out ("JSON:Parsed Bool: true");
 				_offset += 4;
-				return new JSONBoolean (true);
+				return new JsonBoolean (true);
 			}
 
@@ -29,8 +29,8 @@
 				//Log.Out ("JSON:Parsed Bool: false");
 				_offset += 5;
-				return new JSONBoolean (false);
+				return new JsonBoolean (false);
 			}
 
-			throw new MalformedJSONException ("No valid boolean found");
+			throw new MalformedJsonException ("No valid boolean found");
 		}
 
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JSONNode.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JSONNode.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JSONNode.cs	(revision 391)
@@ -2,5 +2,5 @@
 
 namespace AllocsFixes.JSON {
-	public abstract class JSONNode {
+	public abstract class JsonNode {
 		public abstract void ToString (StringBuilder _stringBuilder, bool _prettyPrint = false, int _currentLevel = 0);
 
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JSONNull.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JSONNull.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JSONNull.cs	(revision 391)
@@ -3,19 +3,19 @@
 
 namespace AllocsFixes.JSON {
-	public class JSONNull : JSONValue {
+	public class JsonNull : JsonValue {
 		public override void ToString (StringBuilder _stringBuilder, bool _prettyPrint = false, int _currentLevel = 0) {
 			_stringBuilder.Append ("null");
 		}
 
-		public static JSONNull Parse (string _json, ref int _offset) {
+		public static JsonNull Parse (string _json, ref int _offset) {
 			//Log.Out ("ParseNull enter (" + offset + ")");
 
 			if (!_json.Substring (_offset, 4).Equals ("null")) {
-				throw new MalformedJSONException ("No valid null value found");
+				throw new MalformedJsonException ("No valid null value found");
 			}
 
 			//Log.Out ("JSON:Parsed Null");
 			_offset += 4;
-			return new JSONNull ();
+			return new JsonNull ();
 		}
 
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JSONNumber.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JSONNumber.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JSONNumber.cs	(revision 391)
@@ -3,8 +3,8 @@
 
 namespace AllocsFixes.JSON {
-	public class JSONNumber : JSONValue {
+	public class JsonNumber : JsonValue {
 		private readonly double value;
 
-		public JSONNumber (double _value) {
+		public JsonNumber (double _value) {
 			value = _value;
 		}
@@ -22,5 +22,5 @@
 		}
 
-		public static JSONNumber Parse (string _json, ref int _offset) {
+		public static JsonNumber Parse (string _json, ref int _offset) {
 			//Log.Out ("ParseNumber enter (" + offset + ")");
 			StringBuilder sbNum = new StringBuilder ();
@@ -37,13 +37,13 @@
 				} else if (_json [_offset] == '.') {
 					if (hasExp) {
-						throw new MalformedJSONException ("Decimal separator in exponent");
+						throw new MalformedJsonException ("Decimal separator in exponent");
 					}
 
 					if (hasDec) {
-						throw new MalformedJSONException ("Multiple decimal separators in number found");
+						throw new MalformedJsonException ("Multiple decimal separators in number found");
 					}
 
 					if (sbNum.Length == 0) {
-						throw new MalformedJSONException ("No leading digits before decimal separator found");
+						throw new MalformedJsonException ("No leading digits before decimal separator found");
 					}
 
@@ -53,5 +53,5 @@
 					if (hasExp) {
 						if (sbExp.Length > 0) {
-							throw new MalformedJSONException ("Negative sign in exponent after digits");
+							throw new MalformedJsonException ("Negative sign in exponent after digits");
 						}
 
@@ -59,5 +59,5 @@
 					} else {
 						if (sbNum.Length > 0) {
-							throw new MalformedJSONException ("Negative sign in mantissa after digits");
+							throw new MalformedJsonException ("Negative sign in mantissa after digits");
 						}
 
@@ -66,9 +66,9 @@
 				} else if (_json [_offset] == 'e' || _json [_offset] == 'E') {
 					if (hasExp) {
-						throw new MalformedJSONException ("Multiple exponential markers in number found");
+						throw new MalformedJsonException ("Multiple exponential markers in number found");
 					}
 
 					if (sbNum.Length == 0) {
-						throw new MalformedJSONException ("No leading digits before exponential marker found");
+						throw new MalformedJsonException ("No leading digits before exponential marker found");
 					}
 
@@ -78,28 +78,26 @@
 					if (hasExp) {
 						if (sbExp.Length > 0) {
-							throw new MalformedJSONException ("Positive sign in exponent after digits");
+							throw new MalformedJsonException ("Positive sign in exponent after digits");
 						}
 
 						sbExp.Append (_json [_offset]);
 					} else {
-						throw new MalformedJSONException ("Positive sign in mantissa found");
+						throw new MalformedJsonException ("Positive sign in mantissa found");
 					}
 				} else {
-					double number;
-					if (!StringParsers.TryParseDouble (sbNum.ToString (), out number)) {
-						throw new MalformedJSONException ("Mantissa is not a valid decimal (\"" + sbNum + "\")");
+					if (!StringParsers.TryParseDouble (sbNum.ToString (), out double number)) {
+						throw new MalformedJsonException ("Mantissa is not a valid decimal (\"" + sbNum + "\")");
 					}
 
 					if (hasExp) {
-						int exp;
-						if (!int.TryParse (sbExp.ToString (), out exp)) {
-							throw new MalformedJSONException ("Exponent is not a valid integer (\"" + sbExp + "\")");
+						if (!int.TryParse (sbExp.ToString (), out int exp)) {
+							throw new MalformedJsonException ("Exponent is not a valid integer (\"" + sbExp + "\")");
 						}
 
-						number = number * Math.Pow (10, exp);
+						number *= Math.Pow (10, exp);
 					}
 
 					//Log.Out ("JSON:Parsed Number: " + number.ToString ());
-					return new JSONNumber (number);
+					return new JsonNumber (number);
 				}
 
@@ -107,5 +105,5 @@
 			}
 
-			throw new MalformedJSONException ("End of JSON reached before parsing number finished");
+			throw new MalformedJsonException ("End of JSON reached before parsing number finished");
 		}
 
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JSONObject.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JSONObject.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JSONObject.cs	(revision 391)
@@ -3,8 +3,8 @@
 
 namespace AllocsFixes.JSON {
-	public class JSONObject : JSONNode {
-		private readonly Dictionary<string, JSONNode> nodes = new Dictionary<string, JSONNode> ();
+	public class JsonObject : JsonNode {
+		private readonly Dictionary<string, JsonNode> nodes = new Dictionary<string, JsonNode> ();
 
-		public JSONNode this [string _name] {
+		public JsonNode this [string _name] {
 			get => nodes [_name];
 			set => nodes [_name] = value;
@@ -19,9 +19,9 @@
 		}
 
-		public bool TryGetValue (string _name, out JSONNode _node) {
+		public bool TryGetValue (string _name, out JsonNode _node) {
 			return nodes.TryGetValue (_name, out _node);
 		}
 
-		public void Add (string _name, JSONNode _node) {
+		public void Add (string _name, JsonNode _node) {
 			nodes.Add (_name, _node);
 		}
@@ -33,15 +33,15 @@
 			}
 
-			foreach (KeyValuePair<string, JSONNode> kvp in nodes) {
+			foreach ((string key, JsonNode value) in nodes) {
 				if (_prettyPrint) {
 					_stringBuilder.Append (new string ('\t', _currentLevel + 1));
 				}
 
-				_stringBuilder.Append (string.Format ("\"{0}\":", kvp.Key));
+				_stringBuilder.Append ($"\"{key}\":");
 				if (_prettyPrint) {
 					_stringBuilder.Append (" ");
 				}
 
-				kvp.Value.ToString (_stringBuilder, _prettyPrint, _currentLevel + 1);
+				value.ToString (_stringBuilder, _prettyPrint, _currentLevel + 1);
 				_stringBuilder.Append (",");
 				if (_prettyPrint) {
@@ -61,7 +61,7 @@
 		}
 
-		public static JSONObject Parse (string _json, ref int _offset) {
+		public static JsonObject Parse (string _json, ref int _offset) {
 			//Log.Out ("ParseObject enter (" + offset + ")");
-			JSONObject obj = new JSONObject ();
+			JsonObject obj = new JsonObject ();
 
 			bool nextElemAllowed = true;
@@ -72,17 +72,17 @@
 					case '"':
 						if (nextElemAllowed) {
-							JSONString key = JSONString.Parse (_json, ref _offset);
+							JsonString key = JsonString.Parse (_json, ref _offset);
 							Parser.SkipWhitespace (_json, ref _offset);
 							if (_json [_offset] != ':') {
-								throw new MalformedJSONException (
+								throw new MalformedJsonException (
 									"Could not parse object, missing colon (\":\") after key");
 							}
 
 							_offset++;
-							JSONNode val = Parser.ParseInternal (_json, ref _offset);
+							JsonNode val = Parser.ParseInternal (_json, ref _offset);
 							obj.Add (key.GetString (), val);
 							nextElemAllowed = false;
 						} else {
-							throw new MalformedJSONException (
+							throw new MalformedJsonException (
 								"Could not parse object, found new key without a separating comma");
 						}
@@ -94,5 +94,5 @@
 							_offset++;
 						} else {
-							throw new MalformedJSONException (
+							throw new MalformedJsonException (
 								"Could not parse object, found a comma without a key/value pair first");
 						}
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JSONString.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JSONString.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JSONString.cs	(revision 391)
@@ -2,8 +2,8 @@
 
 namespace AllocsFixes.JSON {
-	public class JSONString : JSONValue {
+	public class JsonString : JsonValue {
 		private readonly string value;
 
-		public JSONString (string _value) {
+		public JsonString (string _value) {
 			value = _value;
 		}
@@ -14,5 +14,5 @@
 
 		public override void ToString (StringBuilder _stringBuilder, bool _prettyPrint = false, int _currentLevel = 0) {
-			if (value == null || value.Length == 0) {
+			if (string.IsNullOrEmpty (value)) {
 				_stringBuilder.Append ("\"\"");
 				return;
@@ -64,5 +64,5 @@
 		}
 
-		public static JSONString Parse (string _json, ref int _offset) {
+		public static JsonString Parse (string _json, ref int _offset) {
 			//Log.Out ("ParseString enter (" + offset + ")");
 			StringBuilder sb = new StringBuilder ();
@@ -104,5 +104,5 @@
 
 						//Log.Out ("JSON:Parsed String: " + sb.ToString ());
-						return new JSONString (sb.ToString ());
+						return new JsonString (sb.ToString ());
 					default:
 						sb.Append (_json [_offset]);
@@ -112,5 +112,5 @@
 			}
 
-			throw new MalformedJSONException ("End of JSON reached before parsing string finished");
+			throw new MalformedJsonException ("End of JSON reached before parsing string finished");
 		}
 
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JSONValue.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JSONValue.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JSONValue.cs	(revision 391)
@@ -1,4 +1,4 @@
 ﻿namespace AllocsFixes.JSON {
-	public abstract class JSONValue : JSONNode {
+	public abstract class JsonValue : JsonNode {
 		public abstract string AsString { get; }
 		public abstract int AsInt { get; }
Index: binary-improvements2/7dtd-server-fixes/src/JSON/JsonManualBuilder.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/JsonManualBuilder.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/JsonManualBuilder.cs	(revision 391)
@@ -9,5 +9,5 @@
 			NonEmpty = 1,
 			Object = 2,
-			Array = 4,
+			Array = 4
 		}
 
@@ -20,20 +20,12 @@
 		private int currentLevelNumber;
 
-		private ELevelInfo CurrentLevelInfo {
-			get { return (ELevelInfo) (currentLevelType & levelBitsMask); }
-		}
-
-		private bool CurrentLevelIsNonEmpty {
-			get { return (CurrentLevelInfo & ELevelInfo.NonEmpty) == ELevelInfo.NonEmpty; }
-		}
-
-		private bool CurrentLevelIsArray {
-			get { return (CurrentLevelInfo & ELevelInfo.Array) != ELevelInfo.Array; }
-		}
-
-		private bool CurrentLevelIsObject {
-			get { return (CurrentLevelInfo & ELevelInfo.Object) != ELevelInfo.Object; }
-		}
-		
+		private ELevelInfo CurrentLevelInfo => (ELevelInfo) (currentLevelType & levelBitsMask);
+
+		private bool CurrentLevelIsNonEmpty => (CurrentLevelInfo & ELevelInfo.NonEmpty) == ELevelInfo.NonEmpty;
+
+		private bool CurrentLevelIsArray => (CurrentLevelInfo & ELevelInfo.Array) != ELevelInfo.Array;
+
+		private bool CurrentLevelIsObject => (CurrentLevelInfo & ELevelInfo.Object) != ELevelInfo.Object;
+
 		public JsonManualBuilder (bool _prettyPrint) {
 			prettyPrint = _prettyPrint;
@@ -54,5 +46,5 @@
 			}
 
-			currentLevelType = currentLevelType | (long) ELevelInfo.NonEmpty;
+			currentLevelType |= (long) ELevelInfo.NonEmpty;
 		}
 
@@ -210,5 +202,5 @@
 
 			currentLevelNumber--;
-			currentLevelType = currentLevelType >> levelTypeBits;
+			currentLevelType >>= levelTypeBits;
 
 			if (prettyPrint) {
Index: binary-improvements2/7dtd-server-fixes/src/JSON/MalformedJSONException.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/MalformedJSONException.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/MalformedJSONException.cs	(revision 391)
@@ -3,15 +3,15 @@
 
 namespace AllocsFixes.JSON {
-	public class MalformedJSONException : ApplicationException {
-		public MalformedJSONException () {
+	public class MalformedJsonException : ApplicationException {
+		public MalformedJsonException () {
 		}
 
-		public MalformedJSONException (string _message) : base (_message) {
+		public MalformedJsonException (string _message) : base (_message) {
 		}
 
-		public MalformedJSONException (string _message, Exception _inner) : base (_message, _inner) {
+		public MalformedJsonException (string _message, Exception _inner) : base (_message, _inner) {
 		}
 
-		protected MalformedJSONException (SerializationInfo _info, StreamingContext _context) : base (_info, _context) {
+		protected MalformedJsonException (SerializationInfo _info, StreamingContext _context) : base (_info, _context) {
 		}
 	}
Index: binary-improvements2/7dtd-server-fixes/src/JSON/Parser.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/JSON/Parser.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/JSON/Parser.cs	(revision 391)
@@ -1,10 +1,10 @@
 namespace AllocsFixes.JSON {
-	public class Parser {
-		public static JSONNode Parse (string _json) {
+	public static class Parser {
+		public static JsonNode Parse (string _json) {
 			int offset = 0;
 			return ParseInternal (_json, ref offset);
 		}
 
-		public static JSONNode ParseInternal (string _json, ref int _offset) {
+		public static JsonNode ParseInternal (string _json, ref int _offset) {
 			SkipWhitespace (_json, ref _offset);
 
@@ -12,16 +12,16 @@
 			switch (_json [_offset]) {
 				case '[':
-					return JSONArray.Parse (_json, ref _offset);
+					return JsonArray.Parse (_json, ref _offset);
 				case '{':
-					return JSONObject.Parse (_json, ref _offset);
+					return JsonObject.Parse (_json, ref _offset);
 				case '"':
-					return JSONString.Parse (_json, ref _offset);
+					return JsonString.Parse (_json, ref _offset);
 				case 't':
 				case 'f':
-					return JSONBoolean.Parse (_json, ref _offset);
+					return JsonBoolean.Parse (_json, ref _offset);
 				case 'n':
-					return JSONNull.Parse (_json, ref _offset);
+					return JsonNull.Parse (_json, ref _offset);
 				default:
-					return JSONNumber.Parse (_json, ref _offset);
+					return JsonNumber.Parse (_json, ref _offset);
 			}
 		}
@@ -42,5 +42,5 @@
 			}
 
-			throw new MalformedJSONException ("End of JSON reached before parsing finished");
+			throw new MalformedJsonException ("End of JSON reached before parsing finished");
 		}
 	}
Index: binary-improvements2/7dtd-server-fixes/src/LandClaimList.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/LandClaimList.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/LandClaimList.cs	(revision 391)
@@ -20,45 +20,50 @@
 			Dictionary<PersistentPlayerData, List<Vector3i>> owners =
 				new Dictionary<PersistentPlayerData, List<Vector3i>> ();
-			foreach (KeyValuePair<Vector3i, PersistentPlayerData> kvp in d) {
+			foreach ((Vector3i claimPos, PersistentPlayerData owner) in d) {
 				bool allowed = true;
 				if (_positionFilters != null) {
 					foreach (PositionFilter pf in _positionFilters) {
-						if (!pf (kvp.Key)) {
-							allowed = false;
-							break;
+						if (pf (claimPos)) {
+							continue;
 						}
+
+						allowed = false;
+						break;
 					}
 				}
 
-				if (allowed) {
-					if (!owners.ContainsKey (kvp.Value)) {
-						owners.Add (kvp.Value, new List<Vector3i> ());
-					}
+				if (!allowed) {
+					continue;
+				}
 
-					owners [kvp.Value].Add (kvp.Key);
+				if (!owners.ContainsKey (owner)) {
+					owners.Add (owner, new List<Vector3i> ());
 				}
+
+				owners [owner].Add (claimPos);
 			}
 
-			foreach (KeyValuePair<PersistentPlayerData, List<Vector3i>> kvp in owners) {
-				Player p = PersistentContainer.Instance.Players [kvp.Key.UserIdentifier, false];
-				if (p == null) {
-					p = new Player (kvp.Key.UserIdentifier);
-				}
+			foreach ((PersistentPlayerData owner, List<Vector3i> claimPositions) in owners) {
+				Player p = PersistentContainer.Instance.Players [owner.UserIdentifier, false] ?? new Player (owner.UserIdentifier);
 
 				bool allowed = true;
 				if (_ownerFilters != null) {
 					foreach (OwnerFilter of in _ownerFilters) {
-						if (!of (p)) {
-							allowed = false;
-							break;
+						if (of (p)) {
+							continue;
 						}
+
+						allowed = false;
+						break;
 					}
 				}
 
-				if (allowed) {
-					result.Add (p, new List<Vector3i> ());
-					foreach (Vector3i v in kvp.Value) {
-						result [p].Add (v);
-					}
+				if (!allowed) {
+					continue;
+				}
+
+				result.Add (p, new List<Vector3i> ());
+				foreach (Vector3i v in claimPositions) {
+					result [p].Add (v);
 				}
 			}
Index: binary-improvements2/7dtd-server-fixes/src/ModApi.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/ModApi.cs	(revision 391)
+++ binary-improvements2/7dtd-server-fixes/src/ModApi.cs	(revision 391)
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using AllocsFixes.PersistentData;
+using JetBrains.Annotations;
+using Platform.Steam;
+
+namespace AllocsFixes {
+	[UsedImplicitly]
+	public class ModApi : IModApi {
+		public void InitMod (Mod _modInstance) {
+			ModEvents.GameStartDone.RegisterHandler (GameAwake);
+			ModEvents.SavePlayerData.RegisterHandler (SavePlayerData);
+			ModEvents.PlayerSpawning.RegisterHandler (PlayerSpawning);
+			ModEvents.PlayerDisconnected.RegisterHandler (PlayerDisconnected);
+			ModEvents.PlayerSpawnedInWorld.RegisterHandler (PlayerSpawned);
+			ModEvents.ChatMessage.RegisterHandler (ChatMessage);
+		}
+
+		private void GameAwake () {
+			PersistentContainer.Load ();
+		}
+
+		private void SavePlayerData (ClientInfo _cInfo, PlayerDataFile _playerDataFile) {
+			PersistentContainer.Instance.Players [_cInfo.InternalId, true].Update (_playerDataFile);
+		}
+
+		private void PlayerSpawning (ClientInfo _cInfo, int _chunkViewDim, PlayerProfile _playerProfile) {
+			string owner = null;
+			if (_cInfo.PlatformId is UserIdentifierSteam identifierSteam) {
+				owner = identifierSteam.OwnerId.ToString ();
+			}
+
+			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
+			);
+		}
+
+		private void PlayerDisconnected (ClientInfo _cInfo, bool _bShutdown) {
+			Player p = PersistentContainer.Instance.Players [_cInfo.InternalId, false];
+			if (p != null) {
+				p.SetOffline ();
+			} else {
+				Log.Out ("Disconnected player not found in client list...");
+			}
+
+			PersistentContainer.Instance.Save ();
+		}
+
+		private void PlayerSpawned (ClientInfo _cInfo, RespawnType _respawnReason, Vector3i _spawnPos) {
+			PersistentContainer.Instance.Players [_cInfo.InternalId, true].SetOnline (_cInfo);
+			PersistentContainer.Instance.Save ();
+		}
+
+		private const string testChatAnswer =
+			"     [ff0000]I[-] [ff7f00]W[-][ffff00]A[-][80ff00]S[-] [00ffff]H[-][0080ff]E[-][0000ff]R[-][8b00ff]E[-]";
+
+		private bool ChatMessage (ClientInfo _cInfo, EChatType _type, int _senderId, string _msg, string _mainName,
+			bool _localizeMain, List<int> _recipientEntityIds) {
+			if (string.IsNullOrEmpty (_msg) || !_msg.EqualsCaseInsensitive ("/alloc")) {
+				return true;
+			}
+
+			if (_cInfo != null) {
+				Log.Out ("Sent chat hook reply to {0}", _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);
+			}
+
+			return false;
+		}
+	}
+}
Index: binary-improvements2/7dtd-server-fixes/src/PersistentData/Attributes.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/PersistentData/Attributes.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/PersistentData/Attributes.cs	(revision 391)
@@ -8,17 +8,11 @@
 
 		public bool HideChatCommands {
-			get { return hideChatCommands; }
-			set { hideChatCommands = value; }
+			get => hideChatCommands;
+			set => hideChatCommands = value;
 		}
 
 		public string HideChatCommandPrefix {
-			get {
-				if (hideChatCommandPrefix == null) {
-					hideChatCommandPrefix = "";
-				}
-
-				return hideChatCommandPrefix;
-			}
-			set { hideChatCommandPrefix = value; }
+			get => hideChatCommandPrefix ?? (hideChatCommandPrefix = "");
+			set => hideChatCommandPrefix = value;
 		}
 	}
Index: binary-improvements2/7dtd-server-fixes/src/PersistentData/PersistentContainer.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/PersistentData/PersistentContainer.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/PersistentData/PersistentContainer.cs	(revision 391)
@@ -10,35 +10,11 @@
 		[OptionalField] private Attributes attributes;
 
-		public Players Players {
-			get {
-				if (players == null) {
-					players = new Players ();
-				}
+		public Players Players => players ?? (players = new Players ());
 
-				return players;
-			}
-		}
-
-		public Attributes Attributes {
-			get {
-				if (attributes == null) {
-					attributes = new Attributes ();
-				}
-
-				return attributes;
-			}
-		}
+		public Attributes Attributes => attributes ?? (attributes = new Attributes ());
 
 		private static PersistentContainer instance;
 
-		public static PersistentContainer Instance {
-			get {
-				if (instance == null) {
-					instance = new PersistentContainer ();
-				}
-
-				return instance;
-			}
-		}
+		public static PersistentContainer Instance => instance ?? (instance = new PersistentContainer ());
 
 		private PersistentContainer () {
@@ -58,8 +34,7 @@
 
 			try {
-				PersistentContainer obj;
 				Stream stream = File.Open (GameIO.GetSaveGameDir () + "/AllocsPeristentData.bin", FileMode.Open);
 				BinaryFormatter bFormatter = new BinaryFormatter ();
-				obj = (PersistentContainer) bFormatter.Deserialize (stream);
+				PersistentContainer obj = (PersistentContainer) bFormatter.Deserialize (stream);
 				stream.Close ();
 				instance = obj;
Index: binary-improvements2/7dtd-server-fixes/src/PersistentData/Players.cs
===================================================================
--- binary-improvements2/7dtd-server-fixes/src/PersistentData/Players.cs	(revision 390)
+++ binary-improvements2/7dtd-server-fixes/src/PersistentData/Players.cs	(revision 391)
@@ -41,19 +41,19 @@
 
 			if (int.TryParse (_nameOrId, out int entityId)) {
-				foreach (KeyValuePair<PlatformUserIdentifierAbs, Player> kvp in Dict) {
-					if (kvp.Value.IsOnline && kvp.Value.EntityID == entityId) {
-						return kvp.Key;
+				foreach ((PlatformUserIdentifierAbs iUserId, Player player) in Dict) {
+					if (player.IsOnline && player.EntityID == entityId) {
+						return iUserId;
 					}
 				}
 			}
 
-			foreach (KeyValuePair<PlatformUserIdentifierAbs, Player> kvp in Dict) {
-				string name = kvp.Value.Name;
+			foreach ((PlatformUserIdentifierAbs iUserId, Player player) in Dict) {
+				string name = player.Name;
 				if (_ignoreColorCodes) {
 					name = Regex.Replace (name, "\\[[0-9a-fA-F]{6}\\]", "");
 				}
 
-				if (kvp.Value.IsOnline && name.EqualsCaseInsensitive (_nameOrId)) {
-					return kvp.Key;
+				if (player.IsOnline && name.EqualsCaseInsensitive (_nameOrId)) {
+					return iUserId;
 				}
 			}
Index: binary-improvements2/CommandExtensions/CommandExtensions.csproj
===================================================================
--- binary-improvements2/CommandExtensions/CommandExtensions.csproj	(revision 391)
+++ binary-improvements2/CommandExtensions/CommandExtensions.csproj	(revision 391)
@@ -0,0 +1,90 @@
+﻿<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{E273D042-57F9-4E2E-8268-5053527E5287}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>CommandExtensions</RootNamespace>
+    <AssemblyName>CommandExtensions</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\bin\Mods\TFP_CommandExtensions\</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <NoStdLib>true</NoStdLib>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_Profiler|AnyCPU' ">
+    <OutputPath>..\bin\Mods\TFP_CommandExtensions\</OutputPath>
+    <DefineConstants>ENABLE_PROFILER</DefineConstants>
+    <Optimize>true</Optimize>
+    <WarningLevel>4</WarningLevel>
+    <NoStdLib>true</NoStdLib>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <OutputPath>..\bin\Mods\TFP_CommandExtensions\</OutputPath>
+    <DebugType>full</DebugType>
+    <DebugSymbols>true</DebugSymbols>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="Assembly-CSharp">
+      <HintPath>..\7dtd-binaries\Assembly-CSharp.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="UnityEngine">
+      <HintPath>..\7dtd-binaries\UnityEngine.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="LogLibrary">
+      <HintPath>..\7dtd-binaries\LogLibrary.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="mscorlib">
+      <HintPath>..\7dtd-binaries\mscorlib.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="Assembly-CSharp-firstpass">
+      <HintPath>..\7dtd-binaries\Assembly-CSharp-firstpass.dll</HintPath>
+      <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>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="src\ModApi.cs" />
+    <Compile Include="src\AssemblyInfo.cs" />
+    <Compile Include="src\ChatHelpers.cs" />
+    <Compile Include="src\Commands\Exception.cs" />
+    <Compile Include="src\Commands\Give.cs" />
+    <Compile Include="src\Commands\ListItems.cs" />
+    <Compile Include="src\Commands\ListKnownPlayers.cs" />
+    <Compile Include="src\Commands\ListLandProtection.cs" />
+    <Compile Include="src\Commands\RemoveLandProtection.cs" />
+    <Compile Include="src\Commands\Reply.cs" />
+    <Compile Include="src\Commands\SayToPlayer.cs" />
+    <Compile Include="src\Commands\ShowInventory.cs" />
+    <Compile Include="src\PrivateMessageConnections.cs" />
+  </ItemGroup>
+  <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>
+</Project>
Index: binary-improvements2/CommandExtensions/ModInfo.xml
===================================================================
--- binary-improvements2/CommandExtensions/ModInfo.xml	(revision 391)
+++ binary-improvements2/CommandExtensions/ModInfo.xml	(revision 391)
@@ -0,0 +1,10 @@
+<?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>
+</xml>
Index: binary-improvements2/CommandExtensions/src/AssemblyInfo.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/AssemblyInfo.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/AssemblyInfo.cs	(revision 391)
@@ -0,0 +1,25 @@
+using System.Reflection;
+
+// Information about this assembly is defined by the following attributes. 
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle ("CommandExtensions")]
+[assembly: AssemblyDescription ("")]
+[assembly: AssemblyConfiguration ("")]
+[assembly: AssemblyCompany ("The Fun Pimps LLC")]
+[assembly: AssemblyProduct ("")]
+[assembly: AssemblyCopyright ("The Fun Pimps LLC")]
+[assembly: AssemblyTrademark ("")]
+[assembly: AssemblyCulture ("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion ("0.0.0.0")]
+
+// The following attributes are used to specify the signing key for the assembly, 
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
Index: binary-improvements2/CommandExtensions/src/ChatHelpers.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/ChatHelpers.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/ChatHelpers.cs	(revision 391)
@@ -0,0 +1,18 @@
+namespace CommandExtensions {
+	public static class ChatHelpers {
+		public static void SendMessage (ClientInfo _receiver, ClientInfo _sender, string _message) {
+			string senderName;
+			if (_sender != null) {
+				PrivateMessageConnections.SetLastPMSender (_sender, _receiver);
+				senderName = _sender.playerName;
+			} else {
+				senderName = "Server";
+			}
+
+			_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}\"");
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/Commands/Exception.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/Exception.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/Commands/Exception.cs	(revision 391)
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class ConsoleCmdException : ConsoleCmdAbstract {
+		public override string[] GetCommands () {
+			return new[] { "exception" };
+		}
+
+		public override bool AllowedInMainMenu => true;
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			Log.Out ("Test info");
+			Log.Warning ("Test warning");
+			Log.Error ("Test error");
+			throw new Exception ("Test exception");
+		}
+
+		public override string GetDescription () {
+			return "Throw an exception / log messages";
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/Commands/Give.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/Give.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/Commands/Give.cs	(revision 391)
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using UnityEngine;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class Give : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "give an item to a player (entity id or name)";
+		}
+
+		public override string GetHelp () {
+			return "Give an item to a player by dropping it in front of that player\n" +
+			       "Usage:\n" +
+			       "   give <name / entity id> <item name> <amount>\n" +
+			       "   give <name / entity id> <item name> <amount> <quality>\n" +
+			       "Either pass the full name of a player or his entity id (given by e.g. \"lpi\").\n" +
+			       "Item name has to be the exact name of an item as listed by \"listitems\".\n" +
+			       "Amount is the number of instances of this item to drop (as a single stack).\n" +
+			       "Quality is the quality of the dropped items for items that have a quality.";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"give", string.Empty};
+		}
+
+		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 +
+				                             ".");
+				return;
+			}
+
+			ClientInfo ci = ConsoleHelper.ParseParamIdOrName (_params [0]);
+
+			if (ci == null) {
+				SdtdConsole.Instance.Output ("Playername or entity id not found.");
+				return;
+			}
+
+			ItemValue iv = ItemClass.GetItem (_params [1], true);
+			if (iv.type == ItemValue.None.type) {
+				SdtdConsole.Instance.Output ("Item not found.");
+				return;
+			}
+
+			iv = new ItemValue (iv.type, true);
+
+			if (!int.TryParse (_params [2], out int n) || n <= 0) {
+				SdtdConsole.Instance.Output ("Amount is not an integer or not greater than zero.");
+				return;
+			}
+
+			int quality = Constants.cItemMaxQuality;
+
+			if (_params.Count == 4) {
+				if (!int.TryParse (_params [3], out quality) || quality <= 0) {
+					SdtdConsole.Instance.Output ("Quality is not an integer or not greater than zero.");
+					return;
+				}
+			}
+
+			if (ItemClass.list [iv.type].HasSubItems) {
+				for (int i = 0; i < iv.Modifications.Length; i++) {
+					ItemValue tmp = iv.Modifications [i];
+					tmp.Quality = quality;
+					iv.Modifications [i] = tmp;
+				}
+			} else if (ItemClass.list [iv.type].HasQuality) {
+				iv.Quality = quality;
+			}
+
+			EntityPlayer p = GameManager.Instance.World.Players.dict [ci.entityId];
+
+			ItemStack invField = new ItemStack (iv, n);
+
+			GameManager.Instance.ItemDropServer (invField, p.GetPosition (), Vector3.zero);
+
+			SdtdConsole.Instance.Output ("Dropped item");
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/Commands/ListItems.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/ListItems.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/Commands/ListItems.cs	(revision 391)
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class ListItems : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "lists all items that contain the given substring";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"listitems", "li"};
+		}
+
+		public override string GetHelp () {
+			return "List all available item names\n" +
+			       "Usage:\n" +
+			       "   1. listitems <searchString>\n" +
+			       "   2. listitems *\n" +
+			       "1. List only names that contain the given string.\n" +
+			       "2. List all names.";
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count != 1 || _params [0].Length == 0) {
+				SdtdConsole.Instance.Output ("Usage: listitems <searchString>");
+				return;
+			}
+
+			int count = ItemClass.ItemNames.Count;
+			bool showAll = _params [0].Trim ().Equals ("*");
+
+			int listed = 0;
+			for (int i = 0; i < count; i++) {
+				string s = ItemClass.ItemNames [i];
+				if (!showAll && s.IndexOf (_params [0], StringComparison.OrdinalIgnoreCase) < 0) {
+					continue;
+				}
+
+				SdtdConsole.Instance.Output ("    " + s);
+				listed++;
+			}
+
+			SdtdConsole.Instance.Output ("Listed " + listed + " matching items.");
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/Commands/ListKnownPlayers.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/ListKnownPlayers.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/Commands/ListKnownPlayers.cs	(revision 391)
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using AllocsFixes.PersistentData;
+using JetBrains.Annotations;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class ListKnownPlayers : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "lists all players that were ever online";
+		}
+
+		public override string GetHelp () {
+			return "Usage:\n" +
+			       "  1. listknownplayers\n" +
+			       "  2. listknownplayers -online\n" +
+			       "  3. listknownplayers -notbanned\n" +
+			       "  4. listknownplayers <player name / userid>\n" +
+			       "1. Lists all players that have ever been online\n" +
+			       "2. Lists only the players that are currently online\n" +
+			       "3. Lists only the players that are not banned\n" +
+			       "4. Lists all players whose name contains the given string or matches the given UserID";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"listknownplayers", "lkp"};
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			AdminTools admTools = GameManager.Instance.adminTools;
+
+			bool onlineOnly = false;
+			bool notBannedOnly = false;
+			string nameFilter = string.Empty;
+			PlatformUserIdentifierAbs userIdFilter = null;
+
+			if (_params.Count == 1) {
+				if (_params [0].EqualsCaseInsensitive ("-online")) {
+					onlineOnly = true;
+				} else if (_params [0].EqualsCaseInsensitive ("-notbanned")) {
+					notBannedOnly = true;
+				} else if (PlatformUserIdentifierAbs.TryFromCombinedString (_params [0], out userIdFilter)) {
+					// if true nothing to do, set by the out parameter
+				} else {
+					nameFilter = _params [0];
+				}
+			}
+
+			if (userIdFilter != null) {
+				Player p = PersistentContainer.Instance.Players [userIdFilter, false];
+
+				if (p != null) {
+					SdtdConsole.Instance.Output (
+						$"{0}. {p.Name}, id={p.EntityID}, steamid={_params [0]}, online={p.IsOnline}, ip={p.IP}, playtime={p.TotalPlayTime / 60} m, seen={p.LastOnline:yyyy-MM-dd HH:mm}"
+					);
+				} else {
+					SdtdConsole.Instance.Output ($"SteamID {_params [0]} unknown!");
+				}
+			} else {
+				int num = 0;
+				foreach ((PlatformUserIdentifierAbs userId, Player player) in PersistentContainer.Instance.Players.Dict) {
+					if (
+						(!onlineOnly || player.IsOnline)
+						&& (!notBannedOnly || !admTools.IsBanned (userId, out _, out _))
+						&& (nameFilter.Length == 0 || player.Name.ContainsCaseInsensitive (nameFilter))
+					) {
+						SdtdConsole.Instance.Output (
+							$"{++num}. {player.Name}, id={player.EntityID}, steamid={userId}, online={player.IsOnline}, ip={player.IP}, playtime={player.TotalPlayTime / 60} m, seen={player.LastOnline:yyyy-MM-dd HH:mm}"
+						);
+					}
+				}
+
+				SdtdConsole.Instance.Output ($"Total of {PersistentContainer.Instance.Players.Count} known");
+			}
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/Commands/ListLandProtection.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/ListLandProtection.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/Commands/ListLandProtection.cs	(revision 391)
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using AllocsFixes;
+using AllocsFixes.PersistentData;
+using JetBrains.Annotations;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class ListLandProtection : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "lists all land protection blocks and owners";
+		}
+
+		public override string GetHelp () {
+			return "Usage:\n" +
+			       "  1. listlandprotection summary\n" +
+			       "  2. listlandprotection <user id / player name / entity id> [parseable]\n" +
+			       "  3. listlandprotection nearby [length]\n" +
+			       "1. Lists only players that own claimstones, the number they own and the protection status\n" +
+			       "2. Lists only the claims of the player given by his UserID / entity id / playername, including the individual claim positions.\n" +
+			       "   If \"parseable\" is specified the output of the individual claims will be in a format better suited for programmatical readout.\n" +
+			       "3. Lists claims in a square with edge length of 64 (or the optionally specified size) around the executing player\n";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"listlandprotection", "llp"};
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count >= 1 && _params [0].EqualsCaseInsensitive ("nearby")) {
+				if (_senderInfo.RemoteClientInfo != null) {
+					_params.Add (_senderInfo.RemoteClientInfo.entityId.ToString ());
+				} else if (_senderInfo.IsLocalGame && !GameManager.IsDedicatedServer) {
+					_params.Add (GameManager.Instance.World.GetPrimaryPlayerId ().ToString ());
+				}
+			}
+
+			World w = GameManager.Instance.World;
+			PersistentPlayerList ppl = GameManager.Instance.GetPersistentPlayerList ();
+
+			bool summaryOnly = false;
+			PlatformUserIdentifierAbs userIdFilter = null;
+			Vector3i closeTo = default;
+			bool onlyCloseToPlayer = false;
+			int closeToDistance = 32;
+			bool parseableOutput = false;
+
+			if (_params.Contains ("parseable")) {
+				parseableOutput = true;
+				_params.Remove ("parseable");
+			}
+
+			if (_params.Count == 1) {
+				if (_params [0].EqualsCaseInsensitive ("summary")) {
+					summaryOnly = true;
+				} else if (PlatformUserIdentifierAbs.TryFromCombinedString (_params[0], out userIdFilter)) {
+				} else {
+					ClientInfo ci = ConsoleHelper.ParseParamIdOrName (_params [0]);
+					if (ci != null) {
+						userIdFilter = ci.InternalId;
+					} else {
+						SdtdConsole.Instance.Output ("Player name or entity id \"" + _params [0] + "\" not found.");
+						return;
+					}
+				}
+			} else if (_params.Count >= 2) {
+				if (_params [0].EqualsCaseInsensitive ("nearby")) {
+					try {
+						if (_params.Count == 3) {
+							if (!int.TryParse (_params [1], out closeToDistance)) {
+								SdtdConsole.Instance.Output ("Given length is not an integer!");
+								return;
+							}
+
+							closeToDistance /= 2;
+						}
+
+						int entityId = int.Parse (_params [_params.Count - 1]);
+						EntityPlayer ep = w.Players.dict [entityId];
+						closeTo = new Vector3i (ep.GetPosition ());
+						onlyCloseToPlayer = true;
+					} catch (Exception e) {
+						SdtdConsole.Instance.Output ("Error getting current player's position");
+						Log.Out ("Error in ListLandProtection.Run: " + e);
+						return;
+					}
+				} else {
+					SdtdConsole.Instance.Output ("Illegal parameter list");
+					return;
+				}
+			}
+
+
+			LandClaimList.OwnerFilter[] ownerFilters = null;
+			if (userIdFilter != null) {
+				ownerFilters = new[] {LandClaimList.UserIdFilter (userIdFilter)};
+			}
+
+			LandClaimList.PositionFilter[] posFilters = null;
+			if (onlyCloseToPlayer) {
+				posFilters = new[] {LandClaimList.CloseToFilter2dRect (closeTo, closeToDistance)};
+			}
+
+			Dictionary<Player, List<Vector3i>> claims = LandClaimList.GetLandClaims (ownerFilters, posFilters);
+
+			foreach ((Player claimOwner, List<Vector3i> claimPositions) in claims) {
+				SdtdConsole.Instance.Output (
+					$"Player \"{claimOwner.Name} ({claimOwner.PlatformId})\" owns {claimPositions.Count} keystones (protected: {claimOwner.LandProtectionActive}, current hardness multiplier: {claimOwner.LandProtectionMultiplier})");
+				
+				if (summaryOnly) {
+					continue;
+				}
+
+				foreach (Vector3i v in claimPositions) {
+					if (parseableOutput) {
+						SdtdConsole.Instance.Output ("LandProtectionOf: id=" + claimOwner.PlatformId +
+						                             ", playerName=" + claimOwner.Name + ", location=" + v);
+					} else {
+						SdtdConsole.Instance.Output ("   (" + v + ")");
+					}
+				}
+			}
+
+			if (userIdFilter == null) {
+				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 391)
+++ binary-improvements2/CommandExtensions/src/Commands/RemoveLandProtection.cs	(revision 391)
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using AllocsFixes;
+using AllocsFixes.PersistentData;
+using JetBrains.Annotations;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class RemoveLandProtection : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "removes the association of a land protection block to the owner";
+		}
+
+		public override string GetHelp () {
+			return "Usage:" +
+			       "  1. removelandprotection <userid>\n" +
+			       "  2. removelandprotection <x> <y> <z>\n" +
+			       "  3. removelandprotection nearby [length]\n" +
+			       "1. Remove all land claims owned by the user with the given UserID\n" +
+			       "2. Remove only the claim block on the exactly given block position\n" +
+			       "3. Remove all claims in a square with edge length of 64 (or the optionally specified size) around the executing player";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"removelandprotection", "rlp"};
+		}
+
+		private void removeById (string _id) {
+			try {
+				if (!PlatformUserIdentifierAbs.TryFromCombinedString (_id, out PlatformUserIdentifierAbs userId)) {
+					SdtdConsole.Instance.Output (
+						"Not a valid Steam ID or user has never logged on. Use \"listlandprotection\" to get a list of keystones.");
+					return;
+				}
+				
+				PersistentPlayerList ppl = GameManager.Instance.GetPersistentPlayerList ();
+
+				if (ppl.Players [userId].LPBlocks == null || ppl.Players [userId].LPBlocks.Count == 0) {
+					SdtdConsole.Instance.Output (
+						"Player does not own any keystones. Use \"listlandprotection\" to get a list of keystones.");
+					return;
+				}
+
+				List<BlockChangeInfo> changes = new List<BlockChangeInfo> ();
+				foreach (Vector3i pos in ppl.Players [userId].LPBlocks) {
+					BlockChangeInfo bci = new BlockChangeInfo (pos, new BlockValue (0), true, false);
+					changes.Add (bci);
+				}
+
+				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);
+			} catch (Exception e) {
+				Log.Out ("Error in RemoveLandProtection.removeById: " + e);
+			}
+		}
+
+		private void removeByPosition (List<string> _coords) {
+			int.TryParse (_coords [0], out int x);
+			int.TryParse (_coords [1], out int y);
+			int.TryParse (_coords [2], out int z);
+
+			if (x == int.MinValue || y == int.MinValue || z == int.MinValue) {
+				SdtdConsole.Instance.Output ("At least one of the given coordinates is not a valid integer");
+				return;
+			}
+
+			Vector3i v = new Vector3i (x, y, z);
+
+			PersistentPlayerList ppl = GameManager.Instance.GetPersistentPlayerList ();
+
+			Dictionary<Vector3i, PersistentPlayerData> d = ppl.m_lpBlockMap;
+			if (d == null || !d.ContainsKey (v)) {
+				SdtdConsole.Instance.Output (
+					"No land protection block at the given position or not a valid position. Use \"listlandprotection\" to get a list of keystones.");
+				return;
+			}
+
+			BlockChangeInfo bci = new BlockChangeInfo (v, new BlockValue (0), true, false);
+
+			List<BlockChangeInfo> changes = new List<BlockChangeInfo> {bci};
+
+			GameManager.Instance.SetBlocksRPC (changes);
+
+			SdtdConsole.Instance.Output ("Land protection block at (" + v + ") removed");
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count > 0 && _params [0].EqualsCaseInsensitive ("nearby")) {
+				if (_senderInfo.RemoteClientInfo != null) {
+					_params.Add (_senderInfo.RemoteClientInfo.entityId.ToString ());
+				} else if (_senderInfo.IsLocalGame && !GameManager.IsDedicatedServer) {
+					_params.Add (GameManager.Instance.World.GetPrimaryPlayerId ().ToString ());
+				}
+
+				try {
+					int closeToDistance = 32;
+					if (_params.Count == 3) {
+						if (!int.TryParse (_params [1], out closeToDistance)) {
+							SdtdConsole.Instance.Output ("Given length is not an integer!");
+							return;
+						}
+
+						closeToDistance /= 2;
+					}
+
+					int entityId = int.Parse (_params [_params.Count - 1]);
+					EntityPlayer ep = GameManager.Instance.World.Players.dict [entityId];
+					Vector3i closeTo = new Vector3i (ep.GetPosition ());
+					LandClaimList.PositionFilter[] posFilters =
+						{LandClaimList.CloseToFilter2dRect (closeTo, closeToDistance)};
+					Dictionary<Player, List<Vector3i>> claims = LandClaimList.GetLandClaims (null, posFilters);
+
+					try {
+						List<BlockChangeInfo> changes = new List<BlockChangeInfo> ();
+						foreach (KeyValuePair<Player, List<Vector3i>> kvp in claims) {
+							foreach (Vector3i v in kvp.Value) {
+								BlockChangeInfo bci = new BlockChangeInfo (v, new BlockValue (0), true, false);
+								changes.Add (bci);
+							}
+						}
+
+						GameManager.Instance.SetBlocksRPC (changes);
+					} catch (Exception e) {
+						SdtdConsole.Instance.Output ("Error removing claims");
+						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);
+				}
+			} else if (_params.Count == 1) {
+				removeById (_params [0]);
+			} else if (_params.Count == 3) {
+				removeByPosition (_params);
+			} else {
+				SdtdConsole.Instance.Output ("Illegal parameters");
+			}
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/Commands/Reply.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/Reply.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/Commands/Reply.cs	(revision 391)
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class Reply : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "send a message to  the player who last sent you a PM";
+		}
+
+		public override string GetHelp () {
+			return "Usage:\n" +
+			       "   reply <message>\n" +
+			       "Send the given message to the user you last received a PM from.";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"reply", "re"};
+		}
+
+		private void RunInternal (ClientInfo _sender, List<string> _params) {
+			if (_params.Count < 1) {
+				SdtdConsole.Instance.Output ("Usage: reply <message>");
+				return;
+			}
+
+			string message = _params [0];
+
+			ClientInfo receiver = PrivateMessageConnections.GetLastPMSenderForPlayer (_sender);
+			if (receiver != null) {
+				ChatHelpers.SendMessage (receiver, _sender, message);
+			} else {
+				SdtdConsole.Instance.Output (
+					"You have not received a PM so far or sender of last received PM is no longer online.");
+			}
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_senderInfo.RemoteClientInfo == null) {
+				Log.Out ("Command \"reply\" can only be used on clients!");
+			} else {
+				RunInternal (_senderInfo.RemoteClientInfo, _params);
+			}
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/Commands/SayToPlayer.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/SayToPlayer.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/Commands/SayToPlayer.cs	(revision 391)
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class SayToPlayer : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "send a message to a single player";
+		}
+
+		public override string GetHelp () {
+			return "Usage:\n" +
+			       "   pm <player name / steam id / entity id> <message>\n" +
+			       "Send a PM to the player given by the player name or entity id (as given by e.g. \"lpi\").";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"sayplayer", "pm"};
+		}
+
+		private void RunInternal (ClientInfo _sender, List<string> _params) {
+			if (_params.Count < 2) {
+				SdtdConsole.Instance.Output ("Usage: sayplayer <playername|entityid> <message>");
+				return;
+			}
+
+			string message = _params [1];
+
+			ClientInfo receiver = ConsoleHelper.ParseParamIdOrName (_params [0]);
+			if (receiver != null) {
+				ChatHelpers.SendMessage (receiver, _sender, message);
+			} else {
+				SdtdConsole.Instance.Output ("Playername or entity ID not found.");
+			}
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			RunInternal (_senderInfo.RemoteClientInfo, _params);
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/Commands/ShowInventory.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/Commands/ShowInventory.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/Commands/ShowInventory.cs	(revision 391)
@@ -0,0 +1,186 @@
+using System.Collections.Generic;
+using AllocsFixes.PersistentData;
+using JetBrains.Annotations;
+
+namespace CommandExtensions.Commands {
+	[UsedImplicitly]
+	public class ShowInventory : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "list inventory of a given player";
+		}
+
+		public override string GetHelp () {
+			return "Usage:\n" +
+			       "   showinventory <user id / player name / entity id> [tag]\n" +
+			       "Show the inventory of the player given by his UserID, player name or\n" +
+			       "entity id (as given by e.g. \"lpi\").\n" +
+			       "Optionally specify a tag that is included in each line of the output. In\n" +
+			       "this case output is designed to be easily parseable by tools.\n" +
+			       "Note: This only shows the player's inventory after it was first sent to\n" +
+			       "the server which happens at least every 30 seconds.";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"showinventory", "si"};
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count < 1) {
+				SdtdConsole.Instance.Output ("Usage: showinventory <steamid|playername|entityid> [tag]");
+				return;
+			}
+
+			PlatformUserIdentifierAbs steamid = PersistentContainer.Instance.Players.GetSteamID (_params [0], true);
+			if (steamid == null) {
+				SdtdConsole.Instance.Output (
+					"Playername or entity/steamid id not found or no inventory saved (first saved after a player has been online for 30s).");
+				return;
+			}
+
+			string tag = null;
+			if (_params.Count > 1 && _params [1].Length > 0) {
+				tag = _params [1];
+			}
+
+			Player p = PersistentContainer.Instance.Players [steamid, false];
+			AllocsFixes.PersistentData.Inventory inv = p.Inventory;
+
+			if (tag == null) {
+				SdtdConsole.Instance.Output ("Belt of player " + p.Name + ":");
+			}
+
+			PrintInv (inv.belt, p.EntityID, "belt", tag);
+			if (tag == null) {
+				SdtdConsole.Instance.Output (string.Empty);
+			}
+
+			if (tag == null) {
+				SdtdConsole.Instance.Output ("Bagpack of player " + p.Name + ":");
+			}
+
+			PrintInv (inv.bag, p.EntityID, "backpack", tag);
+			if (tag == null) {
+				SdtdConsole.Instance.Output (string.Empty);
+			}
+
+			if (tag == null) {
+				SdtdConsole.Instance.Output ("Equipment of player " + p.Name + ":");
+			}
+
+			PrintEquipment (inv.equipment, p.EntityID, "equipment", tag);
+
+			if (tag != null) {
+				SdtdConsole.Instance.Output ("tracker_item id=" + p.EntityID + ", tag=" + tag +
+				                             ", SHOWINVENTORY DONE");
+			}
+		}
+
+		private void PrintInv (List<InvItem> _inv, int _entityId, string _location, string _tag) {
+			for (int i = 0; i < _inv.Count; i++) {
+				if (_inv [i] == null) {
+					continue;
+				}
+
+				if (_tag == null) {
+					// no Tag defined -> readable output
+					if (_inv [i].quality < 0) {
+						SdtdConsole.Instance.Output ($"    Slot {i}: {_inv [i].count:000} * {_inv [i].itemName}");
+					} else {
+						SdtdConsole.Instance.Output ($"    Slot {i}: {_inv [i].count:000} * {_inv [i].itemName} - quality: {_inv [i].quality}");
+					}
+
+					DoParts (_inv [i].parts, 1, null);
+				} else {
+					// 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 + ")";
+					SdtdConsole.Instance.Output (msg);
+				}
+			}
+		}
+
+		private void PrintEquipment (InvItem[] _equipment, int _entityId, string _location, string _tag) {
+			AddEquipment ("head", _equipment, EquipmentSlots.Headgear, _entityId, _location, _tag);
+			AddEquipment ("eyes", _equipment, EquipmentSlots.Eyewear, _entityId, _location, _tag);
+			AddEquipment ("face", _equipment, EquipmentSlots.Face, _entityId, _location, _tag);
+
+			AddEquipment ("armor", _equipment, EquipmentSlots.ChestArmor, _entityId, _location, _tag);
+			AddEquipment ("jacket", _equipment, EquipmentSlots.Jacket, _entityId, _location, _tag);
+			AddEquipment ("shirt", _equipment, EquipmentSlots.Shirt, _entityId, _location, _tag);
+
+			AddEquipment ("legarmor", _equipment, EquipmentSlots.LegArmor, _entityId, _location, _tag);
+			AddEquipment ("pants", _equipment, EquipmentSlots.Legs, _entityId, _location, _tag);
+			AddEquipment ("boots", _equipment, EquipmentSlots.Feet, _entityId, _location, _tag);
+
+			AddEquipment ("gloves", _equipment, EquipmentSlots.Hands, _entityId, _location, _tag);
+		}
+
+		private void AddEquipment (string _slotname, InvItem[] _items, EquipmentSlots _slot, int _entityId,
+			string _location, string _tag) {
+			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]];
+				if (_tag == null) {
+					// no Tag defined -> readable output
+					if (item.quality < 0) {
+						SdtdConsole.Instance.Output ($"    Slot {_slotname:8}: {item.itemName:000}");
+					} else {
+						SdtdConsole.Instance.Output ($"    Slot {_slotname:8}: {item.itemName:000} - quality: {item.quality}");
+					}
+
+					DoParts (_items [slotindices [i]].parts, 1, null);
+				} else {
+					// 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 + ")";
+					SdtdConsole.Instance.Output (msg);
+				}
+
+				return;
+			}
+		}
+
+		private string DoParts (InvItem[] _parts, int _indent, string _currentMessage) {
+			if (_parts == null || _parts.Length <= 0) {
+				return _currentMessage;
+			}
+
+			string indenter = new string (' ', _indent * 4);
+			for (int i = 0; i < _parts.Length; i++) {
+				if (_parts [i] == null) {
+					continue;
+				}
+
+				if (_currentMessage == null) {
+					// no currentMessage given -> readable output
+					if (_parts [i].quality < 0) {
+						SdtdConsole.Instance.Output ($"{indenter}         - {_parts [i].itemName}");
+					} else {
+						SdtdConsole.Instance.Output ($"{indenter}         - {_parts [i].itemName} - quality: {_parts [i].quality}");
+					}
+
+					DoParts (_parts [i].parts, _indent + 1, _currentMessage);
+				} else {
+					// currentMessage given -> parseable output
+					if (_currentMessage.Length > 0) {
+						_currentMessage += ",";
+					}
+
+					_currentMessage += _parts [i].itemName + "@" + _parts [i].quality;
+					_currentMessage = DoParts (_parts [i].parts, _indent + 1, _currentMessage);
+				}
+			}
+
+			return _currentMessage;
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/ModApi.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/ModApi.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/ModApi.cs	(revision 391)
@@ -0,0 +1,9 @@
+using JetBrains.Annotations;
+
+namespace CommandExtensions {
+	[UsedImplicitly]
+	public class ModApi : IModApi {
+		public void InitMod (Mod _modInstance) {
+		}
+	}
+}
Index: binary-improvements2/CommandExtensions/src/PrivateMessageConnections.cs
===================================================================
--- binary-improvements2/CommandExtensions/src/PrivateMessageConnections.cs	(revision 391)
+++ binary-improvements2/CommandExtensions/src/PrivateMessageConnections.cs	(revision 391)
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace CommandExtensions {
+	public static class PrivateMessageConnections {
+		private static readonly Dictionary<PlatformUserIdentifierAbs, PlatformUserIdentifierAbs> senderOfLastPM = new Dictionary<PlatformUserIdentifierAbs, PlatformUserIdentifierAbs> ();
+
+		public static void SetLastPMSender (ClientInfo _sender, ClientInfo _receiver) {
+			senderOfLastPM [_receiver.InternalId] = _sender.InternalId;
+		}
+
+		public static ClientInfo GetLastPMSenderForPlayer (ClientInfo _player) {
+			if (!senderOfLastPM.TryGetValue (_player.InternalId, out PlatformUserIdentifierAbs recUserId)) {
+				return null;
+			}
+
+			ClientInfo recInfo = ConnectionManager.Instance.Clients.ForUserId (recUserId);
+			return recInfo;
+		}
+	}
+}
Index: binary-improvements2/MapRendering/API.cs
===================================================================
--- binary-improvements2/MapRendering/API.cs	(revision 390)
+++ 	(revision )
@@ -1,39 +1,0 @@
-using AllocsFixes.NetConnections.Servers.Web;
-using AllocsFixes.NetConnections.Servers.Web.Handlers;
-
-namespace AllocsFixes {
-	public class API : IModApi {
-		private Web webInstance;
-		private Mod modInstance;
-		
-		public void InitMod (Mod _modInstance) {
-			ModEvents.GameStartDone.RegisterHandler (GameStartDone);
-			ModEvents.GameShutdown.RegisterHandler (GameShutdown);
-			ModEvents.CalcChunkColorsDone.RegisterHandler (CalcChunkColorsDone);
-			modInstance = _modInstance;
-		}
-
-		private void GameStartDone () {
-			// ReSharper disable once ObjectCreationAsStatement
-			if (!ConnectionManager.Instance.IsServer) {
-				return;
-			}
-			
-			webInstance = new Web (modInstance.Path);
-			LogBuffer.Init ();
-
-			if (ItemIconHandler.Instance != null) {
-				ItemIconHandler.Instance.LoadIcons ();
-			}
-		}
-
-		private void GameShutdown () {
-			webInstance?.Shutdown ();
-			MapRendering.MapRendering.Shutdown ();
-		}
-
-		private void CalcChunkColorsDone (Chunk _chunk) {
-			MapRendering.MapRendering.RenderSingleChunk (_chunk);
-		}
-	}
-}
Index: binary-improvements2/MapRendering/AssemblyInfo.cs
===================================================================
--- binary-improvements2/MapRendering/AssemblyInfo.cs	(revision 390)
+++ 	(revision )
@@ -1,25 +1,0 @@
-using System.Reflection;
-
-// Information about this assembly is defined by the following attributes. 
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle ("MapRendering")]
-[assembly: AssemblyDescription ("")]
-[assembly: AssemblyConfiguration ("")]
-[assembly: AssemblyCompany ("")]
-[assembly: AssemblyProduct ("")]
-[assembly: AssemblyCopyright ("ci")]
-[assembly: AssemblyTrademark ("")]
-[assembly: AssemblyCulture ("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion ("0.0.0.0")]
-
-// The following attributes are used to specify the signing key for the assembly, 
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
Index: binary-improvements2/MapRendering/Commands/EnableOpenIDDebug.cs
===================================================================
--- binary-improvements2/MapRendering/Commands/EnableOpenIDDebug.cs	(revision 390)
+++ 	(revision )
@@ -1,24 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.NetConnections.Servers.Web;
-
-namespace AllocsFixes.CustomCommands {
-	public class EnableOpenIDDebug : ConsoleCmdAbstract {
-		public override string GetDescription () {
-			return "enable/disable OpenID debugging";
-		}
-
-		public override string[] GetCommands () {
-			return new[] {"openiddebug"};
-		}
-
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
-			if (_params.Count != 1) {
-				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"));
-		}
-	}
-}
Index: binary-improvements2/MapRendering/Commands/EnableRendering.cs
===================================================================
--- binary-improvements2/MapRendering/Commands/EnableRendering.cs	(revision 390)
+++ 	(revision )
@@ -1,23 +1,0 @@
-using System.Collections.Generic;
-
-namespace AllocsFixes.CustomCommands {
-	public class EnableRendering : ConsoleCmdAbstract {
-		public override string GetDescription () {
-			return "enable/disable live map rendering";
-		}
-
-		public override string[] GetCommands () {
-			return new[] {"enablerendering"};
-		}
-
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
-			if (_params.Count != 1) {
-				SdtdConsole.Instance.Output ("Current state: " + MapRendering.MapRendering.renderingEnabled);
-				return;
-			}
-
-			MapRendering.MapRendering.renderingEnabled = _params [0].Equals ("1");
-			SdtdConsole.Instance.Output ("Set live map rendering to " + _params [0].Equals ("1"));
-		}
-	}
-}
Index: binary-improvements2/MapRendering/Commands/Exception.cs
===================================================================
--- binary-improvements2/MapRendering/Commands/Exception.cs	(revision 390)
+++ 	(revision )
@@ -1,23 +1,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace AllocsFixes.CustomCommands {
-	public class ConsoleCmdException : ConsoleCmdAbstract {
-		public override string[] GetCommands () {
-			return new[] { "exception" };
-		}
-
-		public override bool AllowedInMainMenu => true;
-
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
-			Log.Out ("Test info");
-			Log.Warning ("Test warning");
-			Log.Error ("Test error");
-			throw new Exception ("Test exception");
-		}
-
-		public override string GetDescription () {
-			return "Throw an exception / log messages";
-		}
-	}
-}
Index: binary-improvements2/MapRendering/Commands/ReloadWebPermissions.cs
===================================================================
--- binary-improvements2/MapRendering/Commands/ReloadWebPermissions.cs	(revision 390)
+++ 	(revision )
@@ -1,19 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.NetConnections.Servers.Web;
-
-namespace AllocsFixes.CustomCommands {
-	public class ReloadWebPermissions : ConsoleCmdAbstract {
-		public override string GetDescription () {
-			return "force reload of web permissions file";
-		}
-
-		public override string[] GetCommands () {
-			return new[] {"reloadwebpermissions"};
-		}
-
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
-			WebPermissions.Instance.Load ();
-			SdtdConsole.Instance.Output ("Web permissions file reloaded");
-		}
-	}
-}
Index: binary-improvements2/MapRendering/Commands/RenderMap.cs
===================================================================
--- binary-improvements2/MapRendering/Commands/RenderMap.cs	(revision 390)
+++ 	(revision )
@@ -1,19 +1,0 @@
-using System.Collections.Generic;
-
-namespace AllocsFixes.CustomCommands {
-	public class RenderMap : ConsoleCmdAbstract {
-		public override string GetDescription () {
-			return "render the current map to a file";
-		}
-
-		public override string[] GetCommands () {
-			return new[] {"rendermap"};
-		}
-
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
-			MapRendering.MapRendering.Instance.RenderFullMap ();
-
-			SdtdConsole.Instance.Output ("Render map done");
-		}
-	}
-}
Index: binary-improvements2/MapRendering/Commands/WebPermissionsCmd.cs
===================================================================
--- binary-improvements2/MapRendering/Commands/WebPermissionsCmd.cs	(revision 390)
+++ 	(revision )
@@ -1,83 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.NetConnections.Servers.Web;
-
-namespace AllocsFixes.CustomCommands {
-	public class WebPermissionsCmd : ConsoleCmdAbstract {
-		public override string[] GetCommands () {
-			return new[] {"webpermission"};
-		}
-
-		public override string GetDescription () {
-			return "Manage web permission levels";
-		}
-
-		public override string GetHelp () {
-			return "Set/get permission levels required to access a given web functionality. Default\n" +
-			       "level required for functions that are not explicitly specified is 0.\n" +
-			       "Usage:\n" +
-			       "   webpermission add <webfunction> <level>\n" +
-			       "   webpermission remove <webfunction>\n" +
-			       "   webpermission list";
-		}
-
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
-			if (_params.Count >= 1) {
-				if (_params [0].EqualsCaseInsensitive ("add")) {
-					ExecuteAdd (_params);
-				} else if (_params [0].EqualsCaseInsensitive ("remove")) {
-					ExecuteRemove (_params);
-				} else if (_params [0].EqualsCaseInsensitive ("list")) {
-					ExecuteList ();
-				} else {
-					SdtdConsole.Instance.Output ($"Invalid sub command \"{_params [0]}\".");
-				}
-			} else {
-				SdtdConsole.Instance.Output ("No sub command given.");
-			}
-		}
-
-		private void ExecuteAdd (List<string> _params) {
-			if (_params.Count != 3) {
-				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 3, found {_params.Count}.");
-				return;
-			}
-
-			if (!WebPermissions.Instance.IsKnownModule (_params [1])) {
-				SdtdConsole.Instance.Output ($"\"{_params [1]}\" is not a valid web function.");
-				return;
-			}
-
-			int level;
-			if (!int.TryParse (_params [2], out level)) {
-				SdtdConsole.Instance.Output ($"\"{_params [2]}\" is not a valid integer.");
-				return;
-			}
-
-			WebPermissions.Instance.AddModulePermission (_params [1], level);
-			SdtdConsole.Instance.Output ($"{_params [1]} added with permission level of {level}.");
-		}
-
-		private void ExecuteRemove (List<string> _params) {
-			if (_params.Count != 2) {
-				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 2, found {_params.Count}.");
-				return;
-			}
-
-			if (!WebPermissions.Instance.IsKnownModule (_params [1])) {
-				SdtdConsole.Instance.Output ($"\"{_params [1]}\" is not a valid web function.");
-				return;
-			}
-
-			WebPermissions.Instance.RemoveModulePermission (_params [1]);
-			SdtdConsole.Instance.Output ($"{_params [1]} removed from permissions list.");
-		}
-
-		private void ExecuteList () {
-			SdtdConsole.Instance.Output ("Defined web function permissions:");
-			SdtdConsole.Instance.Output ("  Level: Web function");
-			foreach (WebPermissions.WebModulePermission wmp in WebPermissions.Instance.GetModules ()) {
-				SdtdConsole.Instance.Output ($"  {wmp.permissionLevel,5}: {wmp.module}");
-			}
-		}
-	}
-}
Index: binary-improvements2/MapRendering/Commands/WebTokens.cs
===================================================================
--- binary-improvements2/MapRendering/Commands/WebTokens.cs	(revision 390)
+++ 	(revision )
@@ -1,111 +1,0 @@
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
-using AllocsFixes.NetConnections.Servers.Web;
-
-namespace AllocsFixes.CustomCommands {
-	public class WebTokens : ConsoleCmdAbstract {
-		private static readonly Regex validNameTokenMatcher = new Regex (@"^\w+$");
-
-		public override string[] GetCommands () {
-			return new[] {"webtokens"};
-		}
-
-		public override string GetDescription () {
-			return "Manage web tokens";
-		}
-
-		public override string GetHelp () {
-			return "Set/get webtoken permission levels. A level of 0 is maximum permission.\n" +
-			       "Usage:\n" +
-			       "   webtokens add <username> <usertoken> <level>\n" +
-			       "   webtokens remove <username>\n" +
-			       "   webtokens list";
-		}
-
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
-			if (_params.Count >= 1) {
-				if (_params [0].EqualsCaseInsensitive ("add")) {
-					ExecuteAdd (_params);
-				} else if (_params [0].EqualsCaseInsensitive ("remove")) {
-					ExecuteRemove (_params);
-				} else if (_params [0].EqualsCaseInsensitive ("list")) {
-					ExecuteList ();
-				} else {
-					SdtdConsole.Instance.Output ("Invalid sub command \"" + _params [0] + "\".");
-				}
-			} else {
-				SdtdConsole.Instance.Output ("No sub command given.");
-			}
-		}
-
-		private void ExecuteAdd (List<string> _params) {
-			if (_params.Count != 4) {
-				SdtdConsole.Instance.Output ("Wrong number of arguments, expected 4, found " + _params.Count + ".");
-				return;
-			}
-
-			if (string.IsNullOrEmpty (_params [1])) {
-				SdtdConsole.Instance.Output ("Argument 'username' is empty.");
-				return;
-			}
-
-			if (!validNameTokenMatcher.IsMatch (_params [1])) {
-				SdtdConsole.Instance.Output (
-					"Argument 'username' may only contain characters (A-Z, a-z), digits (0-9) and underscores (_).");
-				return;
-			}
-
-			if (string.IsNullOrEmpty (_params [2])) {
-				SdtdConsole.Instance.Output ("Argument 'usertoken' is empty.");
-				return;
-			}
-
-			if (!validNameTokenMatcher.IsMatch (_params [2])) {
-				SdtdConsole.Instance.Output (
-					"Argument 'usertoken' may only contain characters (A-Z, a-z), digits (0-9) and underscores (_).");
-				return;
-			}
-
-			int level;
-			if (!int.TryParse (_params [3], out level)) {
-				SdtdConsole.Instance.Output ("Argument 'level' is not a valid integer.");
-				return;
-			}
-
-			WebPermissions.Instance.AddAdmin (_params [1], _params [2], level);
-			SdtdConsole.Instance.Output (string.Format (
-				"Web user with name={0} and password={1} added with permission level of {2}.", _params [1], _params [2],
-				level));
-		}
-
-		private void ExecuteRemove (List<string> _params) {
-			if (_params.Count != 2) {
-				SdtdConsole.Instance.Output ("Wrong number of arguments, expected 2, found " + _params.Count + ".");
-				return;
-			}
-
-			if (string.IsNullOrEmpty (_params [1])) {
-				SdtdConsole.Instance.Output ("Argument 'username' is empty.");
-				return;
-			}
-
-			if (!validNameTokenMatcher.IsMatch (_params [1])) {
-				SdtdConsole.Instance.Output (
-					"Argument 'username' may only contain characters (A-Z, a-z), digits (0-9) and underscores (_).");
-				return;
-			}
-
-			WebPermissions.Instance.RemoveAdmin (_params [1]);
-			SdtdConsole.Instance.Output (string.Format ("{0} removed from web user permissions list.", _params [1]));
-		}
-
-		private void ExecuteList () {
-			SdtdConsole.Instance.Output ("Defined webuser permissions:");
-			SdtdConsole.Instance.Output ("  Level: Name / Token");
-			foreach (WebPermissions.AdminToken at in WebPermissions.Instance.GetAdmins ()) {
-				SdtdConsole.Instance.Output (
-					string.Format ("  {0,5}: {1} / {2}", at.permissionLevel, at.name, at.token));
-			}
-		}
-	}
-}
Index: binary-improvements2/MapRendering/Commands/webstat.cs
===================================================================
--- binary-improvements2/MapRendering/Commands/webstat.cs	(revision 390)
+++ 	(revision )
@@ -1,33 +1,0 @@
-using System.Collections.Generic;
-using AllocsFixes.NetConnections.Servers.Web;
-
-namespace AllocsFixes.CustomCommands {
-	public class webstat : ConsoleCmdAbstract {
-		public override string GetDescription () {
-			return "DEBUG PURPOSES ONLY";
-		}
-
-		public override string[] GetCommands () {
-			return new[] {"webstat"};
-		}
-
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
-			int curHandlers = Web.currentHandlers;
-			int totalHandlers = Web.handlingCount;
-			long totalTime = Web.totalHandlingTime;
-			SdtdConsole.Instance.Output ("Current Web handlers: " + curHandlers + " - total: " + totalHandlers);
-			SdtdConsole.Instance.Output (" - Total time: " + totalTime + " µs - average time: " +
-			                             totalTime / totalHandlers + " µs");
-
-			curHandlers = WebCommandResult.currentHandlers;
-			totalHandlers = WebCommandResult.handlingCount;
-			totalTime = WebCommandResult.totalHandlingTime;
-			SdtdConsole.Instance.Output ("Current Web command handlers: " + curHandlers + " - total: " +
-			                             totalHandlers);
-			SdtdConsole.Instance.Output (" - Total time: " + totalTime + " µs" +
-			                             (totalHandlers > 0
-				                             ? " - average time: " + totalTime / totalHandlers + " µs"
-				                             : ""));
-		}
-	}
-}
Index: binary-improvements2/MapRendering/MapRendering.csproj
===================================================================
--- binary-improvements2/MapRendering/MapRendering.csproj	(revision 391)
+++ binary-improvements2/MapRendering/MapRendering.csproj	(revision 391)
@@ -0,0 +1,102 @@
+﻿<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{A1847B5F-7BFC-4BCD-94AA-A6C9FB7E7C54}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>MapRendering</RootNamespace>
+    <AssemblyName>MapRendering</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <LangVersion>8</LangVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\bin\Mods\TFP_MapRendering\</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <NoStdLib>true</NoStdLib>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_Profiler|AnyCPU' ">
+    <OutputPath>..\bin\Mods\TFP_MapRendering\</OutputPath>
+    <DefineConstants>ENABLE_PROFILER</DefineConstants>
+    <Optimize>true</Optimize>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <WarningLevel>4</WarningLevel>
+    <NoStdLib>true</NoStdLib>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <OutputPath>..\bin\Mods\TFP_MapRendering\</OutputPath>
+    <DebugType>full</DebugType>
+    <DebugSymbols>true</DebugSymbols>
+  </PropertyGroup>
+  <ItemGroup>
+    <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="LogLibrary">
+      <HintPath>..\7dtd-binaries\LogLibrary.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+      <HintPath>..\7dtd-binaries\mscorlib.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+      <HintPath>..\7dtd-binaries\System.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+      <HintPath>..\7dtd-binaries\System.Xml.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="UnityEngine">
+      <HintPath>..\7dtd-binaries\UnityEngine.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="Assembly-CSharp">
+      <HintPath>..\7dtd-binaries\Assembly-CSharp.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="mscorlib">
+      <HintPath>..\7dtd-binaries\mscorlib.dll</HintPath>
+      <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="UnityEngine.ImageConversionModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>..\7dtd-binaries\UnityEngine.ImageConversionModule.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="src\Constants.cs" />
+    <Compile Include="src\MapRenderBlockBuffer.cs" />
+    <Compile Include="src\MapRenderer.cs" />
+    <Compile Include="src\ModApi.cs" />
+    <Compile Include="src\AssemblyInfo.cs" />
+    <Compile Include="src\Commands\EnableRendering.cs" />
+    <Compile Include="src\Commands\RenderMap.cs" />
+  </ItemGroup>
+  <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>
+</Project>
Index: binary-improvements2/MapRendering/MapRendering/Constants.cs
===================================================================
--- binary-improvements2/MapRendering/MapRendering/Constants.cs	(revision 390)
+++ 	(revision )
@@ -1,20 +1,0 @@
-using UnityEngine;
-
-namespace AllocsFixes.MapRendering {
-	public class Constants {
-		public static readonly TextureFormat DEFAULT_TEX_FORMAT = TextureFormat.ARGB32;
-		public static int MAP_BLOCK_SIZE = 128;
-		public const int MAP_CHUNK_SIZE = 16;
-		public const int MAP_REGION_SIZE = 512;
-		public static int ZOOMLEVELS = 5;
-		public static string MAP_DIRECTORY = string.Empty;
-
-		public static int MAP_BLOCK_TO_CHUNK_DIV {
-			get { return MAP_BLOCK_SIZE / MAP_CHUNK_SIZE; }
-		}
-
-		public static int MAP_REGION_TO_CHUNK_DIV {
-			get { return MAP_REGION_SIZE / MAP_CHUNK_SIZE; }
-		}
-	}
-}
Index: binary-improvements2/MapRendering/MapRendering/MapRenderBlockBuffer.cs
===================================================================
--- binary-improvements2/MapRendering/MapRendering/MapRenderBlockBuffer.cs	(revision 390)
+++ 	(revision )
@@ -1,232 +1,0 @@
-using System;
-using System.IO;
-using AllocsFixes.FileCache;
-using Unity.Collections;
-using UnityEngine;
-using UnityEngine.Profiling;
-
-namespace AllocsFixes.MapRendering {
-	public class MapRenderBlockBuffer {
-		private readonly Texture2D blockMap = new Texture2D (Constants.MAP_BLOCK_SIZE, Constants.MAP_BLOCK_SIZE, Constants.DEFAULT_TEX_FORMAT, false);
-		private readonly MapTileCache cache;
-		private readonly NativeArray<int> emptyImageData;
-		private readonly Texture2D zoomBuffer = new Texture2D (Constants.MAP_BLOCK_SIZE / 2, Constants.MAP_BLOCK_SIZE / 2, Constants.DEFAULT_TEX_FORMAT, false);
-		private readonly int zoomLevel;
-		private readonly string folderBase;
-		
-		private Vector2i currentBlockMapPos = new Vector2i (Int32.MinValue, Int32.MinValue);
-		private string currentBlockMapFolder = string.Empty;
-
-		public MapRenderBlockBuffer (int _level, MapTileCache _cache) {
-			zoomLevel = _level;
-			cache = _cache;
-			folderBase = Constants.MAP_DIRECTORY + "/" + zoomLevel + "/";
-
-			{
-				// Initialize empty tile data
-				Color nullColor = new Color (0, 0, 0, 0);
-				for (int x = 0; x < Constants.MAP_BLOCK_SIZE; x++) {
-					for (int y = 0; y < Constants.MAP_BLOCK_SIZE; y++) {
-						blockMap.SetPixel (x, y, nullColor);
-					}
-				}
-
-				NativeArray<int> blockMapData = blockMap.GetRawTextureData<int> ();
-				emptyImageData = new NativeArray<int> (blockMapData.Length, Allocator.Persistent,
-					NativeArrayOptions.UninitializedMemory);
-				blockMapData.CopyTo (emptyImageData);
-			}
-		}
-
-		public TextureFormat FormatSelf {
-			get { return blockMap.format; }
-		}
-
-		public void ResetBlock () {
-			currentBlockMapFolder = string.Empty;
-			currentBlockMapPos = new Vector2i (Int32.MinValue, Int32.MinValue);
-			cache.ResetTile (zoomLevel);
-		}
-
-		public void SaveBlock () {
-			Profiler.BeginSample ("SaveBlock");
-			try {
-				saveTextureToFile ();
-			} catch (Exception e) {
-				Log.Warning ("Exception in MapRenderBlockBuffer.SaveBlock(): " + e);
-			}
-			Profiler.EndSample ();
-		}
-
-		public bool LoadBlock (Vector2i _block) {
-			Profiler.BeginSample ("LoadBlock");
-			lock (blockMap) {
-				if (currentBlockMapPos != _block) {
-					Profiler.BeginSample ("LoadBlock.Strings");
-					string folder;
-					if (currentBlockMapPos.x != _block.x) {
-						folder = folderBase + _block.x + '/';
-
-						Profiler.BeginSample ("LoadBlock.Directory");
-						Directory.CreateDirectory (folder);
-						Profiler.EndSample ();
-					} else {
-						folder = currentBlockMapFolder;
-					}
-
-					string fileName = folder + _block.y + ".png";
-					Profiler.EndSample ();
-					
-					SaveBlock ();
-					loadTextureFromFile (fileName);
-
-					currentBlockMapFolder = folder;
-					currentBlockMapPos = _block;
-
-					Profiler.EndSample ();
-					return true;
-				}
-			}
-
-			Profiler.EndSample ();
-			return false;
-		}
-
-		public void SetPart (Vector2i _offset, int _partSize, Color32[] _pixels) {
-			if (_offset.x + _partSize > Constants.MAP_BLOCK_SIZE || _offset.y + _partSize > Constants.MAP_BLOCK_SIZE) {
-				Log.Error (string.Format ("MapBlockBuffer[{0}].SetPart ({1}, {2}, {3}) has blockMap.size ({4}/{5})",
-					zoomLevel, _offset, _partSize, _pixels.Length, Constants.MAP_BLOCK_SIZE, Constants.MAP_BLOCK_SIZE));
-				return;
-			}
-
-			Profiler.BeginSample ("SetPart");
-			blockMap.SetPixels32 (_offset.x, _offset.y, _partSize, _partSize, _pixels);
-			Profiler.EndSample ();
-		}
-
-		public Color32[] GetHalfScaled () {
-			Profiler.BeginSample ("HalfScaled.ResizeBuffer");
-			zoomBuffer.Resize (Constants.MAP_BLOCK_SIZE, Constants.MAP_BLOCK_SIZE);
-			Profiler.EndSample ();
-
-			Profiler.BeginSample ("HalfScaled.CopyPixels");
-			if (blockMap.format == zoomBuffer.format) {
-				Profiler.BeginSample ("Native");
-				NativeArray<byte> dataSrc = blockMap.GetRawTextureData<byte> ();
-				NativeArray<byte> dataZoom = zoomBuffer.GetRawTextureData<byte> ();
-				dataSrc.CopyTo (dataZoom);
-				Profiler.EndSample ();
-			} else {
-				Profiler.BeginSample ("GetSetPixels");
-				zoomBuffer.SetPixels32 (blockMap.GetPixels32 ());
-				Profiler.EndSample ();
-			}
-			Profiler.EndSample ();
-
-			Profiler.BeginSample ("HalfScaled.Scale");
-			TextureScale.Point (zoomBuffer, Constants.MAP_BLOCK_SIZE / 2, Constants.MAP_BLOCK_SIZE / 2);
-			Profiler.EndSample ();
-
-			Profiler.BeginSample ("HalfScaled.Return");
-			Color32[] result = zoomBuffer.GetPixels32 ();
-			Profiler.EndSample ();
-
-			return result;
-		}
-
-		public void SetPartNative (Vector2i _offset, int _partSize, NativeArray<int> _pixels) {
-			if (_offset.x + _partSize > Constants.MAP_BLOCK_SIZE || _offset.y + _partSize > Constants.MAP_BLOCK_SIZE) {
-				Log.Error (string.Format ("MapBlockBuffer[{0}].SetPart ({1}, {2}, {3}) has blockMap.size ({4}/{5})",
-					zoomLevel, _offset, _partSize, _pixels.Length, Constants.MAP_BLOCK_SIZE, Constants.MAP_BLOCK_SIZE));
-				return;
-			}
-
-			Profiler.BeginSample ("SetPartNative");
-			NativeArray<int> destData = blockMap.GetRawTextureData<int> ();
-			
-			for (int y = 0; y < _partSize; y++) {
-				int srcLineStartIdx = _partSize * y;
-				int destLineStartIdx = blockMap.width * (_offset.y + y) + _offset.x;
-				for (int x = 0; x < _partSize; x++) {
-					destData [destLineStartIdx + x] = _pixels [srcLineStartIdx + x];
-				}
-			}
-			Profiler.EndSample ();
-		}
-
-		public NativeArray<int> GetHalfScaledNative () {
-			Profiler.BeginSample ("HalfScaledNative.ResizeBuffer");
-			if (zoomBuffer.format != blockMap.format || zoomBuffer.height != Constants.MAP_BLOCK_SIZE / 2 || zoomBuffer.width != Constants.MAP_BLOCK_SIZE / 2) {
-				zoomBuffer.Resize (Constants.MAP_BLOCK_SIZE / 2, Constants.MAP_BLOCK_SIZE / 2, blockMap.format, false);
-			}
-			Profiler.EndSample ();
-
-			Profiler.BeginSample ("HalfScaledNative.Scale");
-			ScaleNative (blockMap, zoomBuffer);
-			Profiler.EndSample ();
-
-			return zoomBuffer.GetRawTextureData<int> ();
-		}
-		
-		private static void ScaleNative (Texture2D _sourceTex, Texture2D _targetTex) {
-			NativeArray<int> srcData = _sourceTex.GetRawTextureData<int> ();
-			NativeArray<int> targetData = _targetTex.GetRawTextureData<int> ();
-			
-			int oldWidth = _sourceTex.width;
-			int oldHeight = _sourceTex.height;
-			int newWidth = _targetTex.width;
-			int newHeight = _targetTex.height;
-			
-			float ratioX = ((float) oldWidth) / newWidth;
-			float ratioY = ((float) oldHeight) / newHeight;
-
-			for (var y = 0; y < newHeight; y++) {
-				var oldLineStart = (int) (ratioY * y) * oldWidth;
-				var newLineStart = y * newWidth;
-				for (var x = 0; x < newWidth; x++) {
-					targetData [newLineStart + x] = srcData [(int) (oldLineStart + ratioX * x)];
-				}
-			}
-		}
-
-		private void loadTextureFromFile (string _fileName) {
-			Profiler.BeginSample ("LoadTexture");
-
-			Profiler.BeginSample ("LoadFile");
-			byte[] array = cache.LoadTile (zoomLevel, _fileName);
-			Profiler.EndSample ();
-
-			Profiler.BeginSample ("LoadImage");
-			if (array != null && blockMap.LoadImage (array) && blockMap.height == Constants.MAP_BLOCK_SIZE &&
-			    blockMap.width == Constants.MAP_BLOCK_SIZE) {
-				Profiler.EndSample ();
-
-				Profiler.EndSample ();
-				return;
-			}
-			Profiler.EndSample ();
-
-			if (array != null) {
-				Log.Error ("Map image tile " + _fileName + " has been corrupted, recreating tile");
-			}
-
-			if (blockMap.format != Constants.DEFAULT_TEX_FORMAT || blockMap.height != Constants.MAP_BLOCK_SIZE ||
-			    blockMap.width != Constants.MAP_BLOCK_SIZE) {
-				blockMap.Resize (Constants.MAP_BLOCK_SIZE, Constants.MAP_BLOCK_SIZE, Constants.DEFAULT_TEX_FORMAT,
-					false);
-			}
-
-			blockMap.LoadRawTextureData (emptyImageData);
-
-			Profiler.EndSample ();
-		}
-
-		private void saveTextureToFile () {
-			Profiler.BeginSample ("EncodePNG");
-			byte[] array = blockMap.EncodeToPNG ();
-			Profiler.EndSample ();
-
-			cache.SaveTile (zoomLevel, array);
-		}
-	}
-}
Index: binary-improvements2/MapRendering/MapRendering/MapRendering.cs
===================================================================
--- binary-improvements2/MapRendering/MapRendering/MapRendering.cs	(revision 390)
+++ 	(revision )
@@ -1,420 +1,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading;
-using AllocsFixes.FileCache;
-using AllocsFixes.JSON;
-using UnityEngine;
-using UnityEngine.Profiling;
-using Object = UnityEngine.Object;
-
-namespace AllocsFixes.MapRendering {
-	public class MapRendering {
-		private static MapRendering instance;
-
-		private static readonly object lockObject = new object ();
-		public static bool renderingEnabled = true;
-		private readonly MapTileCache cache = new MapTileCache (Constants.MAP_BLOCK_SIZE);
-		private readonly Dictionary<Vector2i, Color32[]> dirtyChunks = new Dictionary<Vector2i, Color32[]> ();
-		private readonly MicroStopwatch msw = new MicroStopwatch ();
-		private readonly MapRenderBlockBuffer[] zoomLevelBuffers;
-		private Coroutine renderCoroutineRef;
-		private bool renderingFullMap;
-		private float renderTimeout = float.MaxValue;
-		private bool shutdown;
-
-		private MapRendering () {
-			Constants.MAP_DIRECTORY = GameIO.GetSaveGameDir () + "/map";
-
-			lock (lockObject) {
-				if (!LoadMapInfo ()) {
-					WriteMapInfo ();
-				}
-			}
-
-			cache.SetZoomCount (Constants.ZOOMLEVELS);
-
-			zoomLevelBuffers = new MapRenderBlockBuffer[Constants.ZOOMLEVELS];
-			for (int i = 0; i < Constants.ZOOMLEVELS; i++) {
-				zoomLevelBuffers [i] = new MapRenderBlockBuffer (i, cache);
-			}
-
-			renderCoroutineRef = ThreadManager.StartCoroutine (renderCoroutine ());
-		}
-
-		public static MapRendering Instance {
-			get {
-				if (instance == null) {
-					instance = new MapRendering ();
-				}
-
-				return instance;
-			}
-		}
-
-		public static MapTileCache GetTileCache () {
-			return Instance.cache;
-		}
-
-		public static void Shutdown () {
-			if (Instance == null) {
-				return;
-			}
-
-			Instance.shutdown = true;
-			
-			if (Instance.renderCoroutineRef != null) {
-				ThreadManager.StopCoroutine (Instance.renderCoroutineRef);
-				Instance.renderCoroutineRef = null;
-			}
-		}
-
-		public static void RenderSingleChunk (Chunk _chunk) {
-			if (renderingEnabled && Instance != null) {
-				// TODO: Replace with regular thread and a blocking queue / set
-				ThreadPool.UnsafeQueueUserWorkItem (_o => {
-					try {
-						if (!Instance.renderingFullMap) {
-							lock (lockObject) {
-								Chunk c = (Chunk) _o;
-								Vector3i cPos = c.GetWorldPos ();
-								Vector2i cPos2 = new Vector2i (cPos.x / Constants.MAP_CHUNK_SIZE,
-									cPos.z / Constants.MAP_CHUNK_SIZE);
-
-								ushort[] mapColors = c.GetMapColors ();
-								if (mapColors != null) {
-									Color32[] realColors =
-										new Color32[Constants.MAP_CHUNK_SIZE * Constants.MAP_CHUNK_SIZE];
-									for (int i_colors = 0; i_colors < mapColors.Length; i_colors++) {
-										realColors [i_colors] = shortColorToColor32 (mapColors [i_colors]);
-									}
-
-									Instance.dirtyChunks [cPos2] = realColors;
-
-									//Log.Out ("Add Dirty: " + cPos2);
-								}
-							}
-						}
-					} catch (Exception e) {
-						Log.Out ("Exception in MapRendering.RenderSingleChunk(): " + e);
-					}
-				}, _chunk);
-			}
-		}
-
-		public void RenderFullMap () {
-			MicroStopwatch microStopwatch = new MicroStopwatch ();
-
-			string regionSaveDir = GameIO.GetSaveGameRegionDir ();
-			RegionFileManager rfm = new RegionFileManager (regionSaveDir, regionSaveDir, 0, false);
-			Texture2D fullMapTexture = null;
-
-			Vector2i minChunk, maxChunk;
-			Vector2i minPos, maxPos;
-			int widthChunks, heightChunks, widthPix, heightPix;
-			getWorldExtent (rfm, out minChunk, out maxChunk, out minPos, out maxPos, out widthChunks, out heightChunks,
-				out widthPix, out heightPix);
-
-			Log.Out (string.Format (
-				"RenderMap: min: {0}, max: {1}, minPos: {2}, maxPos: {3}, w/h: {4}/{5}, wP/hP: {6}/{7}",
-				minChunk.ToString (), maxChunk.ToString (),
-				minPos.ToString (), maxPos.ToString (),
-				widthChunks, heightChunks,
-				widthPix, heightPix)
-			);
-
-			lock (lockObject) {
-				for (int i = 0; i < Constants.ZOOMLEVELS; i++) {
-					zoomLevelBuffers [i].ResetBlock ();
-				}
-
-				if (Directory.Exists (Constants.MAP_DIRECTORY)) {
-					Directory.Delete (Constants.MAP_DIRECTORY, true);
-				}
-
-				WriteMapInfo ();
-
-				renderingFullMap = true;
-
-				if (widthPix <= 8192 && heightPix <= 8192) {
-					fullMapTexture = new Texture2D (widthPix, heightPix);
-				}
-
-				Vector2i curFullMapPos = default (Vector2i);
-				Vector2i curChunkPos = default (Vector2i);
-				for (curFullMapPos.x = 0; curFullMapPos.x < widthPix; curFullMapPos.x += Constants.MAP_CHUNK_SIZE) {
-					for (curFullMapPos.y = 0;
-						curFullMapPos.y < heightPix;
-						curFullMapPos.y += Constants.MAP_CHUNK_SIZE) {
-						curChunkPos.x = curFullMapPos.x / Constants.MAP_CHUNK_SIZE + minChunk.x;
-						curChunkPos.y = curFullMapPos.y / Constants.MAP_CHUNK_SIZE + minChunk.y;
-
-						try {
-							long chunkKey = WorldChunkCache.MakeChunkKey (curChunkPos.x, curChunkPos.y);
-							if (rfm.ContainsChunkSync (chunkKey)) {
-								Chunk c = rfm.GetChunkSync (chunkKey);
-								ushort[] mapColors = c.GetMapColors ();
-								if (mapColors != null) {
-									Color32[] realColors =
-										new Color32[Constants.MAP_CHUNK_SIZE * Constants.MAP_CHUNK_SIZE];
-									for (int i_colors = 0; i_colors < mapColors.Length; i_colors++) {
-										realColors [i_colors] = shortColorToColor32 (mapColors [i_colors]);
-									}
-
-									dirtyChunks [curChunkPos] = realColors;
-									if (fullMapTexture != null) {
-										fullMapTexture.SetPixels32 (curFullMapPos.x, curFullMapPos.y,
-											Constants.MAP_CHUNK_SIZE, Constants.MAP_CHUNK_SIZE, realColors);
-									}
-								}
-							}
-						} catch (Exception e) {
-							Log.Out ("Exception: " + e);
-						}
-					}
-
-					while (dirtyChunks.Count > 0) {
-						RenderDirtyChunks ();
-					}
-
-					Log.Out (string.Format ("RenderMap: {0}/{1} ({2}%)", curFullMapPos.x, widthPix,
-						(int) ((float) curFullMapPos.x / widthPix * 100)));
-				}
-			}
-			
-			rfm.Cleanup ();
-
-			if (fullMapTexture != null) {
-				byte[] array = fullMapTexture.EncodeToPNG ();
-				File.WriteAllBytes (Constants.MAP_DIRECTORY + "/map.png", array);
-				Object.Destroy (fullMapTexture);
-			}
-
-			renderingFullMap = false;
-
-			Log.Out ("Generating map took: " + microStopwatch.ElapsedMilliseconds + " ms");
-			Log.Out ("World extent: " + minPos + " - " + maxPos);
-		}
-
-		private void SaveAllBlockMaps () {
-			for (int i = 0; i < Constants.ZOOMLEVELS; i++) {
-				zoomLevelBuffers [i].SaveBlock ();
-			}
-		}
-		
-		private readonly WaitForSeconds coroutineDelay = new WaitForSeconds (0.2f);
-
-		private IEnumerator renderCoroutine () {
-			while (!shutdown) {
-				lock (lockObject) {
-					if (dirtyChunks.Count > 0 && renderTimeout >= float.MaxValue / 2) {
-						renderTimeout = Time.time + 0.5f;
-					}
-
-					if (Time.time > renderTimeout || dirtyChunks.Count > 200) {
-						Profiler.BeginSample ("RenderDirtyChunks");
-						RenderDirtyChunks ();
-						Profiler.EndSample ();
-					}
-				}
-
-				yield return coroutineDelay;
-			}
-		}
-
-		private readonly List<Vector2i> chunksToRender = new List<Vector2i> ();
-		private readonly List<Vector2i> chunksRendered = new List<Vector2i> ();
-
-		private void RenderDirtyChunks () {
-			msw.ResetAndRestart ();
-
-			if (dirtyChunks.Count <= 0) {
-				return;
-			}
-
-			Profiler.BeginSample ("RenderDirtyChunks.Prepare");
-			chunksToRender.Clear ();
-			chunksRendered.Clear ();
-
-			dirtyChunks.CopyKeysTo (chunksToRender);
-
-			Vector2i chunkPos = chunksToRender [0];
-			chunksRendered.Add (chunkPos);
-
-			//Log.Out ("Start Dirty: " + chunkPos);
-
-			getBlockNumber (chunkPos, out Vector2i block, out _, Constants.MAP_BLOCK_TO_CHUNK_DIV, Constants.MAP_CHUNK_SIZE);
-
-			zoomLevelBuffers [Constants.ZOOMLEVELS - 1].LoadBlock (block);
-			Profiler.EndSample ();
-
-			Profiler.BeginSample ("RenderDirtyChunks.Work");
-			// Write all chunks that are in the same image tile of the highest zoom level 
-			Vector2i v_block, v_blockOffset;
-			foreach (Vector2i v in chunksToRender) {
-				getBlockNumber (v, out v_block, out v_blockOffset, Constants.MAP_BLOCK_TO_CHUNK_DIV,
-					Constants.MAP_CHUNK_SIZE);
-				if (v_block.Equals (block)) {
-					//Log.Out ("Dirty: " + v + " render: true");
-					chunksRendered.Add (v);
-					if (dirtyChunks [v].Length != Constants.MAP_CHUNK_SIZE * Constants.MAP_CHUNK_SIZE) {
-						Log.Error (string.Format ("Rendering chunk has incorrect data size of {0} instead of {1}",
-							dirtyChunks [v].Length, Constants.MAP_CHUNK_SIZE * Constants.MAP_CHUNK_SIZE));
-					}
-
-					zoomLevelBuffers [Constants.ZOOMLEVELS - 1]
-						.SetPart (v_blockOffset, Constants.MAP_CHUNK_SIZE, dirtyChunks [v]);
-				}
-			}
-			Profiler.EndSample ();
-
-			foreach (Vector2i v in chunksRendered) {
-				dirtyChunks.Remove (v);
-			}
-
-			// Update lower zoom levels affected by the change of the highest one
-			RenderZoomLevel (block);
-
-			Profiler.BeginSample ("RenderDirtyChunks.SaveAll");
-			SaveAllBlockMaps ();
-			Profiler.EndSample ();
-		}
-
-		private void RenderZoomLevel (Vector2i _innerBlock) {
-			Profiler.BeginSample ("RenderZoomLevel");
-			int level = Constants.ZOOMLEVELS - 1;
-			while (level > 0) {
-				Vector2i block, blockOffset;
-				getBlockNumber (_innerBlock, out block, out blockOffset, 2, Constants.MAP_BLOCK_SIZE / 2);
-
-				zoomLevelBuffers [level - 1].LoadBlock (block);
-
-				Profiler.BeginSample ("RenderZoomLevel.Transfer");
-				if ((zoomLevelBuffers [level].FormatSelf == TextureFormat.ARGB32 ||
-				     zoomLevelBuffers [level].FormatSelf == TextureFormat.RGBA32) &&
-				    zoomLevelBuffers [level].FormatSelf == zoomLevelBuffers [level - 1].FormatSelf) {
-					zoomLevelBuffers [level - 1].SetPartNative (blockOffset, Constants.MAP_BLOCK_SIZE / 2, zoomLevelBuffers [level].GetHalfScaledNative ());
-				} else {
-					zoomLevelBuffers [level - 1].SetPart (blockOffset, Constants.MAP_BLOCK_SIZE / 2, zoomLevelBuffers [level].GetHalfScaled ());
-				}
-				Profiler.EndSample ();
-
-				level--;
-				_innerBlock = block;
-			}
-			Profiler.EndSample ();
-		}
-
-		private void getBlockNumber (Vector2i _innerPos, out Vector2i _block, out Vector2i _blockOffset, int _scaleFactor,
-			int _offsetSize) {
-			_block = default (Vector2i);
-			_blockOffset = default (Vector2i);
-			_block.x = (_innerPos.x + 16777216) / _scaleFactor - 16777216 / _scaleFactor;
-			_block.y = (_innerPos.y + 16777216) / _scaleFactor - 16777216 / _scaleFactor;
-			_blockOffset.x = (_innerPos.x + 16777216) % _scaleFactor * _offsetSize;
-			_blockOffset.y = (_innerPos.y + 16777216) % _scaleFactor * _offsetSize;
-		}
-
-		private void WriteMapInfo () {
-			JSONObject mapInfo = new JSONObject ();
-			mapInfo.Add ("blockSize", new JSONNumber (Constants.MAP_BLOCK_SIZE));
-			mapInfo.Add ("maxZoom", new JSONNumber (Constants.ZOOMLEVELS - 1));
-
-			Directory.CreateDirectory (Constants.MAP_DIRECTORY);
-			File.WriteAllText (Constants.MAP_DIRECTORY + "/mapinfo.json", mapInfo.ToString (), Encoding.UTF8);
-		}
-
-		private bool LoadMapInfo () {
-			if (!File.Exists (Constants.MAP_DIRECTORY + "/mapinfo.json")) {
-				return false;
-			}
-
-			string json = File.ReadAllText (Constants.MAP_DIRECTORY + "/mapinfo.json", Encoding.UTF8);
-			try {
-				JSONNode node = Parser.Parse (json);
-				if (node is JSONObject) {
-					JSONObject jo = (JSONObject) node;
-					if (jo.ContainsKey ("blockSize")) {
-						Constants.MAP_BLOCK_SIZE = ((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);
-			}
-
-			return false;
-		}
-
-		private void getWorldExtent (RegionFileManager _rfm, out Vector2i _minChunk, out Vector2i _maxChunk,
-			out Vector2i _minPos, out Vector2i _maxPos,
-			out int _widthChunks, out int _heightChunks,
-			out int _widthPix, out int _heightPix) {
-			_minChunk = default (Vector2i);
-			_maxChunk = default (Vector2i);
-			_minPos = default (Vector2i);
-			_maxPos = default (Vector2i);
-
-			long[] keys = _rfm.GetAllChunkKeys ();
-			int minX = int.MaxValue;
-			int minY = int.MaxValue;
-			int maxX = int.MinValue;
-			int maxY = int.MinValue;
-			foreach (long key in keys) {
-				int x = WorldChunkCache.extractX (key);
-				int y = WorldChunkCache.extractZ (key);
-
-				if (x < minX) {
-					minX = x;
-				}
-
-				if (x > maxX) {
-					maxX = x;
-				}
-
-				if (y < minY) {
-					minY = y;
-				}
-
-				if (y > maxY) {
-					maxY = y;
-				}
-			}
-
-			_minChunk.x = minX;
-			_minChunk.y = minY;
-
-			_maxChunk.x = maxX;
-			_maxChunk.y = maxY;
-
-			_minPos.x = minX * Constants.MAP_CHUNK_SIZE;
-			_minPos.y = minY * Constants.MAP_CHUNK_SIZE;
-
-			_maxPos.x = maxX * Constants.MAP_CHUNK_SIZE;
-			_maxPos.y = maxY * Constants.MAP_CHUNK_SIZE;
-
-			_widthChunks = maxX - minX + 1;
-			_heightChunks = maxY - minY + 1;
-
-			_widthPix = _widthChunks * Constants.MAP_CHUNK_SIZE;
-			_heightPix = _heightChunks * Constants.MAP_CHUNK_SIZE;
-		}
-
-		private static Color32 shortColorToColor32 (ushort _col) {
-			byte r = (byte) (256 * ((_col >> 10) & 31) / 32);
-			byte g = (byte) (256 * ((_col >> 5) & 31) / 32);
-			byte b = (byte) (256 * (_col & 31) / 32);
-			const byte a = 255;
-			return new Color32 (r, g, b, a);
-		}
-	}
-}
Index: binary-improvements2/MapRendering/ModInfo.xml
===================================================================
--- binary-improvements2/MapRendering/ModInfo.xml	(revision 390)
+++ binary-improvements2/MapRendering/ModInfo.xml	(revision 391)
@@ -2,9 +2,9 @@
 <xml>
 	<ModInfo>
-		<Name value="Allocs MapRendering and Webinterface" />
+		<Name value="TFP_MapRendering" />
 		<Description value="Render the game map to image map tiles as it is uncovered" />
-		<Author value="Christian 'Alloc' Illy" />
-		<Version value="39" />
-		<Website value="http://7dtd.illy.bz" />
+		<Author value="The Fun Pimps LLC" />
+		<Version value="1" />
+		<Website value="" />
 	</ModInfo>
 </xml>
Index: binary-improvements2/MapRendering/WebAndMapRendering.csproj
===================================================================
--- binary-improvements2/MapRendering/WebAndMapRendering.csproj	(revision 390)
+++ 	(revision )
@@ -1,158 +1,0 @@
-﻿<?xml version="1.0" encoding="utf-8"?>
-<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <PropertyGroup>
-    <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
-    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
-    <ProductVersion>8.0.30703</ProductVersion>
-    <SchemaVersion>2.0</SchemaVersion>
-    <ProjectGuid>{A1847B5F-7BFC-4BCD-94AA-A6C9FB7E7C54}</ProjectGuid>
-    <OutputType>Library</OutputType>
-    <RootNamespace>MapRendering</RootNamespace>
-    <AssemblyName>MapRendering</AssemblyName>
-    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
-    <LangVersion>8</LangVersion>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
-    <DebugType>none</DebugType>
-    <Optimize>true</Optimize>
-    <OutputPath>..\bin\Mods\Allocs_WebAndMapRendering\</OutputPath>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-    <ConsolePause>false</ConsolePause>
-    <NoStdLib>true</NoStdLib>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_Profiler|AnyCPU' ">
-    <OutputPath>..\bin\Mods\Allocs_WebAndMapRendering\</OutputPath>
-    <DefineConstants>ENABLE_PROFILER</DefineConstants>
-    <Optimize>true</Optimize>
-    <DebugSymbols>true</DebugSymbols>
-    <DebugType>full</DebugType>
-    <WarningLevel>4</WarningLevel>
-    <NoStdLib>true</NoStdLib>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <OutputPath>..\bin\Mods\Allocs_WebAndMapRendering\</OutputPath>
-    <DebugType>full</DebugType>
-    <DebugSymbols>true</DebugSymbols>
-  </PropertyGroup>
-  <ItemGroup>
-    <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="LogLibrary">
-      <HintPath>..\7dtd-binaries\LogLibrary.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-    <Reference Include="mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
-      <HintPath>..\7dtd-binaries\mscorlib.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-    <Reference Include="System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
-      <HintPath>..\7dtd-binaries\System.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-    <Reference Include="System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
-      <HintPath>..\7dtd-binaries\System.Xml.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-    <Reference Include="UnityEngine">
-      <HintPath>..\7dtd-binaries\UnityEngine.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-    <Reference Include="Assembly-CSharp">
-      <HintPath>..\7dtd-binaries\Assembly-CSharp.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-    <Reference Include="mscorlib">
-      <HintPath>..\7dtd-binaries\mscorlib.dll</HintPath>
-      <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="UnityEngine.ImageConversionModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
-      <HintPath>..\7dtd-binaries\UnityEngine.ImageConversionModule.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-  </ItemGroup>
-  <ItemGroup>
-    <Compile Include="AssemblyInfo.cs" />
-    <Compile Include="Commands\Exception.cs" />
-    <Compile Include="MapRendering\MapRendering.cs" />
-    <Compile Include="MapRendering\MapRenderBlockBuffer.cs" />
-    <Compile Include="MapRendering\Constants.cs" />
-    <Compile Include="Commands\RenderMap.cs" />
-    <Compile Include="Commands\EnableRendering.cs" />
-    <Compile Include="API.cs" />
-    <Compile Include="Web\API\AbsRestApi.cs" />
-    <Compile Include="Web\API\GetAnimalsLocation.cs" />
-    <Compile Include="Web\API\GetHostileLocation.cs" />
-    <Compile Include="Web\API\GetWebMods.cs" />
-    <Compile Include="Web\API\Null.cs" />
-    <Compile Include="Web\Handlers\RewriteHandler.cs" />
-    <Compile Include="Web\RequestContext.cs" />
-    <Compile Include="Web\SSE\EventLog.cs" />
-    <Compile Include="Web\SSE\SseHandler.cs" />
-    <Compile Include="Web\SSE\EventBase.cs" />
-    <Compile Include="Web\Web.cs" />
-    <Compile Include="Web\MimeType.cs" />
-    <Compile Include="Web\API\GetPlayersOnline.cs" />
-    <Compile Include="Web\API\AbsWebAPI.cs" />
-    <Compile Include="Web\API\GetPlayersLocation.cs" />
-    <Compile Include="Web\API\GetPlayerInventory.cs" />
-    <Compile Include="Web\API\GetLandClaims.cs" />
-    <Compile Include="Commands\webstat.cs" />
-    <Compile Include="Web\API\GetStats.cs" />
-    <Compile Include="Web\WebConnection.cs" />
-    <Compile Include="Web\OpenID.cs" />
-    <Compile Include="Web\ConnectionHandler.cs" />
-    <Compile Include="Web\WebMod.cs" />
-    <Compile Include="Web\WebPermissions.cs" />
-    <Compile Include="Web\Handlers\ApiHandler.cs" />
-    <Compile Include="Web\Handlers\ItemIconHandler.cs" />
-    <Compile Include="Web\Handlers\AbsHandler.cs" />
-    <Compile Include="Web\Handlers\SimpleRedirectHandler.cs" />
-    <Compile Include="Web\Handlers\StaticHandler.cs" />
-    <Compile Include="Web\Handlers\SessionHandler.cs" />
-    <Compile Include="Web\API\ExecuteConsoleCommand.cs" />
-    <Compile Include="Commands\ReloadWebPermissions.cs" />
-    <Compile Include="Web\Handlers\UserStatusHandler.cs" />
-    <Compile Include="Commands\WebTokens.cs" />
-    <Compile Include="Commands\WebPermissionsCmd.cs" />
-    <Compile Include="Web\LogBuffer.cs" />
-    <Compile Include="Web\API\GetLog.cs" />
-    <Compile Include="Web\API\GetWebUIUpdates.cs" />
-    <Compile Include="Web\API\GetServerInfo.cs" />
-    <Compile Include="Web\API\GetPlayerList.cs" />
-    <Compile Include="Web\WebCommandResult.cs" />
-    <Compile Include="Web\API\GetAllowedCommands.cs" />
-    <Compile Include="Commands\EnableOpenIDDebug.cs" />
-    <Compile Include="Web\API\GetPlayerInventories.cs" />
-    <Compile Include="Web\WebUtils.cs" />
-  </ItemGroup>
-  <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>
-    <ProjectReference Include="..\SpaceWizards.HttpListener\SpaceWizards.HttpListener.csproj">
-      <Project>{e273d042-57f9-4e2e-8268-5053527e5287}</Project>
-      <Name>SpaceWizards.HttpListener</Name>
-    </ProjectReference>
-  </ItemGroup>
-  <ItemGroup>
-    <None Include="ModInfo.xml">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="steam-intermediate.cer">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="steam-rootca.cer">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-  </ItemGroup>
-</Project>
Index: binary-improvements2/MapRendering/src/AssemblyInfo.cs
===================================================================
--- binary-improvements2/MapRendering/src/AssemblyInfo.cs	(revision 391)
+++ binary-improvements2/MapRendering/src/AssemblyInfo.cs	(revision 391)
@@ -0,0 +1,25 @@
+using System.Reflection;
+
+// Information about this assembly is defined by the following attributes. 
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle ("MapRendering")]
+[assembly: AssemblyDescription ("")]
+[assembly: AssemblyConfiguration ("")]
+[assembly: AssemblyCompany ("The Fun Pimps LLC")]
+[assembly: AssemblyProduct ("")]
+[assembly: AssemblyCopyright ("The Fun Pimps LLC")]
+[assembly: AssemblyTrademark ("")]
+[assembly: AssemblyCulture ("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion ("0.0.0.0")]
+
+// The following attributes are used to specify the signing key for the assembly, 
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
Index: binary-improvements2/MapRendering/src/Commands/EnableRendering.cs
===================================================================
--- binary-improvements2/MapRendering/src/Commands/EnableRendering.cs	(revision 391)
+++ binary-improvements2/MapRendering/src/Commands/EnableRendering.cs	(revision 391)
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace MapRendering.Commands {
+	[UsedImplicitly]
+	public class EnableRendering : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "enable/disable live map rendering";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"enablerendering"};
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count != 1) {
+				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"));
+		}
+	}
+}
Index: binary-improvements2/MapRendering/src/Commands/RenderMap.cs
===================================================================
--- binary-improvements2/MapRendering/src/Commands/RenderMap.cs	(revision 391)
+++ binary-improvements2/MapRendering/src/Commands/RenderMap.cs	(revision 391)
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace MapRendering.Commands {
+	[UsedImplicitly]
+	public class RenderMap : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "render the current map to a file";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"rendermap"};
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			MapRenderer.Instance.RenderFullMap ();
+
+			SdtdConsole.Instance.Output ("Render map done");
+		}
+	}
+}
Index: binary-improvements2/MapRendering/src/Constants.cs
===================================================================
--- binary-improvements2/MapRendering/src/Constants.cs	(revision 391)
+++ binary-improvements2/MapRendering/src/Constants.cs	(revision 391)
@@ -0,0 +1,16 @@
+using UnityEngine;
+
+namespace MapRendering {
+	public static class Constants {
+		public static readonly TextureFormat DefaultTextureFormat = TextureFormat.ARGB32;
+		public static int MapBlockSize = 128;
+		public const int MapChunkSize = 16;
+		public const int MapRegionSize = 512;
+		public static int Zoomlevels = 5;
+		public static string MapDirectory = string.Empty;
+
+		public static int MAP_BLOCK_TO_CHUNK_DIV => MapBlockSize / MapChunkSize;
+
+		public static int MAP_REGION_TO_CHUNK_DIV => MapRegionSize / MapChunkSize;
+	}
+}
Index: binary-improvements2/MapRendering/src/MapRenderBlockBuffer.cs
===================================================================
--- binary-improvements2/MapRendering/src/MapRenderBlockBuffer.cs	(revision 391)
+++ binary-improvements2/MapRendering/src/MapRenderBlockBuffer.cs	(revision 391)
@@ -0,0 +1,230 @@
+using System;
+using System.IO;
+using AllocsFixes.FileCache;
+using Unity.Collections;
+using UnityEngine;
+using UnityEngine.Profiling;
+
+namespace MapRendering {
+	public class MapRenderBlockBuffer {
+		private readonly Texture2D blockMap = new Texture2D (Constants.MapBlockSize, Constants.MapBlockSize, Constants.DefaultTextureFormat, false);
+		private readonly MapTileCache cache;
+		private readonly NativeArray<int> emptyImageData;
+		private readonly Texture2D zoomBuffer = new Texture2D (Constants.MapBlockSize / 2, Constants.MapBlockSize / 2, Constants.DefaultTextureFormat, false);
+		private readonly int zoomLevel;
+		private readonly string folderBase;
+		
+		private Vector2i currentBlockMapPos = new Vector2i (int.MinValue, int.MinValue);
+		private string currentBlockMapFolder = string.Empty;
+
+		public MapRenderBlockBuffer (int _level, MapTileCache _cache) {
+			zoomLevel = _level;
+			cache = _cache;
+			folderBase = Constants.MapDirectory + "/" + zoomLevel + "/";
+
+			{
+				// Initialize empty tile data
+				Color nullColor = new Color (0, 0, 0, 0);
+				for (int x = 0; x < Constants.MapBlockSize; x++) {
+					for (int y = 0; y < Constants.MapBlockSize; y++) {
+						blockMap.SetPixel (x, y, nullColor);
+					}
+				}
+
+				NativeArray<int> blockMapData = blockMap.GetRawTextureData<int> ();
+				emptyImageData = new NativeArray<int> (blockMapData.Length, Allocator.Persistent,
+					NativeArrayOptions.UninitializedMemory);
+				blockMapData.CopyTo (emptyImageData);
+			}
+		}
+
+		public TextureFormat FormatSelf => blockMap.format;
+
+		public void ResetBlock () {
+			currentBlockMapFolder = string.Empty;
+			currentBlockMapPos = new Vector2i (int.MinValue, int.MinValue);
+			cache.ResetTile (zoomLevel);
+		}
+
+		public void SaveBlock () {
+			Profiler.BeginSample ("SaveBlock");
+			try {
+				saveTextureToFile ();
+			} catch (Exception e) {
+				Log.Warning ("Exception in MapRenderBlockBuffer.SaveBlock(): " + e);
+			}
+			Profiler.EndSample ();
+		}
+
+		public bool LoadBlock (Vector2i _block) {
+			Profiler.BeginSample ("LoadBlock");
+			lock (blockMap) {
+				if (currentBlockMapPos != _block) {
+					Profiler.BeginSample ("LoadBlock.Strings");
+					string folder;
+					if (currentBlockMapPos.x != _block.x) {
+						folder = folderBase + _block.x + '/';
+
+						Profiler.BeginSample ("LoadBlock.Directory");
+						Directory.CreateDirectory (folder);
+						Profiler.EndSample ();
+					} else {
+						folder = currentBlockMapFolder;
+					}
+
+					string fileName = folder + _block.y + ".png";
+					Profiler.EndSample ();
+					
+					SaveBlock ();
+					loadTextureFromFile (fileName);
+
+					currentBlockMapFolder = folder;
+					currentBlockMapPos = _block;
+
+					Profiler.EndSample ();
+					return true;
+				}
+			}
+
+			Profiler.EndSample ();
+			return false;
+		}
+
+		public void SetPart (Vector2i _offset, int _partSize, Color32[] _pixels) {
+			if (_offset.x + _partSize > Constants.MapBlockSize || _offset.y + _partSize > Constants.MapBlockSize) {
+				Log.Error (
+					$"MapBlockBuffer[{zoomLevel}].SetPart ({_offset}, {_partSize}, {_pixels.Length}) has blockMap.size ({Constants.MapBlockSize}/{Constants.MapBlockSize})");
+				return;
+			}
+
+			Profiler.BeginSample ("SetPart");
+			blockMap.SetPixels32 (_offset.x, _offset.y, _partSize, _partSize, _pixels);
+			Profiler.EndSample ();
+		}
+
+		public Color32[] GetHalfScaled () {
+			Profiler.BeginSample ("HalfScaled.ResizeBuffer");
+			zoomBuffer.Resize (Constants.MapBlockSize, Constants.MapBlockSize);
+			Profiler.EndSample ();
+
+			Profiler.BeginSample ("HalfScaled.CopyPixels");
+			if (blockMap.format == zoomBuffer.format) {
+				Profiler.BeginSample ("Native");
+				NativeArray<byte> dataSrc = blockMap.GetRawTextureData<byte> ();
+				NativeArray<byte> dataZoom = zoomBuffer.GetRawTextureData<byte> ();
+				dataSrc.CopyTo (dataZoom);
+				Profiler.EndSample ();
+			} else {
+				Profiler.BeginSample ("GetSetPixels");
+				zoomBuffer.SetPixels32 (blockMap.GetPixels32 ());
+				Profiler.EndSample ();
+			}
+			Profiler.EndSample ();
+
+			Profiler.BeginSample ("HalfScaled.Scale");
+			TextureScale.Point (zoomBuffer, Constants.MapBlockSize / 2, Constants.MapBlockSize / 2);
+			Profiler.EndSample ();
+
+			Profiler.BeginSample ("HalfScaled.Return");
+			Color32[] result = zoomBuffer.GetPixels32 ();
+			Profiler.EndSample ();
+
+			return result;
+		}
+
+		public void SetPartNative (Vector2i _offset, int _partSize, NativeArray<int> _pixels) {
+			if (_offset.x + _partSize > Constants.MapBlockSize || _offset.y + _partSize > Constants.MapBlockSize) {
+				Log.Error (
+					$"MapBlockBuffer[{zoomLevel}].SetPart ({_offset}, {_partSize}, {_pixels.Length}) has blockMap.size ({Constants.MapBlockSize}/{Constants.MapBlockSize})");
+				return;
+			}
+
+			Profiler.BeginSample ("SetPartNative");
+			NativeArray<int> destData = blockMap.GetRawTextureData<int> ();
+			
+			for (int y = 0; y < _partSize; y++) {
+				int srcLineStartIdx = _partSize * y;
+				int destLineStartIdx = blockMap.width * (_offset.y + y) + _offset.x;
+				for (int x = 0; x < _partSize; x++) {
+					destData [destLineStartIdx + x] = _pixels [srcLineStartIdx + x];
+				}
+			}
+			Profiler.EndSample ();
+		}
+
+		public NativeArray<int> GetHalfScaledNative () {
+			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);
+			}
+			Profiler.EndSample ();
+
+			Profiler.BeginSample ("HalfScaledNative.Scale");
+			ScaleNative (blockMap, zoomBuffer);
+			Profiler.EndSample ();
+
+			return zoomBuffer.GetRawTextureData<int> ();
+		}
+		
+		private static void ScaleNative (Texture2D _sourceTex, Texture2D _targetTex) {
+			NativeArray<int> srcData = _sourceTex.GetRawTextureData<int> ();
+			NativeArray<int> targetData = _targetTex.GetRawTextureData<int> ();
+			
+			int oldWidth = _sourceTex.width;
+			int oldHeight = _sourceTex.height;
+			int newWidth = _targetTex.width;
+			int newHeight = _targetTex.height;
+			
+			float ratioX = (float) oldWidth / newWidth;
+			float ratioY = (float) oldHeight / newHeight;
+
+			for (int y = 0; y < newHeight; y++) {
+				int oldLineStart = (int) (ratioY * y) * oldWidth;
+				int newLineStart = y * newWidth;
+				for (int x = 0; x < newWidth; x++) {
+					targetData [newLineStart + x] = srcData [(int) (oldLineStart + ratioX * x)];
+				}
+			}
+		}
+
+		private void loadTextureFromFile (string _fileName) {
+			Profiler.BeginSample ("LoadTexture");
+
+			Profiler.BeginSample ("LoadFile");
+			byte[] array = cache.LoadTile (zoomLevel, _fileName);
+			Profiler.EndSample ();
+
+			Profiler.BeginSample ("LoadImage");
+			if (array != null && blockMap.LoadImage (array) && blockMap.height == Constants.MapBlockSize &&
+			    blockMap.width == Constants.MapBlockSize) {
+				Profiler.EndSample ();
+
+				Profiler.EndSample ();
+				return;
+			}
+			Profiler.EndSample ();
+
+			if (array != null) {
+				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,
+					false);
+			}
+
+			blockMap.LoadRawTextureData (emptyImageData);
+
+			Profiler.EndSample ();
+		}
+
+		private void saveTextureToFile () {
+			Profiler.BeginSample ("EncodePNG");
+			byte[] array = blockMap.EncodeToPNG ();
+			Profiler.EndSample ();
+
+			cache.SaveTile (zoomLevel, array);
+		}
+	}
+}
Index: binary-improvements2/MapRendering/src/MapRenderer.cs
===================================================================
--- binary-improvements2/MapRendering/src/MapRenderer.cs	(revision 391)
+++ binary-improvements2/MapRendering/src/MapRenderer.cs	(revision 391)
@@ -0,0 +1,407 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using AllocsFixes.FileCache;
+using AllocsFixes.JSON;
+using UnityEngine;
+using UnityEngine.Profiling;
+using Object = UnityEngine.Object;
+
+namespace MapRendering {
+	public class MapRenderer {
+		private static MapRenderer instance;
+
+		private static readonly object lockObject = new object ();
+		public static bool renderingEnabled = true;
+		private readonly MapTileCache cache = new MapTileCache (Constants.MapBlockSize);
+		private readonly Dictionary<Vector2i, Color32[]> dirtyChunks = new Dictionary<Vector2i, Color32[]> ();
+		private readonly MicroStopwatch msw = new MicroStopwatch ();
+		private readonly MapRenderBlockBuffer[] zoomLevelBuffers;
+		private Coroutine renderCoroutineRef;
+		private bool renderingFullMap;
+		private float renderTimeout = float.MaxValue;
+		private bool shutdown;
+
+		private MapRenderer () {
+			Constants.MapDirectory = GameIO.GetSaveGameDir () + "/map";
+
+			lock (lockObject) {
+				if (!LoadMapInfo ()) {
+					WriteMapInfo ();
+				}
+			}
+
+			cache.SetZoomCount (Constants.Zoomlevels);
+
+			zoomLevelBuffers = new MapRenderBlockBuffer[Constants.Zoomlevels];
+			for (int i = 0; i < Constants.Zoomlevels; i++) {
+				zoomLevelBuffers [i] = new MapRenderBlockBuffer (i, cache);
+			}
+
+			renderCoroutineRef = ThreadManager.StartCoroutine (renderCoroutine ());
+		}
+
+		public static MapRenderer Instance => instance ??= new MapRenderer ();
+
+		public static MapTileCache GetTileCache () {
+			return Instance.cache;
+		}
+
+		public static void Shutdown () {
+			if (Instance == null) {
+				return;
+			}
+
+			Instance.shutdown = true;
+			
+			if (Instance.renderCoroutineRef != null) {
+				ThreadManager.StopCoroutine (Instance.renderCoroutineRef);
+				Instance.renderCoroutineRef = null;
+			}
+		}
+
+		public static void RenderSingleChunk (Chunk _chunk) {
+			if (renderingEnabled && Instance != null) {
+				// TODO: Replace with regular thread and a blocking queue / set
+				ThreadPool.UnsafeQueueUserWorkItem (_o => {
+					try {
+						if (Instance.renderingFullMap) {
+							return;
+						}
+
+						lock (lockObject) {
+							Chunk c = (Chunk) _o;
+							Vector3i cPos = c.GetWorldPos ();
+							Vector2i cPos2 = new Vector2i (cPos.x / Constants.MapChunkSize,
+								cPos.z / Constants.MapChunkSize);
+
+							ushort[] mapColors = c.GetMapColors ();
+							if (mapColors == null) {
+								return;
+							}
+
+							Color32[] realColors =
+								new Color32[Constants.MapChunkSize * Constants.MapChunkSize];
+							for (int iColors = 0; iColors < mapColors.Length; iColors++) {
+								realColors [iColors] = shortColorToColor32 (mapColors [iColors]);
+							}
+
+							Instance.dirtyChunks [cPos2] = realColors;
+
+							//Log.Out ("Add Dirty: " + cPos2);
+						}
+					} catch (Exception e) {
+						Log.Out ("Exception in MapRendering.RenderSingleChunk(): " + e);
+					}
+				}, _chunk);
+			}
+		}
+
+		public void RenderFullMap () {
+			MicroStopwatch microStopwatch = new MicroStopwatch ();
+
+			string regionSaveDir = GameIO.GetSaveGameRegionDir ();
+			RegionFileManager rfm = new RegionFileManager (regionSaveDir, regionSaveDir, 0, false);
+			Texture2D fullMapTexture = null;
+
+			getWorldExtent (rfm, out Vector2i minChunk, out Vector2i maxChunk, out Vector2i minPos, out Vector2i maxPos, out int widthChunks, out int heightChunks,
+				out int widthPix, out int heightPix);
+
+			Log.Out (
+				$"RenderMap: min: {minChunk.ToString ()}, max: {maxChunk.ToString ()}, minPos: {minPos.ToString ()}, maxPos: {maxPos.ToString ()}, w/h: {widthChunks}/{heightChunks}, wP/hP: {widthPix}/{heightPix}"
+			);
+
+			lock (lockObject) {
+				for (int i = 0; i < Constants.Zoomlevels; i++) {
+					zoomLevelBuffers [i].ResetBlock ();
+				}
+
+				if (Directory.Exists (Constants.MapDirectory)) {
+					Directory.Delete (Constants.MapDirectory, true);
+				}
+
+				WriteMapInfo ();
+
+				renderingFullMap = true;
+
+				if (widthPix <= 8192 && heightPix <= 8192) {
+					fullMapTexture = new Texture2D (widthPix, heightPix);
+				}
+
+				Vector2i curFullMapPos = default;
+				Vector2i curChunkPos = default;
+				for (curFullMapPos.x = 0; curFullMapPos.x < widthPix; curFullMapPos.x += Constants.MapChunkSize) {
+					for (curFullMapPos.y = 0;
+						curFullMapPos.y < heightPix;
+						curFullMapPos.y += Constants.MapChunkSize) {
+						curChunkPos.x = curFullMapPos.x / Constants.MapChunkSize + minChunk.x;
+						curChunkPos.y = curFullMapPos.y / Constants.MapChunkSize + minChunk.y;
+
+						try {
+							long chunkKey = WorldChunkCache.MakeChunkKey (curChunkPos.x, curChunkPos.y);
+							if (rfm.ContainsChunkSync (chunkKey)) {
+								Chunk c = rfm.GetChunkSync (chunkKey);
+								ushort[] mapColors = c.GetMapColors ();
+								if (mapColors != null) {
+									Color32[] realColors =
+										new Color32[Constants.MapChunkSize * Constants.MapChunkSize];
+									for (int iColors = 0; iColors < mapColors.Length; iColors++) {
+										realColors [iColors] = shortColorToColor32 (mapColors [iColors]);
+									}
+
+									dirtyChunks [curChunkPos] = realColors;
+									if (fullMapTexture != null) {
+										fullMapTexture.SetPixels32 (curFullMapPos.x, curFullMapPos.y,
+											Constants.MapChunkSize, Constants.MapChunkSize, realColors);
+									}
+								}
+							}
+						} catch (Exception e) {
+							Log.Out ("Exception: " + e);
+						}
+					}
+
+					while (dirtyChunks.Count > 0) {
+						RenderDirtyChunks ();
+					}
+
+					Log.Out ($"RenderMap: {curFullMapPos.x}/{widthPix} ({(int)((float)curFullMapPos.x / widthPix * 100)}%)");
+				}
+			}
+			
+			rfm.Cleanup ();
+
+			if (fullMapTexture != null) {
+				byte[] array = fullMapTexture.EncodeToPNG ();
+				File.WriteAllBytes (Constants.MapDirectory + "/map.png", array);
+				Object.Destroy (fullMapTexture);
+			}
+
+			renderingFullMap = false;
+
+			Log.Out ("Generating map took: " + microStopwatch.ElapsedMilliseconds + " ms");
+			Log.Out ("World extent: " + minPos + " - " + maxPos);
+		}
+
+		private void SaveAllBlockMaps () {
+			for (int i = 0; i < Constants.Zoomlevels; i++) {
+				zoomLevelBuffers [i].SaveBlock ();
+			}
+		}
+		
+		private readonly WaitForSeconds coroutineDelay = new WaitForSeconds (0.2f);
+
+		private IEnumerator renderCoroutine () {
+			while (!shutdown) {
+				lock (lockObject) {
+					if (dirtyChunks.Count > 0 && renderTimeout >= float.MaxValue / 2) {
+						renderTimeout = Time.time + 0.5f;
+					}
+
+					if (Time.time > renderTimeout || dirtyChunks.Count > 200) {
+						Profiler.BeginSample ("RenderDirtyChunks");
+						RenderDirtyChunks ();
+						Profiler.EndSample ();
+					}
+				}
+
+				yield return coroutineDelay;
+			}
+		}
+
+		private readonly List<Vector2i> chunksToRender = new List<Vector2i> ();
+		private readonly List<Vector2i> chunksRendered = new List<Vector2i> ();
+
+		private void RenderDirtyChunks () {
+			msw.ResetAndRestart ();
+
+			if (dirtyChunks.Count <= 0) {
+				return;
+			}
+
+			Profiler.BeginSample ("RenderDirtyChunks.Prepare");
+			chunksToRender.Clear ();
+			chunksRendered.Clear ();
+
+			dirtyChunks.CopyKeysTo (chunksToRender);
+
+			Vector2i chunkPos = chunksToRender [0];
+			chunksRendered.Add (chunkPos);
+
+			//Log.Out ("Start Dirty: " + chunkPos);
+
+			getBlockNumber (chunkPos, out Vector2i block, out _, Constants.MAP_BLOCK_TO_CHUNK_DIV, Constants.MapChunkSize);
+
+			zoomLevelBuffers [Constants.Zoomlevels - 1].LoadBlock (block);
+			Profiler.EndSample ();
+
+			Profiler.BeginSample ("RenderDirtyChunks.Work");
+			// Write all chunks that are in the same image tile of the highest zoom level 
+			foreach (Vector2i v in chunksToRender) {
+				getBlockNumber (v, out Vector2i vBlock, out Vector2i vBlockOffset, Constants.MAP_BLOCK_TO_CHUNK_DIV,
+					Constants.MapChunkSize);
+				if (!vBlock.Equals (block)) {
+					continue;
+				}
+
+				//Log.Out ("Dirty: " + v + " render: true");
+				chunksRendered.Add (v);
+				if (dirtyChunks [v].Length != Constants.MapChunkSize * Constants.MapChunkSize) {
+					Log.Error (
+						$"Rendering chunk has incorrect data size of {dirtyChunks [v].Length} instead of {Constants.MapChunkSize * Constants.MapChunkSize}");
+				}
+
+				zoomLevelBuffers [Constants.Zoomlevels - 1]
+					.SetPart (vBlockOffset, Constants.MapChunkSize, dirtyChunks [v]);
+			}
+			Profiler.EndSample ();
+
+			foreach (Vector2i v in chunksRendered) {
+				dirtyChunks.Remove (v);
+			}
+
+			// Update lower zoom levels affected by the change of the highest one
+			RenderZoomLevel (block);
+
+			Profiler.BeginSample ("RenderDirtyChunks.SaveAll");
+			SaveAllBlockMaps ();
+			Profiler.EndSample ();
+		}
+
+		private void RenderZoomLevel (Vector2i _innerBlock) {
+			Profiler.BeginSample ("RenderZoomLevel");
+			int level = Constants.Zoomlevels - 1;
+			while (level > 0) {
+				getBlockNumber (_innerBlock, out Vector2i block, out Vector2i blockOffset, 2, Constants.MapBlockSize / 2);
+
+				zoomLevelBuffers [level - 1].LoadBlock (block);
+
+				Profiler.BeginSample ("RenderZoomLevel.Transfer");
+				if ((zoomLevelBuffers [level].FormatSelf == TextureFormat.ARGB32 ||
+				     zoomLevelBuffers [level].FormatSelf == TextureFormat.RGBA32) &&
+				    zoomLevelBuffers [level].FormatSelf == zoomLevelBuffers [level - 1].FormatSelf) {
+					zoomLevelBuffers [level - 1].SetPartNative (blockOffset, Constants.MapBlockSize / 2, zoomLevelBuffers [level].GetHalfScaledNative ());
+				} else {
+					zoomLevelBuffers [level - 1].SetPart (blockOffset, Constants.MapBlockSize / 2, zoomLevelBuffers [level].GetHalfScaled ());
+				}
+				Profiler.EndSample ();
+
+				level--;
+				_innerBlock = block;
+			}
+			Profiler.EndSample ();
+		}
+
+		private void getBlockNumber (Vector2i _innerPos, out Vector2i _block, out Vector2i _blockOffset, int _scaleFactor,
+			int _offsetSize) {
+			_block = default;
+			_blockOffset = default;
+			_block.x = (_innerPos.x + 16777216) / _scaleFactor - 16777216 / _scaleFactor;
+			_block.y = (_innerPos.y + 16777216) / _scaleFactor - 16777216 / _scaleFactor;
+			_blockOffset.x = (_innerPos.x + 16777216) % _scaleFactor * _offsetSize;
+			_blockOffset.y = (_innerPos.y + 16777216) % _scaleFactor * _offsetSize;
+		}
+
+		private void WriteMapInfo () {
+			JsonObject mapInfo = new JsonObject ();
+			mapInfo.Add ("blockSize", new JsonNumber (Constants.MapBlockSize));
+			mapInfo.Add ("maxZoom", new JsonNumber (Constants.Zoomlevels - 1));
+
+			Directory.CreateDirectory (Constants.MapDirectory);
+			File.WriteAllText (Constants.MapDirectory + "/mapinfo.json", mapInfo.ToString (), Encoding.UTF8);
+		}
+
+		private bool LoadMapInfo () {
+			if (!File.Exists (Constants.MapDirectory + "/mapinfo.json")) {
+				return false;
+			}
+
+			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);
+			}
+
+			return false;
+		}
+
+		private void getWorldExtent (RegionFileManager _rfm, out Vector2i _minChunk, out Vector2i _maxChunk,
+			out Vector2i _minPos, out Vector2i _maxPos,
+			out int _widthChunks, out int _heightChunks,
+			out int _widthPix, out int _heightPix) {
+			_minChunk = default;
+			_maxChunk = default;
+			_minPos = default;
+			_maxPos = default;
+
+			long[] keys = _rfm.GetAllChunkKeys ();
+			int minX = int.MaxValue;
+			int minY = int.MaxValue;
+			int maxX = int.MinValue;
+			int maxY = int.MinValue;
+			foreach (long key in keys) {
+				int x = WorldChunkCache.extractX (key);
+				int y = WorldChunkCache.extractZ (key);
+
+				if (x < minX) {
+					minX = x;
+				}
+
+				if (x > maxX) {
+					maxX = x;
+				}
+
+				if (y < minY) {
+					minY = y;
+				}
+
+				if (y > maxY) {
+					maxY = y;
+				}
+			}
+
+			_minChunk.x = minX;
+			_minChunk.y = minY;
+
+			_maxChunk.x = maxX;
+			_maxChunk.y = maxY;
+
+			_minPos.x = minX * Constants.MapChunkSize;
+			_minPos.y = minY * Constants.MapChunkSize;
+
+			_maxPos.x = maxX * Constants.MapChunkSize;
+			_maxPos.y = maxY * Constants.MapChunkSize;
+
+			_widthChunks = maxX - minX + 1;
+			_heightChunks = maxY - minY + 1;
+
+			_widthPix = _widthChunks * Constants.MapChunkSize;
+			_heightPix = _heightChunks * Constants.MapChunkSize;
+		}
+
+		private static Color32 shortColorToColor32 (ushort _col) {
+			byte r = (byte) (256 * ((_col >> 10) & 31) / 32);
+			byte g = (byte) (256 * ((_col >> 5) & 31) / 32);
+			byte b = (byte) (256 * (_col & 31) / 32);
+			const byte a = 255;
+			return new Color32 (r, g, b, a);
+		}
+	}
+}
Index: binary-improvements2/MapRendering/src/ModApi.cs
===================================================================
--- binary-improvements2/MapRendering/src/ModApi.cs	(revision 391)
+++ binary-improvements2/MapRendering/src/ModApi.cs	(revision 391)
@@ -0,0 +1,19 @@
+using JetBrains.Annotations;
+
+namespace MapRendering {
+	[UsedImplicitly]
+	public class ModApi : IModApi {
+		public void InitMod (Mod _modInstance) {
+			ModEvents.GameShutdown.RegisterHandler (GameShutdown);
+			ModEvents.CalcChunkColorsDone.RegisterHandler (CalcChunkColorsDone);
+		}
+
+		private void GameShutdown () {
+			MapRenderer.Shutdown ();
+		}
+
+		private void CalcChunkColorsDone (Chunk _chunk) {
+			MapRenderer.RenderSingleChunk (_chunk);
+		}
+	}
+}
Index: binary-improvements2/MarkersMod/API.cs
===================================================================
--- binary-improvements2/MarkersMod/API.cs	(revision 390)
+++ 	(revision )
@@ -1,6 +1,0 @@
-namespace Examples {
-	public class API : IModApi {
-		public void InitMod (Mod _modInstance) {
-		}
-	}
-}
Index: binary-improvements2/MarkersMod/AssemblyInfo.cs
===================================================================
--- binary-improvements2/MarkersMod/AssemblyInfo.cs	(revision 390)
+++ 	(revision )
@@ -1,25 +1,0 @@
-using System.Reflection;
-
-// Information about this assembly is defined by the following attributes. 
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle ("MarkersMod")]
-[assembly: AssemblyDescription ("")]
-[assembly: AssemblyConfiguration ("")]
-[assembly: AssemblyCompany ("")]
-[assembly: AssemblyProduct ("")]
-[assembly: AssemblyCopyright ("")]
-[assembly: AssemblyTrademark ("")]
-[assembly: AssemblyCulture ("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion ("0.0.0.0")]
-
-// The following attributes are used to specify the signing key for the assembly, 
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
Index: binary-improvements2/MarkersMod/Markers.cs
===================================================================
--- binary-improvements2/MarkersMod/Markers.cs	(revision 390)
+++ 	(revision )
@@ -1,121 +1,0 @@
-using System.Collections.Generic;
-using System.Net;
-using AllocsFixes.JSON;
-using AllocsFixes.NetConnections.Servers.Web;
-using AllocsFixes.NetConnections.Servers.Web.API;
-
-namespace Examples {
-	class Markers : AbsRestApi {
-		private const int NumRandomMarkers = 5;
-
-		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 ();
-			
-			for (int i = 0; i < NumRandomMarkers; i++) {
-				int lat = random.RandomRange (-1000, 1001);
-				int lng = random.RandomRange (-1000, 1001);
-
-				markers.Add (WebUtils.GenerateGuid (), (lat, lng));
-			}
-		}
-
-		protected override void HandleRestGet (RequestContext _context) {
-			string id = _context.RequestPath;
-			
-			if (string.IsNullOrEmpty (id)) {
-				JSONArray result = new JSONArray ();
-
-				foreach (KeyValuePair<string, (int, int)> kvp in markers) {
-					JSONObject marker = new JSONObject ();
-					marker.Add ("id", new JSONString (kvp.Key));
-					marker.Add ("lat", new JSONNumber (kvp.Value.Item1));
-					marker.Add ("lng", new JSONNumber (kvp.Value.Item2));
-					result.Add (marker);
-				}
-				
-				SendEnvelopedResult (_context, result);
-				return;
-			}
-
-			if (!markers.TryGetValue (id, out (int, int) location)) {
-				SendEnvelopedResult (_context, emptyResult, 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);
-			}
-		}
-
-		protected override void HandleRestPost (RequestContext _context, JSONNode _jsonBody) {
-			if (!(_jsonBody is JSONObject bodyObject)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "BODY_NOT_OBJECT");
-				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");
-				return;
-			}
-
-			string newId = WebUtils.GenerateGuid ();
-			markers.Add (newId, (lat, lng));
-
-			JSONString result = new JSONString (newId);
-			SendEnvelopedResult (_context, result);
-		}
-
-		protected override void HandleRestPut (RequestContext _context, JSONNode _jsonBody) {
-			if (!(_jsonBody is JSONObject bodyObject)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "BODY_NOT_OBJECT");
-				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");
-				return;
-			}
-
-			string id = _context.RequestPath;
-
-			if (!markers.TryGetValue (id, out (int, int) location)) {
-				SendEnvelopedResult (_context, null, HttpStatusCode.NotFound, _jsonBody, "ID_NOT_FOUND");
-				return;
-			}
-			
-			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);
-		}
-
-		protected override void HandleRestDelete (RequestContext _context) {
-			string id = _context.RequestPath;
-
-			SendEnvelopedResult (_context, null, markers.Remove (id) ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
-		}
-	}
-}
Index: binary-improvements2/MarkersMod/MarkersMod.csproj
===================================================================
--- binary-improvements2/MarkersMod/MarkersMod.csproj	(revision 390)
+++ binary-improvements2/MarkersMod/MarkersMod.csproj	(revision 391)
@@ -8,5 +8,5 @@
     <ProjectGuid>{2A008E16-6EB8-4B85-A175-3CB89D9FF4AE}</ProjectGuid>
     <OutputType>Library</OutputType>
-    <RootNamespace>MarkersMod</RootNamespace>
+    <RootNamespace>Examples</RootNamespace>
     <AssemblyName>MarkersMod</AssemblyName>
     <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
@@ -16,5 +16,5 @@
     <DebugType>none</DebugType>
     <Optimize>true</Optimize>
-    <OutputPath>..\bin\Mods\MarkersMod\</OutputPath>
+    <OutputPath>..\bin\Mods\Xample_MarkersMod\</OutputPath>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
@@ -23,5 +23,5 @@
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_Profiler|AnyCPU' ">
-    <OutputPath>..\bin\Mods\MarkersMod\</OutputPath>
+    <OutputPath>..\bin\Mods\Xample_MarkersMod\</OutputPath>
     <DefineConstants>ENABLE_PROFILER</DefineConstants>
     <Optimize>true</Optimize>
@@ -32,5 +32,5 @@
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <OutputPath>..\bin\Mods\MarkersMod\</OutputPath>
+    <OutputPath>..\bin\Mods\Xample_MarkersMod\</OutputPath>
     <DebugType>full</DebugType>
     <DebugSymbols>true</DebugSymbols>
@@ -55,7 +55,7 @@
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="AssemblyInfo.cs" />
-    <Compile Include="API.cs" />
-    <Compile Include="Markers.cs" />
+    <Compile Include="src\ModApi.cs" />
+    <Compile Include="src\AssemblyInfo.cs" />
+    <Compile Include="src\Markers.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
@@ -71,7 +71,7 @@
       <Private>False</Private>
     </ProjectReference>
-    <ProjectReference Include="..\MapRendering\WebAndMapRendering.csproj">
-      <Project>{a1847b5f-7bfc-4bcd-94aa-a6c9fb7e7c54}</Project>
-      <Name>WebAndMapRendering</Name>
+    <ProjectReference Include="..\WebServer\WebServer.csproj">
+      <Project>{01b5f981-b9fd-4364-8f9e-9127130d2542}</Project>
+      <Name>WebServer</Name>
       <Private>False</Private>
     </ProjectReference>
Index: binary-improvements2/MarkersMod/src/AssemblyInfo.cs
===================================================================
--- binary-improvements2/MarkersMod/src/AssemblyInfo.cs	(revision 391)
+++ binary-improvements2/MarkersMod/src/AssemblyInfo.cs	(revision 391)
@@ -0,0 +1,25 @@
+using System.Reflection;
+
+// Information about this assembly is defined by the following attributes. 
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle ("MarkersMod")]
+[assembly: AssemblyDescription ("")]
+[assembly: AssemblyConfiguration ("")]
+[assembly: AssemblyCompany ("The Fun Pimps LLC")]
+[assembly: AssemblyProduct ("")]
+[assembly: AssemblyCopyright ("The Fun Pimps LLC")]
+[assembly: AssemblyTrademark ("")]
+[assembly: AssemblyCulture ("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion ("0.0.0.0")]
+
+// The following attributes are used to specify the signing key for the assembly, 
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
Index: binary-improvements2/MarkersMod/src/Markers.cs
===================================================================
--- binary-improvements2/MarkersMod/src/Markers.cs	(revision 391)
+++ binary-improvements2/MarkersMod/src/Markers.cs	(revision 391)
@@ -0,0 +1,121 @@
+using System.Collections.Generic;
+using System.Net;
+using AllocsFixes.JSON;
+using Webserver;
+using Webserver.WebAPI;
+
+namespace Examples {
+	public class Markers : AbsRestApi {
+		private const int numRandomMarkers = 5;
+
+		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 ();
+			
+			for (int i = 0; i < numRandomMarkers; i++) {
+				int lat = random.RandomRange (-1000, 1001);
+				int lng = random.RandomRange (-1000, 1001);
+
+				markers.Add (WebUtils.GenerateGuid (), (lat, lng));
+			}
+		}
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+			
+			if (string.IsNullOrEmpty (id)) {
+				JsonArray result = new JsonArray ();
+
+				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);
+				}
+				
+				SendEnvelopedResult (_context, result);
+				return;
+			}
+
+			if (!markers.TryGetValue (id, out (int, int) location)) {
+				SendEnvelopedResult (_context, emptyResult, 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);
+			}
+		}
+
+		protected override void HandleRestPost (RequestContext _context, JsonNode _jsonBody) {
+			if (!(_jsonBody is JsonObject bodyObject)) {
+				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "BODY_NOT_OBJECT");
+				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");
+				return;
+			}
+
+			string newId = WebUtils.GenerateGuid ();
+			markers.Add (newId, (lat, lng));
+
+			JsonString result = new JsonString (newId);
+			SendEnvelopedResult (_context, result, HttpStatusCode.Created);
+		}
+
+		protected override void HandleRestPut (RequestContext _context, JsonNode _jsonBody) {
+			if (!(_jsonBody is JsonObject bodyObject)) {
+				SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, _jsonBody, "BODY_NOT_OBJECT");
+				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");
+				return;
+			}
+
+			string id = _context.RequestPath;
+
+			if (!markers.TryGetValue (id, out _)) {
+				SendEnvelopedResult (_context, null, HttpStatusCode.NotFound, _jsonBody, "ID_NOT_FOUND");
+				return;
+			}
+			
+			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);
+		}
+
+		protected override void HandleRestDelete (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			SendEnvelopedResult (_context, null, markers.Remove (id) ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+		}
+	}
+}
Index: binary-improvements2/MarkersMod/src/ModApi.cs
===================================================================
--- binary-improvements2/MarkersMod/src/ModApi.cs	(revision 391)
+++ binary-improvements2/MarkersMod/src/ModApi.cs	(revision 391)
@@ -0,0 +1,6 @@
+namespace Examples {
+	public class ModApi : IModApi {
+		public void InitMod (Mod _modInstance) {
+		}
+	}
+}
Index: binary-improvements2/SpaceWizards.HttpListener/SpaceWizards.HttpListener.csproj
===================================================================
--- binary-improvements2/SpaceWizards.HttpListener/SpaceWizards.HttpListener.csproj	(revision 390)
+++ binary-improvements2/SpaceWizards.HttpListener/SpaceWizards.HttpListener.csproj	(revision 391)
@@ -17,5 +17,5 @@
     <DebugType>none</DebugType>
     <Optimize>true</Optimize>
-    <OutputPath>..\bin\Mods\Allocs_WebAndMapRendering\</OutputPath>
+    <OutputPath>..\bin\Mods\TFP_WebServer\</OutputPath>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
@@ -26,5 +26,5 @@
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_Profiler|AnyCPU' ">
-    <OutputPath>..\bin\Mods\Allocs_WebAndMapRendering\</OutputPath>
+    <OutputPath>..\bin\Mods\TFP_WebServer\</OutputPath>
     <DefineConstants>ENABLE_PROFILER;UNITY_NETFRAMEWORK</DefineConstants>
     <Optimize>true</Optimize>
@@ -34,5 +34,5 @@
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <OutputPath>..\bin\Mods\Allocs_WebAndMapRendering\</OutputPath>
+    <OutputPath>..\bin\Mods\TFP_WebServer\</OutputPath>
     <DebugType>full</DebugType>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Index: binary-improvements2/WebServer/ModInfo.xml
===================================================================
--- binary-improvements2/WebServer/ModInfo.xml	(revision 391)
+++ binary-improvements2/WebServer/ModInfo.xml	(revision 391)
@@ -0,0 +1,10 @@
+<?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>
+</xml>
Index: binary-improvements2/WebServer/WebServer.csproj
===================================================================
--- binary-improvements2/WebServer/WebServer.csproj	(revision 391)
+++ binary-improvements2/WebServer/WebServer.csproj	(revision 391)
@@ -0,0 +1,157 @@
+﻿<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{01B5F981-B9FD-4364-8F9E-9127130D2542}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>Webserver</RootNamespace>
+    <AssemblyName>WebServer</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <LangVersion>8</LangVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\bin\Mods\TFP_WebServer\</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <NoStdLib>true</NoStdLib>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_Profiler|AnyCPU' ">
+    <OutputPath>..\bin\Mods\TFP_WebServer\</OutputPath>
+    <DefineConstants>ENABLE_PROFILER</DefineConstants>
+    <Optimize>true</Optimize>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <WarningLevel>4</WarningLevel>
+    <NoStdLib>true</NoStdLib>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <OutputPath>..\bin\Mods\TFP_WebServer\</OutputPath>
+    <DebugType>full</DebugType>
+    <DebugSymbols>true</DebugSymbols>
+  </PropertyGroup>
+  <ItemGroup>
+    <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="LogLibrary">
+      <HintPath>..\7dtd-binaries\LogLibrary.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+      <HintPath>..\7dtd-binaries\mscorlib.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+      <HintPath>..\7dtd-binaries\System.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+      <HintPath>..\7dtd-binaries\System.Xml.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="UnityEngine">
+      <HintPath>..\7dtd-binaries\UnityEngine.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="Assembly-CSharp">
+      <HintPath>..\7dtd-binaries\Assembly-CSharp.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="mscorlib">
+      <HintPath>..\7dtd-binaries\mscorlib.dll</HintPath>
+      <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="UnityEngine.ImageConversionModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>..\7dtd-binaries\UnityEngine.ImageConversionModule.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="src\ModApi.cs" />
+    <Compile Include="src\UrlHandlers\ApiHandler.cs" />
+    <Compile Include="src\UrlHandlers\SseHandler.cs" />
+    <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\Null.cs" />
+    <Compile Include="src\AssemblyInfo.cs" />
+    <Compile Include="src\Commands\EnableOpenIDDebug.cs" />
+    <Compile Include="src\Commands\ReloadWebPermissions.cs" />
+    <Compile Include="src\Commands\WebPermissionsCmd.cs" />
+    <Compile Include="src\Commands\WebTokens.cs" />
+    <Compile Include="src\ConnectionHandler.cs" />
+    <Compile Include="src\UrlHandlers\AbsHandler.cs" />
+    <Compile Include="src\UrlHandlers\ItemIconHandler.cs" />
+    <Compile Include="src\UrlHandlers\RewriteHandler.cs" />
+    <Compile Include="src\UrlHandlers\SessionHandler.cs" />
+    <Compile Include="src\UrlHandlers\SimpleRedirectHandler.cs" />
+    <Compile Include="src\UrlHandlers\StaticHandler.cs" />
+    <Compile Include="src\UrlHandlers\UserStatusHandler.cs" />
+    <Compile Include="src\LogBuffer.cs" />
+    <Compile Include="src\MimeType.cs" />
+    <Compile Include="src\OpenID.cs" />
+    <Compile Include="src\RequestContext.cs" />
+    <Compile Include="src\SSE\AbsEvent.cs" />
+    <Compile Include="src\SSE\EventLog.cs" />
+    <Compile Include="src\Web.cs" />
+    <Compile Include="src\WebCommandResult.cs" />
+    <Compile Include="src\WebConnection.cs" />
+    <Compile Include="src\WebMod.cs" />
+    <Compile Include="src\WebPermissions.cs" />
+    <Compile Include="src\WebUtils.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ItemGroup>
+    <None Include="ModInfo.xml">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+    <None Include="steam-intermediate.cer">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+    <None Include="steam-rootca.cer">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </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>
+      <Name>SpaceWizards.HttpListener</Name>
+      <Private>False</Private>
+    </ProjectReference>
+  </ItemGroup>
+</Project>
Index: binary-improvements2/WebServer/src/AssemblyInfo.cs
===================================================================
--- binary-improvements2/WebServer/src/AssemblyInfo.cs	(revision 391)
+++ binary-improvements2/WebServer/src/AssemblyInfo.cs	(revision 391)
@@ -0,0 +1,25 @@
+using System.Reflection;
+
+// Information about this assembly is defined by the following attributes. 
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle ("WebServer")]
+[assembly: AssemblyDescription ("")]
+[assembly: AssemblyConfiguration ("")]
+[assembly: AssemblyCompany ("The Fun Pimps LLC")]
+[assembly: AssemblyProduct ("")]
+[assembly: AssemblyCopyright ("The Fun Pimps LLC")]
+[assembly: AssemblyTrademark ("")]
+[assembly: AssemblyCulture ("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion ("0.0.0.0")]
+
+// The following attributes are used to specify the signing key for the assembly, 
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
Index: binary-improvements2/WebServer/src/Commands/EnableOpenIDDebug.cs
===================================================================
--- binary-improvements2/WebServer/src/Commands/EnableOpenIDDebug.cs	(revision 391)
+++ binary-improvements2/WebServer/src/Commands/EnableOpenIDDebug.cs	(revision 391)
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace Webserver.Commands {
+	[UsedImplicitly]
+	public class EnableOpenIDDebug : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "enable/disable OpenID debugging";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"openiddebug"};
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count != 1) {
+				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"));
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/Commands/ReloadWebPermissions.cs
===================================================================
--- binary-improvements2/WebServer/src/Commands/ReloadWebPermissions.cs	(revision 391)
+++ binary-improvements2/WebServer/src/Commands/ReloadWebPermissions.cs	(revision 391)
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace Webserver.Commands {
+	[UsedImplicitly]
+	public class ReloadWebPermissions : ConsoleCmdAbstract {
+		public override string GetDescription () {
+			return "force reload of web permissions file";
+		}
+
+		public override string[] GetCommands () {
+			return new[] {"reloadwebpermissions"};
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			WebPermissions.Instance.Load ();
+			SdtdConsole.Instance.Output ("Web permissions file reloaded");
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/Commands/WebPermissionsCmd.cs
===================================================================
--- binary-improvements2/WebServer/src/Commands/WebPermissionsCmd.cs	(revision 391)
+++ binary-improvements2/WebServer/src/Commands/WebPermissionsCmd.cs	(revision 391)
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace Webserver.Commands {
+	[UsedImplicitly]
+	public class WebPermissionsCmd : ConsoleCmdAbstract {
+		public override string[] GetCommands () {
+			return new[] {"webpermission"};
+		}
+
+		public override string GetDescription () {
+			return "Manage web permission levels";
+		}
+
+		public override string GetHelp () {
+			return "Set/get permission levels required to access a given web functionality. Default\n" +
+			       "level required for functions that are not explicitly specified is 0.\n" +
+			       "Usage:\n" +
+			       "   webpermission add <webfunction> <level>\n" +
+			       "   webpermission remove <webfunction>\n" +
+			       "   webpermission list";
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count >= 1) {
+				if (_params [0].EqualsCaseInsensitive ("add")) {
+					ExecuteAdd (_params);
+				} else if (_params [0].EqualsCaseInsensitive ("remove")) {
+					ExecuteRemove (_params);
+				} else if (_params [0].EqualsCaseInsensitive ("list")) {
+					ExecuteList ();
+				} else {
+					SdtdConsole.Instance.Output ($"Invalid sub command \"{_params [0]}\".");
+				}
+			} else {
+				SdtdConsole.Instance.Output ("No sub command given.");
+			}
+		}
+
+		private void ExecuteAdd (List<string> _params) {
+			if (_params.Count != 3) {
+				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 3, found {_params.Count}.");
+				return;
+			}
+
+			if (!WebPermissions.Instance.IsKnownModule (_params [1])) {
+				SdtdConsole.Instance.Output ($"\"{_params [1]}\" is not a valid web function.");
+				return;
+			}
+
+			if (!int.TryParse (_params [2], out int level)) {
+				SdtdConsole.Instance.Output ($"\"{_params [2]}\" is not a valid integer.");
+				return;
+			}
+
+			WebPermissions.Instance.AddModulePermission (_params [1], level);
+			SdtdConsole.Instance.Output ($"{_params [1]} added with permission level of {level}.");
+		}
+
+		private void ExecuteRemove (List<string> _params) {
+			if (_params.Count != 2) {
+				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 2, found {_params.Count}.");
+				return;
+			}
+
+			if (!WebPermissions.Instance.IsKnownModule (_params [1])) {
+				SdtdConsole.Instance.Output ($"\"{_params [1]}\" is not a valid web function.");
+				return;
+			}
+
+			WebPermissions.Instance.RemoveModulePermission (_params [1]);
+			SdtdConsole.Instance.Output ($"{_params [1]} removed from permissions list.");
+		}
+
+		private void ExecuteList () {
+			SdtdConsole.Instance.Output ("Defined web function permissions:");
+			SdtdConsole.Instance.Output ("  Level: Web function");
+			foreach (WebPermissions.WebModulePermission wmp in WebPermissions.Instance.GetModules ()) {
+				SdtdConsole.Instance.Output ($"  {wmp.permissionLevel,5}: {wmp.module}");
+			}
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/Commands/WebTokens.cs
===================================================================
--- binary-improvements2/WebServer/src/Commands/WebTokens.cs	(revision 391)
+++ binary-improvements2/WebServer/src/Commands/WebTokens.cs	(revision 391)
@@ -0,0 +1,108 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using JetBrains.Annotations;
+
+namespace Webserver.Commands {
+	[UsedImplicitly]
+	public class WebTokens : ConsoleCmdAbstract {
+		private static readonly Regex validNameTokenMatcher = new Regex (@"^\w+$");
+
+		public override string[] GetCommands () {
+			return new[] {"webtokens"};
+		}
+
+		public override string GetDescription () {
+			return "Manage web tokens";
+		}
+
+		public override string GetHelp () {
+			return "Set/get webtoken permission levels. A level of 0 is maximum permission.\n" +
+			       "Usage:\n" +
+			       "   webtokens add <username> <usertoken> <level>\n" +
+			       "   webtokens remove <username>\n" +
+			       "   webtokens list";
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			if (_params.Count >= 1) {
+				if (_params [0].EqualsCaseInsensitive ("add")) {
+					ExecuteAdd (_params);
+				} else if (_params [0].EqualsCaseInsensitive ("remove")) {
+					ExecuteRemove (_params);
+				} else if (_params [0].EqualsCaseInsensitive ("list")) {
+					ExecuteList ();
+				} else {
+					SdtdConsole.Instance.Output ("Invalid sub command \"" + _params [0] + "\".");
+				}
+			} else {
+				SdtdConsole.Instance.Output ("No sub command given.");
+			}
+		}
+
+		private void ExecuteAdd (List<string> _params) {
+			if (_params.Count != 4) {
+				SdtdConsole.Instance.Output ("Wrong number of arguments, expected 4, found " + _params.Count + ".");
+				return;
+			}
+
+			if (string.IsNullOrEmpty (_params [1])) {
+				SdtdConsole.Instance.Output ("Argument 'username' is empty.");
+				return;
+			}
+
+			if (!validNameTokenMatcher.IsMatch (_params [1])) {
+				SdtdConsole.Instance.Output (
+					"Argument 'username' may only contain characters (A-Z, a-z), digits (0-9) and underscores (_).");
+				return;
+			}
+
+			if (string.IsNullOrEmpty (_params [2])) {
+				SdtdConsole.Instance.Output ("Argument 'usertoken' is empty.");
+				return;
+			}
+
+			if (!validNameTokenMatcher.IsMatch (_params [2])) {
+				SdtdConsole.Instance.Output (
+					"Argument 'usertoken' may only contain characters (A-Z, a-z), digits (0-9) and underscores (_).");
+				return;
+			}
+
+			if (!int.TryParse (_params [3], out int level)) {
+				SdtdConsole.Instance.Output ("Argument 'level' is not a valid integer.");
+				return;
+			}
+
+			WebPermissions.Instance.AddAdmin (_params [1], _params [2], level);
+			SdtdConsole.Instance.Output ($"Web user with name={_params [1]} and password={_params [2]} added with permission level of {level}.");
+		}
+
+		private void ExecuteRemove (List<string> _params) {
+			if (_params.Count != 2) {
+				SdtdConsole.Instance.Output ("Wrong number of arguments, expected 2, found " + _params.Count + ".");
+				return;
+			}
+
+			if (string.IsNullOrEmpty (_params [1])) {
+				SdtdConsole.Instance.Output ("Argument 'username' is empty.");
+				return;
+			}
+
+			if (!validNameTokenMatcher.IsMatch (_params [1])) {
+				SdtdConsole.Instance.Output (
+					"Argument 'username' may only contain characters (A-Z, a-z), digits (0-9) and underscores (_).");
+				return;
+			}
+
+			WebPermissions.Instance.RemoveAdmin (_params [1]);
+			SdtdConsole.Instance.Output ($"{_params [1]} removed from web user permissions list.");
+		}
+
+		private void ExecuteList () {
+			SdtdConsole.Instance.Output ("Defined webuser permissions:");
+			SdtdConsole.Instance.Output ("  Level: Name / Token");
+			foreach (WebPermissions.AdminToken at in WebPermissions.Instance.GetAdmins ()) {
+				SdtdConsole.Instance.Output ($"  {at.permissionLevel,5}: {at.name} / {at.token}");
+			}
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/ConnectionHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/ConnectionHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/ConnectionHandler.cs	(revision 391)
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using Platform.Steam;
+
+namespace Webserver {
+	public class ConnectionHandler {
+		private readonly Dictionary<string, WebConnection> connections = new Dictionary<string, WebConnection> ();
+
+		public WebConnection IsLoggedIn (string _sessionId, IPAddress _ip) {
+			if (!connections.TryGetValue (_sessionId, out WebConnection con)) {
+				return null;
+			}
+
+//			if (con.Age.TotalMinutes > parent.sessionTimeoutMinutes) {
+//				connections.Remove (_sessionId);
+//				return null;
+//			}
+
+			if (!Equals (con.Endpoint, _ip)) {
+				// Fixed: Allow different clients from same NAT network
+//				connections.Remove (_sessionId);
+				return null;
+			}
+
+			con.UpdateUsage ();
+
+			return con;
+		}
+
+		public void LogOut (string _sessionId) {
+			connections.Remove (_sessionId);
+		}
+
+		public WebConnection LogIn (ulong _steamId, IPAddress _ip) {
+			string sessionId = Guid.NewGuid ().ToString ();
+			PlatformUserIdentifierAbs userId = new UserIdentifierSteam (_steamId);
+			WebConnection con = new WebConnection (sessionId, _ip, userId);
+			connections.Add (sessionId, con);
+			return con;
+		}
+
+		public void SendLine (string _line) {
+			foreach (KeyValuePair<string, WebConnection> kvp in connections) {
+				kvp.Value.SendLine (_line);
+			}
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/LogBuffer.cs
===================================================================
--- binary-improvements2/WebServer/src/LogBuffer.cs	(revision 391)
+++ binary-improvements2/WebServer/src/LogBuffer.cs	(revision 391)
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Webserver {
+	public class LogBuffer {
+		private const int maxEntries = 3000;
+		
+		private static LogBuffer instance;
+
+		private readonly List<LogEntry> logEntries = new List<LogEntry> ();
+
+		private int listOffset;
+
+		public static void Init () {
+			instance ??= new LogBuffer ();
+		}
+
+		private LogBuffer () {
+			Log.LogCallbacksExtended += LogCallback;
+		}
+
+		public static LogBuffer Instance => instance ??= new LogBuffer ();
+
+		public int OldestLine {
+			get {
+				lock (logEntries) {
+					return listOffset;
+				}
+			}
+		}
+
+		public int LatestLine {
+			get {
+				lock (logEntries) {
+					return listOffset + logEntries.Count - 1;
+				}
+			}
+		}
+
+		public int StoredLines {
+			get {
+				lock (logEntries) {
+					return logEntries.Count;
+				}
+			}
+		}
+
+		public LogEntry this [int _index] {
+			get {
+				lock (logEntries) {
+					if (_index >= listOffset && _index < listOffset + logEntries.Count) {
+						return logEntries [_index];
+					}
+				}
+
+				return null;
+			}
+		}
+
+		private void LogCallback (string _formattedMsg, string _plainMsg, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
+			LogEntry le = new LogEntry (_timestamp, _plainMsg, _trace, _type, _uptime);
+
+			lock (logEntries) {
+				logEntries.Add (le);
+				if (logEntries.Count > maxEntries) {
+					listOffset += logEntries.Count - maxEntries;
+					logEntries.RemoveRange (0, logEntries.Count - maxEntries);
+				}
+			}
+		}
+
+		private readonly List<LogEntry> emptyList = new List<LogEntry> ();
+
+		public List<LogEntry> GetRange (ref int _start, int _count, out int _end) {
+			lock (logEntries) {
+				int index;
+				
+				if (_count < 0) {
+					_count = -_count;
+					
+					if (_start >= listOffset + logEntries.Count) {
+						_start = listOffset + logEntries.Count - 1;
+					}
+
+					_end = _start;
+
+					if (_start < listOffset) {
+						return emptyList;
+					}
+					
+					_start -= _count - 1;
+
+					if (_start < listOffset) {
+						_start = listOffset;
+					}
+
+					index = _start - listOffset;
+					_end += 1;
+					_count = _end - _start;
+				} else {
+					if (_start < listOffset) {
+						_start = listOffset;
+					}
+
+					if (_start >= listOffset + logEntries.Count) {
+						_end = _start;
+						return emptyList;
+					}
+
+					index = _start - listOffset;
+
+					if (index + _count > logEntries.Count) {
+						_count = logEntries.Count - index;
+					}
+
+					_end = _start + _count;
+				}
+
+				return logEntries.GetRange (index, _count);
+			}
+		}
+
+
+		public class LogEntry {
+			public readonly DateTime timestamp;
+			public readonly string isoTime;
+			public readonly string message;
+			public readonly string trace;
+			public readonly LogType type;
+			public readonly long uptime;
+
+			public LogEntry (DateTime _timestamp, string _message, string _trace, LogType _type, long _uptime) {
+				timestamp = _timestamp;
+				isoTime = _timestamp.ToString ("o");
+
+				message = _message;
+				trace = _trace;
+				type = _type;
+				uptime = _uptime;
+			}
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/MimeType.cs
===================================================================
--- binary-improvements2/WebServer/src/MimeType.cs	(revision 391)
+++ binary-improvements2/WebServer/src/MimeType.cs	(revision 391)
@@ -0,0 +1,582 @@
+using System;
+using System.Collections.Generic;
+
+namespace Webserver {
+	public static class MimeType {
+		private static readonly IDictionary<string, string> mappings =
+			new CaseInsensitiveStringDictionary<string> {
+				{".323", "text/h323"},
+				{".3g2", "video/3gpp2"},
+				{".3gp", "video/3gpp"},
+				{".3gp2", "video/3gpp2"},
+				{".3gpp", "video/3gpp"},
+				{".7z", "application/x-7z-compressed"},
+				{".aa", "audio/audible"},
+				{".AAC", "audio/aac"},
+				{".aaf", "application/octet-stream"},
+				{".aax", "audio/vnd.audible.aax"},
+				{".ac3", "audio/ac3"},
+				{".aca", "application/octet-stream"},
+				{".accda", "application/msaccess.addin"},
+				{".accdb", "application/msaccess"},
+				{".accdc", "application/msaccess.cab"},
+				{".accde", "application/msaccess"},
+				{".accdr", "application/msaccess.runtime"},
+				{".accdt", "application/msaccess"},
+				{".accdw", "application/msaccess.webapplication"},
+				{".accft", "application/msaccess.ftemplate"},
+				{".acx", "application/internet-property-stream"},
+				{".AddIn", "text/xml"},
+				{".ade", "application/msaccess"},
+				{".adobebridge", "application/x-bridge-url"},
+				{".adp", "application/msaccess"},
+				{".ADT", "audio/vnd.dlna.adts"},
+				{".ADTS", "audio/aac"},
+				{".afm", "application/octet-stream"},
+				{".ai", "application/postscript"},
+				{".aif", "audio/x-aiff"},
+				{".aifc", "audio/aiff"},
+				{".aiff", "audio/aiff"},
+				{".air", "application/vnd.adobe.air-application-installer-package+zip"},
+				{".amc", "application/x-mpeg"},
+				{".application", "application/x-ms-application"},
+				{".art", "image/x-jg"},
+				{".asa", "application/xml"},
+				{".asax", "application/xml"},
+				{".ascx", "application/xml"},
+				{".asd", "application/octet-stream"},
+				{".asf", "video/x-ms-asf"},
+				{".ashx", "application/xml"},
+				{".asi", "application/octet-stream"},
+				{".asm", "text/plain"},
+				{".asmx", "application/xml"},
+				{".aspx", "application/xml"},
+				{".asr", "video/x-ms-asf"},
+				{".asx", "video/x-ms-asf"},
+				{".atom", "application/atom+xml"},
+				{".au", "audio/basic"},
+				{".avi", "video/x-msvideo"},
+				{".axs", "application/olescript"},
+				{".bas", "text/plain"},
+				{".bcpio", "application/x-bcpio"},
+				{".bin", "application/octet-stream"},
+				{".bmp", "image/bmp"},
+				{".c", "text/plain"},
+				{".cab", "application/octet-stream"},
+				{".caf", "audio/x-caf"},
+				{".calx", "application/vnd.ms-office.calx"},
+				{".cat", "application/vnd.ms-pki.seccat"},
+				{".cc", "text/plain"},
+				{".cd", "text/plain"},
+				{".cdda", "audio/aiff"},
+				{".cdf", "application/x-cdf"},
+				{".cer", "application/x-x509-ca-cert"},
+				{".chm", "application/octet-stream"},
+				{".class", "application/x-java-applet"},
+				{".clp", "application/x-msclip"},
+				{".cmx", "image/x-cmx"},
+				{".cnf", "text/plain"},
+				{".cod", "image/cis-cod"},
+				{".config", "application/xml"},
+				{".contact", "text/x-ms-contact"},
+				{".coverage", "application/xml"},
+				{".cpio", "application/x-cpio"},
+				{".cpp", "text/plain"},
+				{".crd", "application/x-mscardfile"},
+				{".crl", "application/pkix-crl"},
+				{".crt", "application/x-x509-ca-cert"},
+				{".cs", "text/plain"},
+				{".csdproj", "text/plain"},
+				{".csh", "application/x-csh"},
+				{".csproj", "text/plain"},
+				{".css", "text/css"},
+				{".csv", "text/csv"},
+				{".cur", "application/octet-stream"},
+				{".cxx", "text/plain"},
+				{".dat", "application/octet-stream"},
+				{".datasource", "application/xml"},
+				{".dbproj", "text/plain"},
+				{".dcr", "application/x-director"},
+				{".def", "text/plain"},
+				{".deploy", "application/octet-stream"},
+				{".der", "application/x-x509-ca-cert"},
+				{".dgml", "application/xml"},
+				{".dib", "image/bmp"},
+				{".dif", "video/x-dv"},
+				{".dir", "application/x-director"},
+				{".disco", "text/xml"},
+				{".dll", "application/x-msdownload"},
+				{".dll.config", "text/xml"},
+				{".dlm", "text/dlm"},
+				{".doc", "application/msword"},
+				{".docm", "application/vnd.ms-word.document.macroEnabled.12"},
+				{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
+				{".dot", "application/msword"},
+				{".dotm", "application/vnd.ms-word.template.macroEnabled.12"},
+				{".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"},
+				{".dsp", "application/octet-stream"},
+				{".dsw", "text/plain"},
+				{".dtd", "text/xml"},
+				{".dtsConfig", "text/xml"},
+				{".dv", "video/x-dv"},
+				{".dvi", "application/x-dvi"},
+				{".dwf", "drawing/x-dwf"},
+				{".dwp", "application/octet-stream"},
+				{".dxr", "application/x-director"},
+				{".eml", "message/rfc822"},
+				{".emz", "application/octet-stream"},
+				{".eot", "application/octet-stream"},
+				{".eps", "application/postscript"},
+				{".etl", "application/etl"},
+				{".etx", "text/x-setext"},
+				{".evy", "application/envoy"},
+				{".exe", "application/octet-stream"},
+				{".exe.config", "text/xml"},
+				{".fdf", "application/vnd.fdf"},
+				{".fif", "application/fractals"},
+				{".filters", "Application/xml"},
+				{".fla", "application/octet-stream"},
+				{".flr", "x-world/x-vrml"},
+				{".flv", "video/x-flv"},
+				{".fsscript", "application/fsharp-script"},
+				{".fsx", "application/fsharp-script"},
+				{".generictest", "application/xml"},
+				{".gif", "image/gif"},
+				{".group", "text/x-ms-group"},
+				{".gsm", "audio/x-gsm"},
+				{".gtar", "application/x-gtar"},
+				{".gz", "application/x-gzip"},
+				{".h", "text/plain"},
+				{".hdf", "application/x-hdf"},
+				{".hdml", "text/x-hdml"},
+				{".hhc", "application/x-oleobject"},
+				{".hhk", "application/octet-stream"},
+				{".hhp", "application/octet-stream"},
+				{".hlp", "application/winhlp"},
+				{".hpp", "text/plain"},
+				{".hqx", "application/mac-binhex40"},
+				{".hta", "application/hta"},
+				{".htc", "text/x-component"},
+				{".htm", "text/html"},
+				{".html", "text/html"},
+				{".htt", "text/webviewhtml"},
+				{".hxa", "application/xml"},
+				{".hxc", "application/xml"},
+				{".hxd", "application/octet-stream"},
+				{".hxe", "application/xml"},
+				{".hxf", "application/xml"},
+				{".hxh", "application/octet-stream"},
+				{".hxi", "application/octet-stream"},
+				{".hxk", "application/xml"},
+				{".hxq", "application/octet-stream"},
+				{".hxr", "application/octet-stream"},
+				{".hxs", "application/octet-stream"},
+				{".hxt", "text/html"},
+				{".hxv", "application/xml"},
+				{".hxw", "application/octet-stream"},
+				{".hxx", "text/plain"},
+				{".i", "text/plain"},
+				{".ico", "image/x-icon"},
+				{".ics", "application/octet-stream"},
+				{".idl", "text/plain"},
+				{".ief", "image/ief"},
+				{".iii", "application/x-iphone"},
+				{".inc", "text/plain"},
+				{".inf", "application/octet-stream"},
+				{".inl", "text/plain"},
+				{".ins", "application/x-internet-signup"},
+				{".ipa", "application/x-itunes-ipa"},
+				{".ipg", "application/x-itunes-ipg"},
+				{".ipproj", "text/plain"},
+				{".ipsw", "application/x-itunes-ipsw"},
+				{".iqy", "text/x-ms-iqy"},
+				{".isp", "application/x-internet-signup"},
+				{".ite", "application/x-itunes-ite"},
+				{".itlp", "application/x-itunes-itlp"},
+				{".itms", "application/x-itunes-itms"},
+				{".itpc", "application/x-itunes-itpc"},
+				{".IVF", "video/x-ivf"},
+				{".jar", "application/java-archive"},
+				{".java", "application/octet-stream"},
+				{".jck", "application/liquidmotion"},
+				{".jcz", "application/liquidmotion"},
+				{".jfif", "image/pjpeg"},
+				{".jnlp", "application/x-java-jnlp-file"},
+				{".jpb", "application/octet-stream"},
+				{".jpe", "image/jpeg"},
+				{".jpeg", "image/jpeg"},
+				{".jpg", "image/jpeg"},
+				{".js", "application/x-javascript"},
+				{".json", "application/json"},
+				{".jsx", "text/jscript"},
+				{".jsxbin", "text/plain"},
+				{".latex", "application/x-latex"},
+				{".library-ms", "application/windows-library+xml"},
+				{".lit", "application/x-ms-reader"},
+				{".loadtest", "application/xml"},
+				{".lpk", "application/octet-stream"},
+				{".lsf", "video/x-la-asf"},
+				{".lst", "text/plain"},
+				{".lsx", "video/x-la-asf"},
+				{".lzh", "application/octet-stream"},
+				{".m13", "application/x-msmediaview"},
+				{".m14", "application/x-msmediaview"},
+				{".m1v", "video/mpeg"},
+				{".m2t", "video/vnd.dlna.mpeg-tts"},
+				{".m2ts", "video/vnd.dlna.mpeg-tts"},
+				{".m2v", "video/mpeg"},
+				{".m3u", "audio/x-mpegurl"},
+				{".m3u8", "audio/x-mpegurl"},
+				{".m4a", "audio/m4a"},
+				{".m4b", "audio/m4b"},
+				{".m4p", "audio/m4p"},
+				{".m4r", "audio/x-m4r"},
+				{".m4v", "video/x-m4v"},
+				{".mac", "image/x-macpaint"},
+				{".mak", "text/plain"},
+				{".man", "application/x-troff-man"},
+				{".manifest", "application/x-ms-manifest"},
+				{".map", "text/plain"},
+				{".master", "application/xml"},
+				{".mda", "application/msaccess"},
+				{".mdb", "application/x-msaccess"},
+				{".mde", "application/msaccess"},
+				{".mdp", "application/octet-stream"},
+				{".me", "application/x-troff-me"},
+				{".mfp", "application/x-shockwave-flash"},
+				{".mht", "message/rfc822"},
+				{".mhtml", "message/rfc822"},
+				{".mid", "audio/mid"},
+				{".midi", "audio/mid"},
+				{".mix", "application/octet-stream"},
+				{".mk", "text/plain"},
+				{".mmf", "application/x-smaf"},
+				{".mno", "text/xml"},
+				{".mny", "application/x-msmoney"},
+				{".mod", "video/mpeg"},
+				{".mov", "video/quicktime"},
+				{".movie", "video/x-sgi-movie"},
+				{".mp2", "video/mpeg"},
+				{".mp2v", "video/mpeg"},
+				{".mp3", "audio/mpeg"},
+				{".mp4", "video/mp4"},
+				{".mp4v", "video/mp4"},
+				{".mpa", "video/mpeg"},
+				{".mpe", "video/mpeg"},
+				{".mpeg", "video/mpeg"},
+				{".mpf", "application/vnd.ms-mediapackage"},
+				{".mpg", "video/mpeg"},
+				{".mpp", "application/vnd.ms-project"},
+				{".mpv2", "video/mpeg"},
+				{".mqv", "video/quicktime"},
+				{".ms", "application/x-troff-ms"},
+				{".msi", "application/octet-stream"},
+				{".mso", "application/octet-stream"},
+				{".mts", "video/vnd.dlna.mpeg-tts"},
+				{".mtx", "application/xml"},
+				{".mvb", "application/x-msmediaview"},
+				{".mvc", "application/x-miva-compiled"},
+				{".mxp", "application/x-mmxp"},
+				{".nc", "application/x-netcdf"},
+				{".nsc", "video/x-ms-asf"},
+				{".nws", "message/rfc822"},
+				{".ocx", "application/octet-stream"},
+				{".oda", "application/oda"},
+				{".odc", "text/x-ms-odc"},
+				{".odh", "text/plain"},
+				{".odl", "text/plain"},
+				{".odp", "application/vnd.oasis.opendocument.presentation"},
+				{".ods", "application/oleobject"},
+				{".odt", "application/vnd.oasis.opendocument.text"},
+				{".one", "application/onenote"},
+				{".onea", "application/onenote"},
+				{".onepkg", "application/onenote"},
+				{".onetmp", "application/onenote"},
+				{".onetoc", "application/onenote"},
+				{".onetoc2", "application/onenote"},
+				{".orderedtest", "application/xml"},
+				{".osdx", "application/opensearchdescription+xml"},
+				{".p10", "application/pkcs10"},
+				{".p12", "application/x-pkcs12"},
+				{".p7b", "application/x-pkcs7-certificates"},
+				{".p7c", "application/pkcs7-mime"},
+				{".p7m", "application/pkcs7-mime"},
+				{".p7r", "application/x-pkcs7-certreqresp"},
+				{".p7s", "application/pkcs7-signature"},
+				{".pbm", "image/x-portable-bitmap"},
+				{".pcast", "application/x-podcast"},
+				{".pct", "image/pict"},
+				{".pcx", "application/octet-stream"},
+				{".pcz", "application/octet-stream"},
+				{".pdf", "application/pdf"},
+				{".pfb", "application/octet-stream"},
+				{".pfm", "application/octet-stream"},
+				{".pfx", "application/x-pkcs12"},
+				{".pgm", "image/x-portable-graymap"},
+				{".pic", "image/pict"},
+				{".pict", "image/pict"},
+				{".pkgdef", "text/plain"},
+				{".pkgundef", "text/plain"},
+				{".pko", "application/vnd.ms-pki.pko"},
+				{".pls", "audio/scpls"},
+				{".pma", "application/x-perfmon"},
+				{".pmc", "application/x-perfmon"},
+				{".pml", "application/x-perfmon"},
+				{".pmr", "application/x-perfmon"},
+				{".pmw", "application/x-perfmon"},
+				{".png", "image/png"},
+				{".pnm", "image/x-portable-anymap"},
+				{".pnt", "image/x-macpaint"},
+				{".pntg", "image/x-macpaint"},
+				{".pnz", "image/png"},
+				{".pot", "application/vnd.ms-powerpoint"},
+				{".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"},
+				{".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"},
+				{".ppa", "application/vnd.ms-powerpoint"},
+				{".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"},
+				{".ppm", "image/x-portable-pixmap"},
+				{".pps", "application/vnd.ms-powerpoint"},
+				{".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"},
+				{".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"},
+				{".ppt", "application/vnd.ms-powerpoint"},
+				{".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"},
+				{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
+				{".prf", "application/pics-rules"},
+				{".prm", "application/octet-stream"},
+				{".prx", "application/octet-stream"},
+				{".ps", "application/postscript"},
+				{".psc1", "application/PowerShell"},
+				{".psd", "application/octet-stream"},
+				{".psess", "application/xml"},
+				{".psm", "application/octet-stream"},
+				{".psp", "application/octet-stream"},
+				{".pub", "application/x-mspublisher"},
+				{".pwz", "application/vnd.ms-powerpoint"},
+				{".qht", "text/x-html-insertion"},
+				{".qhtm", "text/x-html-insertion"},
+				{".qt", "video/quicktime"},
+				{".qti", "image/x-quicktime"},
+				{".qtif", "image/x-quicktime"},
+				{".qtl", "application/x-quicktimeplayer"},
+				{".qxd", "application/octet-stream"},
+				{".ra", "audio/x-pn-realaudio"},
+				{".ram", "audio/x-pn-realaudio"},
+				{".rar", "application/octet-stream"},
+				{".ras", "image/x-cmu-raster"},
+				{".rat", "application/rat-file"},
+				{".rc", "text/plain"},
+				{".rc2", "text/plain"},
+				{".rct", "text/plain"},
+				{".rdlc", "application/xml"},
+				{".resx", "application/xml"},
+				{".rf", "image/vnd.rn-realflash"},
+				{".rgb", "image/x-rgb"},
+				{".rgs", "text/plain"},
+				{".rm", "application/vnd.rn-realmedia"},
+				{".rmi", "audio/mid"},
+				{".rmp", "application/vnd.rn-rn_music_package"},
+				{".roff", "application/x-troff"},
+				{".rpm", "audio/x-pn-realaudio-plugin"},
+				{".rqy", "text/x-ms-rqy"},
+				{".rtf", "application/rtf"},
+				{".rtx", "text/richtext"},
+				{".ruleset", "application/xml"},
+				{".s", "text/plain"},
+				{".safariextz", "application/x-safari-safariextz"},
+				{".scd", "application/x-msschedule"},
+				{".sct", "text/scriptlet"},
+				{".sd2", "audio/x-sd2"},
+				{".sdp", "application/sdp"},
+				{".sea", "application/octet-stream"},
+				{".searchConnector-ms", "application/windows-search-connector+xml"},
+				{".setpay", "application/set-payment-initiation"},
+				{".setreg", "application/set-registration-initiation"},
+				{".settings", "application/xml"},
+				{".sgimb", "application/x-sgimb"},
+				{".sgml", "text/sgml"},
+				{".sh", "application/x-sh"},
+				{".shar", "application/x-shar"},
+				{".shtml", "text/html"},
+				{".sit", "application/x-stuffit"},
+				{".sitemap", "application/xml"},
+				{".skin", "application/xml"},
+				{".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"},
+				{".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"},
+				{".slk", "application/vnd.ms-excel"},
+				{".sln", "text/plain"},
+				{".slupkg-ms", "application/x-ms-license"},
+				{".smd", "audio/x-smd"},
+				{".smi", "application/octet-stream"},
+				{".smx", "audio/x-smd"},
+				{".smz", "audio/x-smd"},
+				{".snd", "audio/basic"},
+				{".snippet", "application/xml"},
+				{".snp", "application/octet-stream"},
+				{".sol", "text/plain"},
+				{".sor", "text/plain"},
+				{".spc", "application/x-pkcs7-certificates"},
+				{".spl", "application/futuresplash"},
+				{".src", "application/x-wais-source"},
+				{".srf", "text/plain"},
+				{".SSISDeploymentManifest", "text/xml"},
+				{".ssm", "application/streamingmedia"},
+				{".sst", "application/vnd.ms-pki.certstore"},
+				{".stl", "application/vnd.ms-pki.stl"},
+				{".sv4cpio", "application/x-sv4cpio"},
+				{".sv4crc", "application/x-sv4crc"},
+				{".svc", "application/xml"},
+				{".swf", "application/x-shockwave-flash"},
+				{".t", "application/x-troff"},
+				{".tar", "application/x-tar"},
+				{".tcl", "application/x-tcl"},
+				{".testrunconfig", "application/xml"},
+				{".testsettings", "application/xml"},
+				{".tex", "application/x-tex"},
+				{".texi", "application/x-texinfo"},
+				{".texinfo", "application/x-texinfo"},
+				{".tgz", "application/x-compressed"},
+				{".thmx", "application/vnd.ms-officetheme"},
+				{".thn", "application/octet-stream"},
+				{".tif", "image/tiff"},
+				{".tiff", "image/tiff"},
+				{".tlh", "text/plain"},
+				{".tli", "text/plain"},
+				{".toc", "application/octet-stream"},
+				{".tr", "application/x-troff"},
+				{".trm", "application/x-msterminal"},
+				{".trx", "application/xml"},
+				{".ts", "video/vnd.dlna.mpeg-tts"},
+				{".tsv", "text/tab-separated-values"},
+				{".ttf", "application/octet-stream"},
+				{".tts", "video/vnd.dlna.mpeg-tts"},
+				{".txt", "text/plain"},
+				{".u32", "application/octet-stream"},
+				{".uls", "text/iuls"},
+				{".user", "text/plain"},
+				{".ustar", "application/x-ustar"},
+				{".vb", "text/plain"},
+				{".vbdproj", "text/plain"},
+				{".vbk", "video/mpeg"},
+				{".vbproj", "text/plain"},
+				{".vbs", "text/vbscript"},
+				{".vcf", "text/x-vcard"},
+				{".vcproj", "Application/xml"},
+				{".vcs", "text/plain"},
+				{".vcxproj", "Application/xml"},
+				{".vddproj", "text/plain"},
+				{".vdp", "text/plain"},
+				{".vdproj", "text/plain"},
+				{".vdx", "application/vnd.ms-visio.viewer"},
+				{".vml", "text/xml"},
+				{".vscontent", "application/xml"},
+				{".vsct", "text/xml"},
+				{".vsd", "application/vnd.visio"},
+				{".vsi", "application/ms-vsi"},
+				{".vsix", "application/vsix"},
+				{".vsixlangpack", "text/xml"},
+				{".vsixmanifest", "text/xml"},
+				{".vsmdi", "application/xml"},
+				{".vspscc", "text/plain"},
+				{".vss", "application/vnd.visio"},
+				{".vsscc", "text/plain"},
+				{".vssettings", "text/xml"},
+				{".vssscc", "text/plain"},
+				{".vst", "application/vnd.visio"},
+				{".vstemplate", "text/xml"},
+				{".vsto", "application/x-ms-vsto"},
+				{".vsw", "application/vnd.visio"},
+				{".vsx", "application/vnd.visio"},
+				{".vtx", "application/vnd.visio"},
+				{".wav", "audio/wav"},
+				{".wave", "audio/wav"},
+				{".wax", "audio/x-ms-wax"},
+				{".wbk", "application/msword"},
+				{".wbmp", "image/vnd.wap.wbmp"},
+				{".wcm", "application/vnd.ms-works"},
+				{".wdb", "application/vnd.ms-works"},
+				{".wdp", "image/vnd.ms-photo"},
+				{".webarchive", "application/x-safari-webarchive"},
+				{".webtest", "application/xml"},
+				{".wiq", "application/xml"},
+				{".wiz", "application/msword"},
+				{".wks", "application/vnd.ms-works"},
+				{".WLMP", "application/wlmoviemaker"},
+				{".wlpginstall", "application/x-wlpg-detect"},
+				{".wlpginstall3", "application/x-wlpg3-detect"},
+				{".wm", "video/x-ms-wm"},
+				{".wma", "audio/x-ms-wma"},
+				{".wmd", "application/x-ms-wmd"},
+				{".wmf", "application/x-msmetafile"},
+				{".wml", "text/vnd.wap.wml"},
+				{".wmlc", "application/vnd.wap.wmlc"},
+				{".wmls", "text/vnd.wap.wmlscript"},
+				{".wmlsc", "application/vnd.wap.wmlscriptc"},
+				{".wmp", "video/x-ms-wmp"},
+				{".wmv", "video/x-ms-wmv"},
+				{".wmx", "video/x-ms-wmx"},
+				{".wmz", "application/x-ms-wmz"},
+				{".wpl", "application/vnd.ms-wpl"},
+				{".wps", "application/vnd.ms-works"},
+				{".wri", "application/x-mswrite"},
+				{".wrl", "x-world/x-vrml"},
+				{".wrz", "x-world/x-vrml"},
+				{".wsc", "text/scriptlet"},
+				{".wsdl", "text/xml"},
+				{".wvx", "video/x-ms-wvx"},
+				{".x", "application/directx"},
+				{".xaf", "x-world/x-vrml"},
+				{".xaml", "application/xaml+xml"},
+				{".xap", "application/x-silverlight-app"},
+				{".xbap", "application/x-ms-xbap"},
+				{".xbm", "image/x-xbitmap"},
+				{".xdr", "text/plain"},
+				{".xht", "application/xhtml+xml"},
+				{".xhtml", "application/xhtml+xml"},
+				{".xla", "application/vnd.ms-excel"},
+				{".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"},
+				{".xlc", "application/vnd.ms-excel"},
+				{".xld", "application/vnd.ms-excel"},
+				{".xlk", "application/vnd.ms-excel"},
+				{".xll", "application/vnd.ms-excel"},
+				{".xlm", "application/vnd.ms-excel"},
+				{".xls", "application/vnd.ms-excel"},
+				{".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"},
+				{".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"},
+				{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
+				{".xlt", "application/vnd.ms-excel"},
+				{".xltm", "application/vnd.ms-excel.template.macroEnabled.12"},
+				{".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"},
+				{".xlw", "application/vnd.ms-excel"},
+				{".xml", "text/xml"},
+				{".xmta", "application/xml"},
+				{".xof", "x-world/x-vrml"},
+				{".XOML", "text/plain"},
+				{".xpm", "image/x-xpixmap"},
+				{".xps", "application/vnd.ms-xpsdocument"},
+				{".xrm-ms", "text/xml"},
+				{".xsc", "application/xml"},
+				{".xsd", "text/xml"},
+				{".xsf", "text/xml"},
+				{".xsl", "text/xml"},
+				{".xslt", "text/xml"},
+				{".xsn", "application/octet-stream"},
+				{".xss", "application/xml"},
+				{".xtp", "application/octet-stream"},
+				{".xwd", "image/x-xwindowdump"},
+				{".z", "application/x-compress"},
+				{".zip", "application/x-zip-compressed"}
+			};
+
+		public static string GetMimeType (string _extension) {
+			if (_extension == null) {
+				throw new ArgumentNullException (nameof (_extension));
+			}
+
+			if (!_extension.StartsWith (".")) {
+				_extension = "." + _extension;
+			}
+
+			return mappings.TryGetValue (_extension, out string mime) ? mime : "application/octet-stream";
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/ModApi.cs
===================================================================
--- binary-improvements2/WebServer/src/ModApi.cs	(revision 391)
+++ binary-improvements2/WebServer/src/ModApi.cs	(revision 391)
@@ -0,0 +1,33 @@
+using JetBrains.Annotations;
+using Webserver.UrlHandlers;
+
+namespace Webserver {
+	[UsedImplicitly]
+	public class ModApi : IModApi {
+		private Web webInstance;
+		private Mod modInstance;
+		
+		public void InitMod (Mod _modInstance) {
+			ModEvents.GameStartDone.RegisterHandler (GameStartDone);
+			ModEvents.GameShutdown.RegisterHandler (GameShutdown);
+			modInstance = _modInstance;
+		}
+
+		private void GameStartDone () {
+			if (!ConnectionManager.Instance.IsServer) {
+				return;
+			}
+			
+			webInstance = new Web (modInstance.Path);
+			LogBuffer.Init ();
+
+			if (ItemIconHandler.Instance != null) {
+				ItemIconHandler.Instance.LoadIcons ();
+			}
+		}
+
+		private void GameShutdown () {
+			webInstance?.Shutdown ();
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/OpenID.cs
===================================================================
--- binary-improvements2/WebServer/src/OpenID.cs	(revision 391)
+++ binary-improvements2/WebServer/src/OpenID.cs	(revision 391)
@@ -0,0 +1,233 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Net;
+using System.Net.Security;
+using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Text.RegularExpressions;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+
+namespace Webserver {
+	public static class OpenID {
+		private const string STEAM_LOGIN = "https://steamcommunity.com/openid/login";
+
+		private static readonly Regex steamIdUrlMatcher =
+			new Regex (@"^https?:\/\/steamcommunity\.com\/openid\/id\/([0-9]{17,18})");
+
+		private static readonly X509Certificate2 caCert =
+			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");
+
+		private const bool verboseSsl = false;
+		public static bool debugOpenId;
+
+		static OpenID () {
+			for (int i = 0; i < Environment.GetCommandLineArgs ().Length; i++) {
+				if (Environment.GetCommandLineArgs () [i].EqualsCaseInsensitive ("-debugopenid")) {
+					debugOpenId = true;
+				}
+			}
+
+			ServicePointManager.ServerCertificateValidationCallback = (_srvPoint, _certificate, _chain, _errors) => {
+				if (_errors == SslPolicyErrors.None) {
+					if (verboseSsl) {
+						Log.Out ("Steam certificate: No error (1)");
+					}
+
+					return true;
+				}
+
+				X509Chain privateChain = new X509Chain ();
+				privateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+
+				privateChain.ChainPolicy.ExtraStore.Add (caCert);
+				privateChain.ChainPolicy.ExtraStore.Add (caIntermediateCert);
+
+				if (privateChain.Build (new X509Certificate2 (_certificate))) {
+					// No errors, immediately return
+					privateChain.Reset ();
+					if (verboseSsl) {
+						Log.Out ("Steam certificate: No error (2)");
+					}
+
+					return true;
+				}
+
+				if (privateChain.ChainStatus.Length == 0) {
+					// No errors, immediately return
+					privateChain.Reset ();
+					if (verboseSsl) {
+						Log.Out ("Steam certificate: No error (3)");
+					}
+
+					return true;
+				}
+
+				// Iterate all chain elements
+				foreach (X509ChainElement chainEl in privateChain.ChainElements) {
+					if (verboseSsl) {
+						Log.Out ("Validating cert: " + chainEl.Certificate.Subject);
+					}
+
+					// Iterate all status flags of the current cert
+					foreach (X509ChainStatus chainStatus in chainEl.ChainElementStatus) {
+						if (verboseSsl) {
+							Log.Out ("   Status: " + chainStatus.Status);
+						}
+
+						if (chainStatus.Status == X509ChainStatusFlags.NoError) {
+							// This status is not an error, skip
+							continue;
+						}
+
+						if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && chainEl.Certificate.Equals (caCert)) {
+							// This status is about the cert being an untrusted root certificate but the certificate is one of those we added, ignore
+							continue;
+						}
+
+						// This status is an error, print information
+						Log.Warning ("Steam certificate error: " + chainEl.Certificate.Subject + " ### Error: " +
+						             chainStatus.Status);
+						privateChain.Reset ();
+						return false;
+					}
+				}
+
+				foreach (X509ChainStatus chainStatus in privateChain.ChainStatus) {
+					if (chainStatus.Status != X509ChainStatusFlags.NoError &&
+					    chainStatus.Status != X509ChainStatusFlags.UntrustedRoot) {
+						Log.Warning ("Steam certificate error: " + chainStatus.Status);
+						privateChain.Reset ();
+						return false;
+					}
+				}
+
+				// We didn't find any errors, chain is valid
+				privateChain.Reset ();
+				if (verboseSsl) {
+					Log.Out ("Steam certificate: No error (4)");
+				}
+
+				return true;
+			};
+		}
+
+		public static string GetOpenIdLoginUrl (string _returnHost, string _returnUrl) {
+			Dictionary<string, string> queryParams = new Dictionary<string, string> {
+				{ "openid.ns", "http://specs.openid.net/auth/2.0" },
+				{ "openid.mode", "checkid_setup" },
+				{ "openid.return_to", _returnUrl },
+				{ "openid.realm", _returnHost },
+				{ "openid.identity", "http://specs.openid.net/auth/2.0/identifier_select" },
+				{ "openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select" }
+			};
+
+			return STEAM_LOGIN + '?' + buildUrlParams (queryParams);
+		}
+
+		public static ulong Validate (HttpListenerRequest _req) {
+			string mode = getValue (_req, "openid.mode");
+			if (mode == "cancel") {
+				Log.Warning ("Steam OpenID login canceled");
+				return 0;
+			}
+
+			if (mode == "error") {
+				Log.Warning ("Steam OpenID login error: " + getValue (_req, "openid.error"));
+				if (debugOpenId) {
+					PrintOpenIdResponse (_req);
+				}
+
+				return 0;
+			}
+
+			string steamIdString = getValue (_req, "openid.claimed_id");
+			ulong steamId;
+			Match steamIdMatch = steamIdUrlMatcher.Match (steamIdString);
+			if (steamIdMatch.Success) {
+				steamId = ulong.Parse (steamIdMatch.Groups [1].Value);
+			} else {
+				Log.Warning ("Steam OpenID login result did not give a valid SteamID");
+				if (debugOpenId) {
+					PrintOpenIdResponse (_req);
+				}
+
+				return 0;
+			}
+
+			Dictionary<string, string> queryParams = new Dictionary<string, string> {
+				{ "openid.ns", "http://specs.openid.net/auth/2.0" },
+				{ "openid.assoc_handle", getValue (_req, "openid.assoc_handle") },
+				{ "openid.signed", getValue (_req, "openid.signed") },
+				{ "openid.sig", getValue (_req, "openid.sig") },
+				{ "openid.identity", "http://specs.openid.net/auth/2.0/identifier_select" },
+				{ "openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select" }
+			};
+
+			string[] signeds = getValue (_req, "openid.signed").Split (',');
+			foreach (string s in signeds) {
+				queryParams ["openid." + s] = getValue (_req, "openid." + s);
+			}
+
+			queryParams.Add ("openid.mode", "check_authentication");
+
+			byte[] postData = Encoding.ASCII.GetBytes (buildUrlParams (queryParams));
+			HttpWebRequest request = (HttpWebRequest) WebRequest.Create (STEAM_LOGIN);
+			request.Method = "POST";
+			request.ContentType = "application/x-www-form-urlencoded";
+			request.ContentLength = postData.Length;
+			request.Headers.Add (HttpRequestHeader.AcceptLanguage, "en");
+			using (Stream st = request.GetRequestStream ()) {
+				st.Write (postData, 0, postData.Length);
+			}
+
+			HttpWebResponse response = (HttpWebResponse) request.GetResponse ();
+			string responseString;
+			using (Stream st = response.GetResponseStream ()) {
+				using (StreamReader str = new StreamReader (st)) {
+					responseString = str.ReadToEnd ();
+				}
+			}
+
+			if (responseString.ContainsCaseInsensitive ("is_valid:true")) {
+				return steamId;
+			}
+
+			Log.Warning ("Steam OpenID login failed: {0}", responseString);
+			return 0;
+		}
+
+		private static string buildUrlParams (Dictionary<string, string> _queryParams) {
+			string[] paramsArr = new string[_queryParams.Count];
+			int i = 0;
+			foreach ((string argName, string argValue) in _queryParams) {
+				paramsArr [i++] = argName + "=" + Uri.EscapeDataString (argValue);
+			}
+
+			return string.Join ("&", paramsArr);
+		}
+
+		private static string getValue (HttpListenerRequest _req, string _name) {
+			NameValueCollection nvc = _req.QueryString;
+			if (nvc [_name] == null) {
+				throw new MissingMemberException ("OpenID parameter \"" + _name + "\" missing");
+			}
+
+			return nvc [_name];
+		}
+
+		private static void PrintOpenIdResponse (HttpListenerRequest _req) {
+			NameValueCollection nvc = _req.QueryString;
+			for (int i = 0; i < nvc.Count; i++) {
+				Log.Out ("   " + nvc.GetKey (i) + " = " + nvc [i]);
+			}
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/RequestContext.cs
===================================================================
--- binary-improvements2/WebServer/src/RequestContext.cs	(revision 391)
+++ binary-improvements2/WebServer/src/RequestContext.cs	(revision 391)
@@ -0,0 +1,19 @@
+using SpaceWizards.HttpListener;
+
+namespace Webserver {
+	public class RequestContext {
+		public string RequestPath;
+		public readonly HttpListenerRequest Request;
+		public readonly HttpListenerResponse Response;
+		public readonly WebConnection Connection;
+		public readonly int PermissionLevel;
+
+		public RequestContext (string _requestPath, HttpListenerRequest _request, HttpListenerResponse _response, WebConnection _connection, int _permissionLevel) {
+			RequestPath = _requestPath;
+			Request = _request;
+			Response = _response;
+			Connection = _connection;
+			PermissionLevel = _permissionLevel;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/SSE/AbsEvent.cs
===================================================================
--- binary-improvements2/WebServer/src/SSE/AbsEvent.cs	(revision 391)
+++ binary-improvements2/WebServer/src/SSE/AbsEvent.cs	(revision 391)
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+using AllocsFixes.JSON;
+using Webserver.UrlHandlers;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
+
+namespace Webserver.SSE {
+	public abstract class AbsEvent {
+		private const int EncodingBufferSize = 1024 * 1024;
+
+		private readonly SseHandler Parent;
+		public readonly string Name;
+
+		private readonly byte[] encodingBuffer;
+		private readonly StringBuilder stringBuilder = new StringBuilder ();
+
+		private readonly List<HttpListenerResponse> openStreams = new List<HttpListenerResponse> ();
+
+		private readonly BlockingQueue<(string _eventName, object _data)> sendQueue =
+			new BlockingQueue<(string _eventName, object _data)> ();
+
+		private int currentlyOpen;
+		private int totalOpened;
+		private int totalClosed;
+
+		protected AbsEvent (SseHandler _parent, bool _reuseEncodingBuffer = true, string _name = null) {
+			Name = _name ?? GetType ().Name;
+			Parent = _parent;
+			if (_reuseEncodingBuffer) {
+				encodingBuffer = new byte[EncodingBufferSize];
+			}
+		}
+
+		public void AddListener (HttpListenerResponse _resp) {
+			totalOpened++;
+			currentlyOpen++;
+
+			openStreams.Add (_resp);
+		}
+
+		protected void SendData (string _eventName, object _data) {
+			sendQueue.Enqueue ((_eventName, _data));
+			Parent.SignalSendQueue ();
+		}
+
+
+		public void ProcessSendQueue () {
+			while (sendQueue.HasData ()) {
+				(string eventName, object 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 ("");
+				string output = stringBuilder.ToString ();
+				stringBuilder.Clear ();
+
+				byte[] buf;
+				int bytesToSend;
+				if (encodingBuffer != null) {
+					buf = encodingBuffer;
+					try {
+						bytesToSend = Encoding.UTF8.GetBytes (output, 0, output.Length, buf, 0);
+					} catch (ArgumentException e) {
+						logError ("Exception while encoding data for output, most likely exceeding buffer size:", false);
+						Log.Exception (e);
+						return;
+					}
+				} else {
+					buf = Encoding.UTF8.GetBytes (output);
+					bytesToSend = buf.Length;
+				}
+
+				for (int i = openStreams.Count - 1; i >= 0; i--) {
+					HttpListenerResponse resp = openStreams [i];
+					try {
+						if (resp.OutputStream.CanWrite) {
+							resp.OutputStream.Write (buf, 0, bytesToSend);
+							resp.OutputStream.Flush ();
+						} else {
+							currentlyOpen--;
+							totalClosed++;
+
+							logError ("Can not write to endpoint, closing", true);
+							openStreams.RemoveAt (i);
+							resp.Close ();
+						}
+					} catch (IOException e) {
+						currentlyOpen--;
+						totalClosed++;
+
+						openStreams.RemoveAt (i);
+
+						if (e.InnerException is SocketException se) {
+							if (se.SocketErrorCode != SocketError.ConnectionAborted && se.SocketErrorCode != SocketError.Shutdown) {
+								logError ($"SocketError ({se.SocketErrorCode}) while trying to write", true);
+							}
+						} else {
+							logError ("IOException while trying to write:", true);
+							Log.Exception (e);
+						}
+					} catch (Exception e) {
+						currentlyOpen--;
+						totalClosed++;
+
+						openStreams.RemoveAt (i);
+						logError ("Exception while trying to write:", true);
+						Log.Exception (e);
+						resp.Close ();
+					}
+				}
+			}
+		}
+
+		protected void logError (string _message, bool _printConnections) {
+			Log.Error (_printConnections
+				? $"SSE ({Name}): {_message} (Left open: {currentlyOpen}, total opened: {totalOpened}, closed: {totalClosed})"
+				: $"SSE ({Name}): {_message}");
+		}
+
+		public virtual int DefaultPermissionLevel () {
+			return 0;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/SSE/EventLog.cs
===================================================================
--- binary-improvements2/WebServer/src/SSE/EventLog.cs	(revision 391)
+++ binary-improvements2/WebServer/src/SSE/EventLog.cs	(revision 391)
@@ -0,0 +1,28 @@
+using System;
+using AllocsFixes.JSON;
+using JetBrains.Annotations;
+using UnityEngine;
+using Webserver.UrlHandlers;
+
+namespace Webserver.SSE {
+	[UsedImplicitly]
+	public class EventLog : AbsEvent {
+		public EventLog (SseHandler _parent) : base (_parent, _name: "log") {
+			Log.LogCallbacksExtended += LogCallback;
+		}
+
+		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);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/UrlHandlers/AbsHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/AbsHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/AbsHandler.cs	(revision 391)
@@ -0,0 +1,29 @@
+namespace Webserver.UrlHandlers {
+	public abstract class AbsHandler {
+		protected readonly string moduleName;
+		protected string urlBasePath;
+		protected Web parent;
+
+		public string ModuleName => moduleName;
+		public string UrlBasePath => urlBasePath;
+
+		protected AbsHandler (string _moduleName, int _defaultPermissionLevel = 0) {
+			moduleName = _moduleName;
+			WebPermissions.Instance.AddKnownModule (_moduleName, _defaultPermissionLevel);
+		}
+
+		public abstract void HandleRequest (RequestContext _context);
+
+		public virtual bool IsAuthorizedForHandler (WebConnection _user, int _permissionLevel) {
+			return moduleName == null || WebPermissions.Instance.ModuleAllowedWithLevel (moduleName, _permissionLevel);
+		}
+
+		public virtual void Shutdown () {
+		}
+
+		public virtual void SetBasePathAndParent (Web _parent, string _relativePath) {
+			parent = _parent;
+			urlBasePath = _relativePath;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/UrlHandlers/ApiHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/ApiHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/ApiHandler.cs	(revision 391)
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Reflection;
+using Webserver.WebAPI;
+
+namespace Webserver.UrlHandlers {
+	public class ApiHandler : AbsHandler {
+		private readonly Dictionary<string, AbsWebAPI> apis = new CaseInsensitiveStringDictionary<AbsWebAPI> ();
+
+		public ApiHandler () : base (null) {
+
+		}
+
+		public override void SetBasePathAndParent (Web _parent, string _relativePath) {
+			base.SetBasePathAndParent (_parent, _relativePath);
+
+			Type[] apiWithParentCtorTypes = { typeof (Web) };
+			object[] apiWithParentCtorArgs = { _parent };
+
+			Type[] apiEmptyCtorTypes = { };
+			object[] apiEmptyCtorArgs = { };
+			
+			
+			ReflectionHelpers.FindTypesImplementingBase (typeof (AbsWebAPI), _type => {
+				ConstructorInfo ctor = _type.GetConstructor (apiWithParentCtorTypes);
+				if (ctor != null) {
+					AbsWebAPI apiInstance = (AbsWebAPI) ctor.Invoke (apiWithParentCtorArgs);
+					addApi (apiInstance);
+					return;
+				}
+					
+				ctor = _type.GetConstructor (apiEmptyCtorTypes);
+				if (ctor != null) {
+					AbsWebAPI apiInstance = (AbsWebAPI) ctor.Invoke (apiEmptyCtorArgs);
+					addApi (apiInstance);
+				}
+			});
+
+			// Permissions that don't map to a real API
+			addApi (new Null ("viewallclaims"));
+			addApi (new Null ("viewallplayers"));
+		}
+
+		private void addApi (AbsWebAPI _api) {
+			apis.Add (_api.Name, _api);
+			WebPermissions.Instance.AddKnownModule ("webapi." + _api.Name, _api.DefaultPermissionLevel ());
+		}
+
+#if ENABLE_PROFILER
+		private static readonly UnityEngine.Profiling.CustomSampler apiHandlerSampler = UnityEngine.Profiling.CustomSampler.Create ("API_Handler");
+#endif
+
+		public override void HandleRequest (RequestContext _context) {
+
+			string apiName;
+			string subPath = null;
+
+			int pathSeparatorIndex = _context.RequestPath.IndexOf ('/', urlBasePath.Length);
+			if (pathSeparatorIndex >= 0) {
+				apiName = _context.RequestPath.Substring (urlBasePath.Length, pathSeparatorIndex - urlBasePath.Length);
+				subPath = _context.RequestPath.Substring (pathSeparatorIndex + 1);
+			} else {
+				apiName = _context.RequestPath.Substring (urlBasePath.Length);
+			}
+			
+			if (!apis.TryGetValue (apiName, out AbsWebAPI api)) {
+				Log.Out ($"Error in {nameof(ApiHandler)}.HandleRequest(): No handler found for API \"{apiName}\"");
+				_context.Response.StatusCode = (int) HttpStatusCode.NotFound;
+				return;
+			}
+
+			if (!IsAuthorizedForApi (apiName, _context.PermissionLevel)) {
+				_context.Response.StatusCode = (int) HttpStatusCode.Forbidden;
+				if (_context.Connection != null) {
+					//Log.Out ($"{nameof(ApiHandler)}: user '{user.SteamID}' not allowed to execute '{apiName}'");
+				}
+
+				return;
+			}
+
+			_context.RequestPath = subPath;
+
+			try {
+#if ENABLE_PROFILER
+				apiHandlerSampler.Begin ();
+#endif
+				api.HandleRequest (_context);
+#if ENABLE_PROFILER
+				apiHandlerSampler.End ();
+#endif
+			} catch (Exception e) {
+				Log.Error ($"Error in {nameof(ApiHandler)}.HandleRequest(): Handler {api.Name} threw an exception:");
+				Log.Exception (e);
+				_context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
+			}
+		}
+
+		private bool IsAuthorizedForApi (string _apiName, int _permissionLevel) {
+			return WebPermissions.Instance.ModuleAllowedWithLevel ("webapi." + _apiName, _permissionLevel);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/UrlHandlers/ItemIconHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/ItemIconHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/ItemIconHandler.cs	(revision 391)
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using AllocsFixes;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace Webserver.UrlHandlers {
+	public class ItemIconHandler : AbsHandler {
+		private readonly Dictionary<string, byte[]> icons = new Dictionary<string, byte[]> ();
+		private readonly bool logMissingFiles;
+
+		private bool loaded;
+
+		static ItemIconHandler () {
+			Instance = null;
+		}
+
+		public ItemIconHandler (bool _logMissingFiles, string _moduleName = null) : base (_moduleName) {
+			logMissingFiles = _logMissingFiles;
+			Instance = this;
+		}
+
+		public static ItemIconHandler Instance { get; private set; }
+
+		public override void HandleRequest (RequestContext _context) {
+			if (!loaded) {
+				_context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
+				Log.Out ("Web:IconHandler: Icons not loaded");
+				return;
+			}
+
+			string requestFileName = _context.RequestPath.Remove (0, urlBasePath.Length);
+			requestFileName = requestFileName.Remove (requestFileName.LastIndexOf ('.'));
+
+			if (icons.ContainsKey (requestFileName) && _context.RequestPath.EndsWith (".png", StringComparison.OrdinalIgnoreCase)) {
+				_context.Response.ContentType = MimeType.GetMimeType (".png");
+
+				byte[] itemIconData = icons [requestFileName];
+
+				_context.Response.ContentLength64 = itemIconData.Length;
+				_context.Response.OutputStream.Write (itemIconData, 0, itemIconData.Length);
+			} else {
+				_context.Response.StatusCode = (int) HttpStatusCode.NotFound;
+				if (logMissingFiles) {
+					Log.Out ("Web:IconHandler:FileNotFound: \"" + _context.RequestPath + "\" ");
+				}
+			}
+		}
+
+		public bool LoadIcons () {
+			
+			lock (icons) {
+				if (loaded) {
+					return true;
+				}
+
+				MicroStopwatch microStopwatch = new MicroStopwatch ();
+
+				// Get list of used tints for all items
+				Dictionary<string, List<Color>> tintedIcons = new Dictionary<string, List<Color>> ();
+				foreach (ItemClass ic in ItemClass.list) {
+					if (ic == null) {
+						continue;
+					}
+
+					Color tintColor = ic.GetIconTint ();
+					if (tintColor == Color.white) {
+						continue;
+					}
+
+					string name = ic.GetIconName ();
+					if (!tintedIcons.ContainsKey (name)) {
+						tintedIcons.Add (name, new List<Color> ());
+					}
+
+					List<Color> list = tintedIcons [name];
+					list.Add (tintColor);
+				}
+
+				try {
+					loadIconsFromFolder (GameIO.GetGameDir ("Data/ItemIcons"), tintedIcons);
+				} catch (Exception e) {
+					Log.Error ("Failed loading icons from base game");
+					Log.Exception (e);
+				}
+
+				// Load icons from mods
+				foreach (Mod mod in ModManager.GetLoadedMods ()) {
+					try {
+						string modIconsPath = mod.Path + "/ItemIcons";
+						loadIconsFromFolder (modIconsPath, tintedIcons);
+					} catch (Exception e) {
+						Log.Error ("Failed loading icons from mod " + mod.ModInfo.Name.Value);
+						Log.Exception (e);
+					}
+				}
+
+				loaded = true;
+				Log.Out ("Web:IconHandler: Icons loaded - {0} ms", microStopwatch.ElapsedMilliseconds);
+
+				return true;
+			}
+		}
+
+		private void loadIconsFromFolder (string _path, Dictionary<string, List<Color>> _tintedIcons) {
+			if (!Directory.Exists (_path)) {
+				return;
+			}
+
+			foreach (string file in Directory.GetFiles (_path)) {
+				try {
+					if (!file.EndsWith (".png", StringComparison.OrdinalIgnoreCase)) {
+						continue;
+					}
+
+					string name = Path.GetFileNameWithoutExtension (file);
+					Texture2D tex = new Texture2D (1, 1, TextureFormat.ARGB32, false);
+					if (!tex.LoadImage (File.ReadAllBytes (file))) {
+						continue;
+					}
+
+					AddIcon (name, tex, _tintedIcons);
+
+					Object.Destroy (tex);
+				} catch (Exception e) {
+					Log.Exception (e);
+				}
+			}
+		}
+
+		private void AddIcon (string _name, Texture2D _tex, Dictionary<string, List<Color>> _tintedIcons) {
+			icons [_name + "__FFFFFF"] = _tex.EncodeToPNG ();
+
+			if (!_tintedIcons.ContainsKey (_name)) {
+				return;
+			}
+
+			foreach (Color c in _tintedIcons [_name]) {
+				string tintedName = _name + "__" + AllocsUtils.ColorToHex (c);
+				if (icons.ContainsKey (tintedName)) {
+					continue;
+				}
+
+				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);
+					}
+				}
+
+				icons [tintedName] = tintedTex.EncodeToPNG ();
+
+				Object.Destroy (tintedTex);
+			}
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/UrlHandlers/RewriteHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/RewriteHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/RewriteHandler.cs	(revision 391)
@@ -0,0 +1,16 @@
+namespace Webserver.UrlHandlers {
+	public class RewriteHandler : AbsHandler {
+		private readonly string target;
+		private readonly bool fixedTarget;
+
+		public RewriteHandler (string _target, bool _fixedTarget = false) : base (null) {
+			target = _target;
+			fixedTarget = _fixedTarget;
+		}
+
+		public override void HandleRequest (RequestContext _context) {
+			_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 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/SessionHandler.cs	(revision 391)
@@ -0,0 +1,97 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+
+namespace Webserver.UrlHandlers {
+	public class SessionHandler : AbsHandler {
+		private const string pageBasePath = "/";
+		private const string steamOpenIdVerifyUrl = "verifysteamopenid";
+		private const string steamLoginUrl = "loginsteam";
+		
+		private readonly string footer = "";
+		private readonly string header = "";
+
+		private readonly ConnectionHandler connectionHandler;
+
+		public SessionHandler (string _dataFolder, ConnectionHandler _connectionHandler) : base (null) {
+			connectionHandler = _connectionHandler;
+			
+			if (File.Exists (_dataFolder + "/sessionheader.tmpl")) {
+				header = File.ReadAllText (_dataFolder + "/sessionheader.tmpl");
+			}
+
+			if (File.Exists (_dataFolder + "/sessionfooter.tmpl")) {
+				footer = File.ReadAllText (_dataFolder + "/sessionfooter.tmpl");
+			}
+		}
+
+		public override void HandleRequest (RequestContext _context) {
+			
+			IPEndPoint reqRemoteEndPoint = _context.Request.RemoteEndPoint;
+			if (reqRemoteEndPoint == null) {
+				_context.Response.Redirect (pageBasePath);
+				return;
+			}
+
+			string subpath = _context.RequestPath.Remove (0, urlBasePath.Length);
+
+			StringBuilder result = new StringBuilder ();
+			result.Append (header);
+
+			if (subpath.StartsWith (steamOpenIdVerifyUrl)) {
+				string remoteEndpointString = reqRemoteEndPoint.ToString ();
+
+				try {
+					ulong id = OpenID.Validate (_context.Request);
+					if (id > 0) {
+						WebConnection con = connectionHandler.LogIn (id, reqRemoteEndPoint.Address);
+						int level = GameManager.Instance.adminTools.GetUserPermissionLevel (con.UserId);
+						Log.Out ("Steam OpenID login from {0} with ID {1}, permission level {2}",
+							remoteEndpointString, con.UserId, 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 ("Error validating login:");
+					Log.Exception (e);
+				}
+
+				Log.Out ($"Steam OpenID login failed from {remoteEndpointString}");
+				result.Append ($"<h1>Login failed, <a href=\"{pageBasePath}\">click to return to main page</a>.</h1>");
+			} else if (subpath.StartsWith ("logout")) {
+				if (_context.Connection != null) {
+					connectionHandler.LogOut (_context.Connection.SessionID);
+					Cookie cookie = new Cookie ("sid", "", "/") {
+						Expired = true
+					};
+					_context.Response.AppendCookie (cookie);
+					_context.Response.Redirect (pageBasePath);
+					return;
+				}
+
+				result.Append ($"<h1>Not logged in, <a href=\"{pageBasePath}\">click to return to main page</a>.</h1>");
+			} else if (subpath.StartsWith (steamLoginUrl)) {
+				string host = (WebUtils.IsSslRedirected (_context.Request) ? "https://" : "http://") + _context.Request.UserHostName;
+				string url = OpenID.GetOpenIdLoginUrl (host, host + urlBasePath + steamOpenIdVerifyUrl);
+				_context.Response.Redirect (url);
+				return;
+			} else {
+				result.Append ($"<h1>Unknown command, <a href=\"{pageBasePath}\">click to return to main page</a>.</h1>");
+			}
+
+			result.Append (footer);
+
+			WebUtils.WriteText (_context.Response, result.ToString (), _mimeType: WebUtils.MimeHtml);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/UrlHandlers/SimpleRedirectHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/SimpleRedirectHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/SimpleRedirectHandler.cs	(revision 391)
@@ -0,0 +1,13 @@
+namespace Webserver.UrlHandlers {
+	public class SimpleRedirectHandler : AbsHandler {
+		private readonly string target;
+
+		public SimpleRedirectHandler (string _target) : base (null) {
+			target = _target;
+		}
+
+		public override void HandleRequest (RequestContext _context) {
+			_context.Response.Redirect (target);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs	(revision 391)
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Reflection;
+using System.Threading;
+using Webserver.SSE;
+
+// Implemented following HTML spec
+// https://html.spec.whatwg.org/multipage/server-sent-events.html
+
+namespace Webserver.UrlHandlers {
+	public class SseHandler : AbsHandler {
+		private readonly Dictionary<string, AbsEvent> events = new CaseInsensitiveStringDictionary<AbsEvent> ();
+
+		private ThreadManager.ThreadInfo queueThead;
+		private readonly AutoResetEvent evSendRequest = new AutoResetEvent (false);
+		private bool shutdown;
+
+		public SseHandler (string _moduleName = null) : base (_moduleName) {
+			Type[] ctorTypes = { typeof (SseHandler) };
+			object[] ctorParams = { this };
+
+			foreach (Type t in Assembly.GetExecutingAssembly ().GetTypes ()) {
+				if (t.IsAbstract || !t.IsSubclassOf (typeof (AbsEvent))) {
+					continue;
+				}
+
+				ConstructorInfo ctor = t.GetConstructor (ctorTypes);
+				if (ctor == null) {
+					continue;
+				}
+
+				AbsEvent apiInstance = (AbsEvent)ctor.Invoke (ctorParams);
+				AddEvent (apiInstance.Name, apiInstance);
+			}
+		}
+
+		public override void SetBasePathAndParent (Web _parent, string _relativePath) {
+			base.SetBasePathAndParent (_parent, _relativePath);
+
+			queueThead = ThreadManager.StartThread ("SSE-Processing_" + urlBasePath, QueueProcessThread, ThreadPriority.BelowNormal,
+				_useRealThread: true);
+		}
+
+		public override void Shutdown () {
+			base.Shutdown ();
+			shutdown = true;
+			SignalSendQueue ();
+		}
+
+		public void AddEvent (string _eventName, AbsEvent _eventInstance) {
+			events.Add (_eventName, _eventInstance);
+			WebPermissions.Instance.AddKnownModule ("webevent." + _eventName, _eventInstance.DefaultPermissionLevel ());
+		}
+
+		public override void HandleRequest (RequestContext _context) {
+			string eventName = _context.RequestPath.Remove (0, urlBasePath.Length);
+
+			if (!events.TryGetValue (eventName, out AbsEvent eventInstance)) {
+				Log.Out ($"Error in {nameof (SseHandler)}.HandleRequest(): No handler found for event \"{eventName}\"");
+				_context.Response.StatusCode = (int)HttpStatusCode.NotFound;
+				return;
+			}
+
+			if (!IsAuthorizedForEvent (eventName, _context.PermissionLevel)) {
+				_context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+				if (_context.Connection != null) {
+					//Log.Out ($"{nameof(SseHandler)}: user '{user.SteamID}' not allowed to access '{eventName}'");
+				}
+
+				return;
+			}
+
+			try {
+				eventInstance.AddListener (_context.Response);
+
+				// Keep the request open
+				_context.Response.SendChunked = true;
+
+				_context.Response.AddHeader ("Content-Type", "text/event-stream");
+				_context.Response.OutputStream.Flush ();
+			} catch (Exception e) {
+				Log.Error ($"Error in {nameof (SseHandler)}.HandleRequest(): Handler {eventInstance.Name} threw an exception:");
+				Log.Exception (e);
+				_context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+			}
+		}
+
+		private bool IsAuthorizedForEvent (string _eventName, int _permissionLevel) {
+			return WebPermissions.Instance.ModuleAllowedWithLevel ("webevent." + _eventName, _permissionLevel);
+		}
+
+		private void QueueProcessThread (ThreadManager.ThreadInfo _threadInfo) {
+			while (!shutdown && !_threadInfo.TerminationRequested ()) {
+				evSendRequest.WaitOne (500);
+
+				foreach ((string eventName, AbsEvent eventHandler) in events) {
+					try {
+						eventHandler.ProcessSendQueue ();
+					} catch (Exception e) {
+						Log.Error ($"SSE ({eventName}): Error processing send queue");
+						Log.Exception (e);
+					}
+				}
+			}
+		}
+
+		public void SignalSendQueue () {
+			evSendRequest.Set ();
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/UrlHandlers/StaticHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/StaticHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/StaticHandler.cs	(revision 391)
@@ -0,0 +1,35 @@
+using System.IO;
+using System.Net;
+using AllocsFixes.FileCache;
+
+namespace Webserver.UrlHandlers {
+	public class StaticHandler : AbsHandler {
+		private readonly AbstractCache cache;
+		private readonly string datapath;
+		private readonly bool logMissingFiles;
+
+		public StaticHandler (string _filePath, AbstractCache _cache, bool _logMissingFiles,
+			string _moduleName = null) : base (_moduleName) {
+			datapath = _filePath + (_filePath [_filePath.Length - 1] == '/' ? "" : "/");
+			cache = _cache;
+			logMissingFiles = _logMissingFiles;
+		}
+
+		public override void HandleRequest (RequestContext _context) {
+			string fn = _context.RequestPath.Remove (0, urlBasePath.Length);
+
+			byte[] content = cache.GetFileContent (datapath + fn);
+
+			if (content != null) {
+				_context.Response.ContentType = MimeType.GetMimeType (Path.GetExtension (fn));
+				_context.Response.ContentLength64 = content.Length;
+				_context.Response.OutputStream.Write (content, 0, content.Length);
+			} else {
+				_context.Response.StatusCode = (int) HttpStatusCode.NotFound;
+				if (logMissingFiles) {
+					Log.Out ("Web:Static:FileNotFound: \"" + _context.RequestPath + "\" @ \"" + datapath + fn + "\"");
+				}
+			}
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs	(revision 391)
+++ binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs	(revision 391)
@@ -0,0 +1,27 @@
+using AllocsFixes.JSON;
+
+namespace Webserver.UrlHandlers {
+	public class UserStatusHandler : AbsHandler {
+		public UserStatusHandler (string _moduleName = null) : base (_moduleName) {
+		}
+
+		public override void HandleRequest (RequestContext _context) {
+			JsonObject result = new JsonObject ();
+
+			result.Add ("loggedin", new JsonBoolean (_context.Connection != null));
+			result.Add ("username", new JsonString (_context.Connection != null ? _context.Connection.UserId.ToString () : string.Empty));
+
+			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);
+			}
+
+			result.Add ("permissions", perms);
+
+			WebUtils.WriteJson (_context.Response, result);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/Web.cs
===================================================================
--- binary-improvements2/WebServer/src/Web.cs	(revision 391)
+++ binary-improvements2/WebServer/src/Web.cs	(revision 391)
@@ -0,0 +1,310 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Sockets;
+using AllocsFixes.FileCache;
+using MapRendering;
+using SpaceWizards.HttpListener;
+using UnityEngine;
+using Webserver.UrlHandlers;
+using Cookie = System.Net.Cookie;
+using HttpStatusCode = System.Net.HttpStatusCode;
+using IPEndPoint = System.Net.IPEndPoint;
+
+namespace Webserver {
+	public class Web : IConsoleServer {
+		private const int guestPermissionLevel = 2000;
+		private const string indexPageUrl = "/app";
+		
+		private readonly List<AbsHandler> handlers = new List<AbsHandler> ();
+		public readonly List<WebMod> webMods = new List<WebMod> ();
+		private readonly ConnectionHandler connectionHandler;
+
+		private readonly HttpListener listener = new HttpListener ();
+		private readonly Version httpProtocolVersion = new Version(1, 1);
+
+		public Web (string _modInstancePath) {
+			try {
+				int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
+				if (webPort < 1 || webPort > 65533) {
+					Log.Out ("Webserver not started (ControlPanelPort not within 1-65533)");
+					return;
+				}
+
+				// TODO: Remove once this becomes the default control panel
+				webPort += 2;
+
+				if (!HttpListener.IsSupported) {
+					Log.Out ("Webserver not started (needs Windows XP SP2, Server 2003 or later or Mono)");
+					return;
+				}
+
+				// TODO: Read from config
+				bool useStaticCache = false;
+
+				string webfilesFolder = _modInstancePath + "/webserver";
+				string webfilesFolderLegacy = _modInstancePath + "/weblegacy";
+
+				connectionHandler = new ConnectionHandler ();
+				
+				RegisterPathHandler ("/", new RewriteHandler ("/files/"));
+
+				// React virtual routing
+				RegisterPathHandler ("/app", new RewriteHandler ("/files/index.html", true));
+				
+				// Legacy web page
+				RegisterPathHandler ("/weblegacy", new StaticHandler (
+					webfilesFolderLegacy,
+					useStaticCache ? (AbstractCache)new SimpleCache () : new DirectAccess (),
+					false)
+				);
+				
+				// Do mods relatively early as they should not be requested a lot, unlike the later registrations, especially for API and map tiles
+				RegisterWebMods (useStaticCache);
+
+				RegisterPathHandler ("/session/", new SessionHandler (webfilesFolder, connectionHandler));
+				RegisterPathHandler ("/userstatus", new UserStatusHandler ());
+				RegisterPathHandler ("/sse/", new SseHandler ());
+				RegisterPathHandler ("/files/", new StaticHandler (
+					webfilesFolder,
+					useStaticCache ? (AbstractCache) 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 ());
+
+				listener.Prefixes.Add ($"http://+:{webPort}/");
+				// listener.Prefixes.Add ($"http://[::1]:{webPort}/");
+				listener.Start ();
+				listener.BeginGetContext (HandleRequest, listener);
+
+				SdtdConsole.Instance.RegisterServer (this);
+
+				Log.Out ("Started Webserver on " + webPort);
+			} catch (Exception e) {
+				Log.Error ("Error in Web.ctor: ");
+				Log.Exception (e);
+			}
+		}
+
+		public void RegisterPathHandler (string _urlBasePath, AbsHandler _handler) {
+			foreach (AbsHandler handler in handlers) {
+				if (handler.UrlBasePath != _urlBasePath) {
+					continue;
+				}
+
+				Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
+				return;
+			}
+			
+			handlers.Add (_handler);
+			_handler.SetBasePathAndParent (this, _urlBasePath);
+		}
+
+		private void RegisterWebMods (bool _useStaticCache) {
+			foreach (Mod mod in ModManager.GetLoadedMods ()) {
+				try {
+					string webModPath = mod.Path + "/WebMod";
+					if (!Directory.Exists (webModPath)) {
+						continue;
+					}
+
+					try {
+						WebMod webMod = new WebMod (this, mod, _useStaticCache);
+						webMods.Add (webMod);
+					} catch (InvalidDataException e) {
+						Log.Error ($"Could not load webmod from mod {mod.ModInfo.Name.Value}: {e.Message}");
+					}
+				} catch (Exception e) {
+					Log.Error ("Failed loading web mods from mod " + mod.ModInfo.Name.Value);
+					Log.Exception (e);
+				}
+			}
+		}
+
+		public void Disconnect () {
+			try {
+				listener.Stop ();
+				listener.Close ();
+			} catch (Exception e) {
+				Log.Out ("Error in Web.Disconnect: " + e);
+			}
+		}
+
+		public void Shutdown () {
+			foreach (AbsHandler handler in handlers) {
+				handler.Shutdown ();
+			}
+		}
+
+		public void SendLine (string _line) {
+			connectionHandler.SendLine (_line);
+		}
+
+		public void SendLog (string _formattedMessage, string _plainMessage, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
+			// Do nothing, handled by LogBuffer internally
+		}
+
+#if ENABLE_PROFILER
+		private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
+		private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");
+#endif
+
+		private void HandleRequest (IAsyncResult _result) {
+			HttpListener listenerInstance = (HttpListener)_result.AsyncState;
+			if (!listenerInstance.IsListening) {
+				return;
+			}
+
+#if ENABLE_PROFILER
+			UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
+			HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
+			try {
+#else
+			HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
+			listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
+#endif
+			try {
+				HttpListenerRequest request = ctx.Request;
+				HttpListenerResponse response = ctx.Response;
+				response.SendChunked = false;
+
+				response.ProtocolVersion = httpProtocolVersion;
+
+				// No game yet -> fail request
+				if (GameManager.Instance.World == null) {
+					response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
+					return;
+				}
+
+				if (request.Url == null) {
+					response.StatusCode = (int) HttpStatusCode.BadRequest;
+					return;
+				}
+
+#if ENABLE_PROFILER
+				authSampler.Begin ();
+#endif
+				int permissionLevel = DoAuthentication (request, out WebConnection conn);
+#if ENABLE_PROFILER
+				authSampler.End ();
+#endif
+
+				//Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
+
+				if (conn != null) {
+					Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
+						Expired = false,
+						Expires = DateTime.MinValue,
+						HttpOnly = true,
+						Secure = false
+					};
+					response.AppendCookie (cookie);
+				}
+
+				string requestPath = request.Url.AbsolutePath;
+
+				if (requestPath.Length < 2) {
+					response.Redirect (indexPageUrl);
+					return;
+				}
+
+				RequestContext context = new RequestContext (requestPath, request, response, conn, permissionLevel);
+				
+				ApplyPathHandler (context);
+
+			} catch (IOException e) {
+				if (e.InnerException is SocketException) {
+					Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " + e.InnerException.Message);
+				} else {
+					Log.Out ("Error (IO) in Web.HandleRequest(): " + e);
+				}
+			} catch (Exception e) {
+				Log.Error ("Error in Web.HandleRequest(): ");
+				Log.Exception (e);
+			} finally {
+				if (!ctx.Response.SendChunked) {
+					ctx.Response.Close ();
+				}
+			}
+#if ENABLE_PROFILER
+			} finally {
+				listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
+				UnityEngine.Profiling.Profiler.EndThreadProfiling ();
+			}
+#endif
+		}
+
+		public void ApplyPathHandler (RequestContext _context) {
+			for (int i = handlers.Count - 1; i >= 0; i--) {
+				AbsHandler handler = handlers [i];
+
+				if (!_context.RequestPath.StartsWith (handler.UrlBasePath)) {
+					continue;
+				}
+
+				if (!handler.IsAuthorizedForHandler (_context.Connection, _context.PermissionLevel)) {
+					_context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+					if (_context.Connection != null) {
+						//Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", _con.SteamID, handler.ModuleName);
+					}
+				} else {
+#if ENABLE_PROFILER
+					handlerSampler.Begin ();
+#endif
+					handler.HandleRequest (_context);
+#if ENABLE_PROFILER
+					handlerSampler.End ();
+#endif
+				}
+
+				return;
+			}
+
+			// Not really relevant for non-debugging purposes:
+			//Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + _requestPath + "\"");
+			_context.Response.StatusCode = (int) HttpStatusCode.NotFound;
+		}
+
+		private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
+			_con = null;
+
+			string sessionId = _req.Cookies ["sid"]?.Value;
+
+			IPEndPoint reqRemoteEndPoint = _req.RemoteEndPoint;
+			if (reqRemoteEndPoint == null) {
+				Log.Warning ("No RemoteEndPoint on web request");
+				return guestPermissionLevel;
+			}
+			
+			if (!string.IsNullOrEmpty (sessionId)) {
+				_con = connectionHandler.IsLoggedIn (sessionId, reqRemoteEndPoint.Address);
+				if (_con != null) {
+					return GameManager.Instance.adminTools.GetUserPermissionLevel (_con.UserId);
+				}
+			}
+
+			string remoteEndpointString = reqRemoteEndPoint.ToString ();
+
+			if (_req.QueryString ["adminuser"] == null || _req.QueryString ["admintoken"] == null) {
+				return guestPermissionLevel;
+			}
+
+			WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"],
+				_req.QueryString ["admintoken"]);
+			if (admin != null) {
+				return admin.permissionLevel;
+			}
+
+			Log.Warning ("Invalid Admintoken used from " + remoteEndpointString);
+
+			return guestPermissionLevel;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs	(revision 391)
@@ -0,0 +1,172 @@
+using System;
+using System.IO;
+using System.Net;
+using AllocsFixes.JSON;
+
+namespace Webserver.WebAPI {
+	public abstract class AbsRestApi : AbsWebAPI {
+		public sealed override void HandleRequest (RequestContext _context) {
+			JsonNode jsonBody = null;
+
+			if (_context.Request.HasEntityBody) {
+				string body = new StreamReader (_context.Request.InputStream).ReadToEnd ();
+
+				if (!string.IsNullOrEmpty (body)) {
+					try {
+						jsonBody = Parser.Parse (body);
+					} catch (Exception e) {
+						SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "INVALID_BODY", e);
+						return;
+					}
+				}
+			}
+
+			try {
+				switch (_context.Request.HttpMethod) {
+					case "GET":
+						if (jsonBody != null) {
+							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, jsonBody, "GET_WITH_BODY");
+							return;
+						}
+
+						HandleRestGet (_context);
+						return;
+					case "POST":
+						if (!string.IsNullOrEmpty (_context.RequestPath)) {
+							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, jsonBody, "POST_WITH_ID");
+							return;
+						}
+
+						if (jsonBody == null) {
+							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "POST_WITHOUT_BODY");
+							return;
+						}
+
+						HandleRestPost (_context, jsonBody);
+						return;
+					case "PUT":
+						if (string.IsNullOrEmpty (_context.RequestPath)) {
+							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, jsonBody, "PUT_WITHOUT_ID");
+							return;
+						}
+
+						if (jsonBody == null) {
+							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "PUT_WITHOUT_BODY");
+							return;
+						}
+
+						HandleRestPut (_context, jsonBody);
+						return;
+					case "DELETE":
+						if (string.IsNullOrEmpty (_context.RequestPath)) {
+							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, jsonBody, "DELETE_WITHOUT_ID");
+							return;
+						}
+
+						if (jsonBody != null) {
+							SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "DELETE_WITH_BODY");
+							return;
+						}
+
+						HandleRestDelete (_context);
+						return;
+					default:
+						SendEnvelopedResult (_context, null, HttpStatusCode.BadRequest, null, "INVALID_METHOD");
+						return;
+				}
+			} catch (Exception e) {
+				SendEnvelopedResult (_context, null, HttpStatusCode.InternalServerError, jsonBody, "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 bool TryGetJsonField (JsonObject _jsonObject, string _fieldName, out int _value) {
+			_value = default;
+			
+			if (!_jsonObject.TryGetValue (_fieldName, out JsonNode fieldNode)) {
+				return false;
+			}
+
+			if (!(fieldNode is JsonValue valueField)) {
+				return false;
+			}
+
+			try {
+				_value = valueField.AsInt;
+				return true;
+			} catch (Exception) {
+				return false;
+			}
+		}
+
+		protected bool TryGetJsonField (JsonObject _jsonObject, string _fieldName, out double _value) {
+			_value = default;
+			
+			if (!_jsonObject.TryGetValue (_fieldName, out JsonNode fieldNode)) {
+				return false;
+			}
+
+			if (!(fieldNode is JsonValue valueField)) {
+				return false;
+			}
+
+			try {
+				_value = valueField.AsDouble;
+				return true;
+			} catch (Exception) {
+				return false;
+			}
+		}
+
+		protected bool TryGetJsonField (JsonObject _jsonObject, string _fieldName, out string _value) {
+			_value = default;
+			
+			if (!_jsonObject.TryGetValue (_fieldName, out JsonNode fieldNode)) {
+				return false;
+			}
+
+			if (!(fieldNode is JsonValue valueField)) {
+				return false;
+			}
+
+			try {
+				_value = valueField.AsString;
+				return true;
+			} catch (Exception) {
+				return false;
+			}
+		}
+
+		protected abstract void HandleRestGet (RequestContext _context);
+
+		protected abstract void HandleRestPost (RequestContext _context, JsonNode _jsonBody);
+
+		protected abstract void HandleRestPut (RequestContext _context, JsonNode _jsonBody);
+
+		protected abstract void HandleRestDelete (RequestContext _context);
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/AbsWebAPI.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/AbsWebAPI.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebAPI/AbsWebAPI.cs	(revision 391)
@@ -0,0 +1,15 @@
+namespace Webserver.WebAPI {
+	public abstract class AbsWebAPI {
+		public readonly string Name;
+
+		protected AbsWebAPI (string _name = null) {
+			Name = _name ?? GetType ().Name;
+		}
+
+		public abstract void HandleRequest (RequestContext _context);
+
+		public virtual int DefaultPermissionLevel () {
+			return 0;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/ExecuteConsoleCommand.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/ExecuteConsoleCommand.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebAPI/ExecuteConsoleCommand.cs	(revision 391)
@@ -0,0 +1,52 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetAllowedCommands.cs	(revision 391)
@@ -0,0 +1,39 @@
+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;
+				}
+
+				string cmd = string.Empty;
+				foreach (string s in cc.GetCommands ()) {
+					if (s.Length > cmd.Length) {
+						cmd = s;
+					}
+				}
+
+				JsonObject cmdObj = new JsonObject ();
+				cmdObj.Add ("command", new JsonString (cmd));
+				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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetAnimalsLocation.cs	(revision 391)
@@ -0,0 +1,41 @@
+﻿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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetHostileLocation.cs	(revision 391)
@@ -0,0 +1,41 @@
+﻿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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetLandClaims.cs	(revision 391)
@@ -0,0 +1,78 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetLog.cs	(revision 391)
@@ -0,0 +1,53 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetPlayerInventories.cs	(revision 391)
@@ -0,0 +1,27 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetPlayerInventory.cs	(revision 391)
@@ -0,0 +1,129 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetPlayerList.cs	(revision 391)
@@ -0,0 +1,270 @@
+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]*)?)$");
+
+#if ENABLE_PROFILER
+		private static readonly UnityEngine.Profiling.CustomSampler jsonSerializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Build");
+#endif
+
+		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> ();
+
+#if ENABLE_PROFILER
+			jsonSerializeSampler.Begin ();
+#endif
+
+			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);
+				}
+			}
+
+#if ENABLE_PROFILER
+			jsonSerializeSampler.End ();
+#endif
+
+			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 ("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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetPlayersLocation.cs	(revision 391)
@@ -0,0 +1,63 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetPlayersOnline.cs	(revision 391)
@@ -0,0 +1,48 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetServerInfo.cs	(revision 391)
@@ -0,0 +1,47 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetStats.cs	(revision 391)
@@ -0,0 +1,28 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetWebMods.cs	(revision 391)
@@ -0,0 +1,37 @@
+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 391)
+++ binary-improvements2/WebServer/src/WebAPI/GetWebUIUpdates.cs	(revision 391)
@@ -0,0 +1,35 @@
+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/Null.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/Null.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebAPI/Null.cs	(revision 391)
@@ -0,0 +1,15 @@
+﻿using System.Text;
+
+namespace Webserver.WebAPI {
+	public class Null : AbsWebAPI {
+		public Null (string _name) : base(_name) {
+		}
+		
+		public override void HandleRequest (RequestContext _context) {
+			_context.Response.ContentLength64 = 0;
+			_context.Response.ContentType = "text/plain";
+			_context.Response.ContentEncoding = Encoding.ASCII;
+			_context.Response.OutputStream.Write (new byte[] { }, 0, 0);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebCommandResult.cs
===================================================================
--- binary-improvements2/WebServer/src/WebCommandResult.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebCommandResult.cs	(revision 391)
@@ -0,0 +1,88 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+using AllocsFixes.JSON;
+using UnityEngine;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
+
+namespace Webserver {
+	public class WebCommandResult : IConsoleConnection {
+		public enum ResultType {
+			Full,
+			ResultOnly,
+			Raw
+		}
+
+		private readonly string command;
+		private readonly string parameters;
+
+		private readonly HttpListenerResponse response;
+		private readonly ResultType responseType;
+
+		public WebCommandResult (string _command, string _parameters, ResultType _responseType, HttpListenerResponse _response) {
+			response = _response;
+			command = _command;
+			parameters = _parameters;
+			responseType = _responseType;
+		}
+
+		public void SendLines (List<string> _output) {
+			StringBuilder sb = new StringBuilder ();
+			foreach (string line in _output) {
+				sb.AppendLine (line);
+			}
+
+			try {
+				response.SendChunked = false;
+
+				if (responseType == ResultType.Raw) {
+					WebUtils.WriteText (response, sb.ToString ());
+				} else {
+					JsonNode result;
+					if (responseType == ResultType.ResultOnly) {
+						result = new JsonString (sb.ToString ());
+					} 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;
+					}
+
+					WebUtils.WriteJson (response, result);
+				}
+			} catch (IOException e) {
+				if (e.InnerException is SocketException) {
+					Log.Out ("Error in WebCommandResult.SendLines(): Remote host closed connection: " +
+					         e.InnerException.Message);
+				} else {
+					Log.Out ("Error (IO) in WebCommandResult.SendLines(): " + e);
+				}
+			} catch (Exception e) {
+				Log.Out ("Error in WebCommandResult.SendLines(): " + e);
+			} finally {
+				response?.Close ();
+			}
+		}
+
+		public void SendLine (string _text) {
+			//throw new NotImplementedException ();
+		}
+
+		public void SendLog (string _formattedMessage, string _plainMessage, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
+			//throw new NotImplementedException ();
+		}
+
+		public void EnableLogLevel (LogType _type, bool _enable) {
+			//throw new NotImplementedException ();
+		}
+
+		public string GetDescription () {
+			return "WebCommandResult_for_" + command;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebConnection.cs
===================================================================
--- binary-improvements2/WebServer/src/WebConnection.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebConnection.cs	(revision 391)
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using UnityEngine;
+
+namespace Webserver {
+	public class WebConnection : ConsoleConnectionAbstract {
+		private readonly DateTime login;
+//		private readonly List<string> outputLines = new List<string> ();
+		private DateTime lastAction;
+		private readonly string conDescription;
+
+		public string SessionID { get; }
+
+		public IPAddress Endpoint { get; }
+
+		public PlatformUserIdentifierAbs UserId { get; }
+
+		public TimeSpan Age => DateTime.Now - lastAction;
+
+		public WebConnection (string _sessionId, IPAddress _endpoint, PlatformUserIdentifierAbs _userId) {
+			SessionID = _sessionId;
+			Endpoint = _endpoint;
+			UserId = _userId;
+			login = DateTime.Now;
+			lastAction = login;
+			conDescription = "WebPanel from " + Endpoint;
+		}
+
+		public static bool CanViewAllPlayers (int _permissionLevel) {
+			return WebPermissions.Instance.ModuleAllowedWithLevel ("webapi.viewallplayers", _permissionLevel);
+		}
+
+		public static bool CanViewAllClaims (int _permissionLevel) {
+			return WebPermissions.Instance.ModuleAllowedWithLevel ("webapi.viewallclaims", _permissionLevel);
+		}
+
+		public void UpdateUsage () {
+			lastAction = DateTime.Now;
+		}
+
+		public override string GetDescription () {
+			return conDescription;
+		}
+
+		public override void SendLine (string _text) {
+//			outputLines.Add (_text);
+		}
+
+		public override void SendLines (List<string> _output) {
+//			outputLines.AddRange (_output);
+		}
+
+		public override void SendLog (string _formattedMsg, string _plainMsg, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
+			// Do nothing, handled by LogBuffer
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebMod.cs
===================================================================
--- binary-improvements2/WebServer/src/WebMod.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebMod.cs	(revision 391)
@@ -0,0 +1,50 @@
+using System.IO;
+using AllocsFixes.FileCache;
+using Webserver.UrlHandlers;
+
+namespace Webserver {
+	public class WebMod {
+		private const string modsBaseUrl = "/webmods/";
+		private const string reactBundleName = "bundle.js";
+		private const string stylingFileName = "styling.css";
+		
+		public readonly Mod ParentMod;
+		public readonly string ReactBundle; // Absolute web path to the React bundle if the mod has one, e.g. "/webmods/myMod/bundle.js"
+		public readonly string CssPath; // Absolute web path to a CSS if the mod has one, e.g. "/webmods/myMod/styling.css";
+
+		public WebMod (Web _parentWeb, Mod _parentMod, bool _useStaticCache) {
+			string folder = _parentMod.Path + "/WebMod";
+			if (!Directory.Exists (folder)) {
+				throw new InvalidDataException("No WebMod folder in mod");
+			}
+
+			string urlWebModBase = $"{modsBaseUrl}{_parentMod.FolderName}/";
+
+			ReactBundle = folder + "/" + reactBundleName;
+			if (File.Exists (ReactBundle)) {
+				ReactBundle = urlWebModBase + reactBundleName;
+			} else {
+				ReactBundle = null;
+			}
+
+			CssPath = folder + "/" + stylingFileName;
+			if (File.Exists (CssPath)) {
+				CssPath = urlWebModBase + stylingFileName;
+			} else {
+				CssPath = null;
+			}
+
+			if (ReactBundle == null && CssPath == null) {
+				throw new InvalidDataException($"WebMod folder has neither a {reactBundleName} nor a {stylingFileName}");
+			}
+
+			ParentMod = _parentMod;
+
+			_parentWeb.RegisterPathHandler (urlWebModBase, new StaticHandler (
+				folder,
+				_useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
+				false)
+			);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebPermissions.cs
===================================================================
--- binary-improvements2/WebServer/src/WebPermissions.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebPermissions.cs	(revision 391)
@@ -0,0 +1,368 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Xml;
+
+namespace Webserver {
+	public class WebPermissions {
+		private const string permissionsFile = "webpermissions.xml";
+		private static WebPermissions instance;
+		private readonly WebModulePermission defaultModulePermission = new WebModulePermission ("", 0);
+
+		private readonly Dictionary<string, WebModulePermission> knownModules =
+			new CaseInsensitiveStringDictionary<WebModulePermission> ();
+
+		private readonly Dictionary<string, AdminToken> admintokens = new CaseInsensitiveStringDictionary<AdminToken> ();
+		private FileSystemWatcher fileWatcher;
+
+		private readonly Dictionary<string, WebModulePermission> modules = new CaseInsensitiveStringDictionary<WebModulePermission> ();
+
+		private WebPermissions () {
+			allModulesList = new List<WebModulePermission> ();
+			allModulesListRo = new ReadOnlyCollection<WebModulePermission> (allModulesList);
+			Directory.CreateDirectory (GetFilePath ());
+			InitFileWatcher ();
+			Load ();
+		}
+
+		public static WebPermissions Instance {
+			get {
+				lock (typeof (WebPermissions)) {
+					return instance ??= new WebPermissions ();
+				}
+			}
+		}
+
+		public bool ModuleAllowedWithLevel (string _module, int _level) {
+			WebModulePermission permInfo = GetModulePermission (_module);
+			return permInfo.permissionLevel >= _level;
+		}
+
+		public AdminToken GetWebAdmin (string _name, string _token) {
+			if (IsAdmin (_name) && admintokens [_name].token == _token) {
+				return admintokens [_name];
+			}
+
+			return null;
+		}
+
+		public WebModulePermission GetModulePermission (string _module) {
+			if (modules.TryGetValue (_module, out WebModulePermission result)) {
+				return result;
+			}
+
+			return knownModules.TryGetValue (_module, out result) ? result : defaultModulePermission;
+		}
+
+
+		// Admins
+		public void AddAdmin (string _name, string _token, int _permissionLevel, bool _save = true) {
+			AdminToken c = new AdminToken (_name, _token, _permissionLevel);
+			lock (this) {
+				admintokens [_name] = c;
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public void RemoveAdmin (string _name, bool _save = true) {
+			lock (this) {
+				admintokens.Remove (_name);
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public bool IsAdmin (string _name) {
+			return admintokens.ContainsKey (_name);
+		}
+
+		public AdminToken[] GetAdmins () {
+			AdminToken[] result = new AdminToken[admintokens.Count];
+			admintokens.CopyValuesTo (result);
+			return result;
+		}
+
+
+		// Commands
+		public void AddModulePermission (string _module, int _permissionLevel, bool _save = true) {
+			WebModulePermission p = new WebModulePermission (_module, _permissionLevel);
+			lock (this) {
+				allModulesList.Clear ();
+				modules [_module] = p;
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public void AddKnownModule (string _module, int _defaultPermission) {
+			if (string.IsNullOrEmpty (_module)) {
+				return;
+			}
+			
+			WebModulePermission p = new WebModulePermission (_module, _defaultPermission);
+
+			lock (this) {
+				allModulesList.Clear ();
+				knownModules [_module] = p;
+			}
+		}
+
+		public bool IsKnownModule (string _module) {
+			if (string.IsNullOrEmpty (_module)) {
+				return false;
+			}
+
+			lock (this) {
+				return knownModules.ContainsKey (_module);
+			}
+
+		}
+
+		public void RemoveModulePermission (string _module, bool _save = true) {
+			lock (this) {
+				allModulesList.Clear ();
+				modules.Remove (_module);
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		private readonly List<WebModulePermission> allModulesList;
+		private readonly ReadOnlyCollection<WebModulePermission> allModulesListRo; 
+
+		public IList<WebModulePermission> GetModules () {
+			if (allModulesList.Count != 0) {
+				return allModulesListRo;
+			}
+
+			foreach ((string moduleName, WebModulePermission moduleDefaultPerm) in knownModules) {
+				allModulesList.Add (modules.TryGetValue (moduleName, out WebModulePermission modulePermission)
+					? modulePermission
+					: moduleDefaultPerm);
+			}
+
+			return allModulesListRo;
+		}
+
+
+		//IO Tasks
+
+		private void InitFileWatcher () {
+			fileWatcher = new FileSystemWatcher (GetFilePath (), GetFileName ());
+			fileWatcher.Changed += OnFileChanged;
+			fileWatcher.Created += OnFileChanged;
+			fileWatcher.Deleted += OnFileChanged;
+			fileWatcher.EnableRaisingEvents = true;
+		}
+
+		private void OnFileChanged (object _source, FileSystemEventArgs _e) {
+			Log.Out ("Reloading " + permissionsFile);
+			Load ();
+		}
+
+		private static string GetFilePath () {
+			return GamePrefs.GetString (EnumUtils.Parse<EnumGamePrefs> ("SaveGameFolder"));
+		}
+
+		private static string GetFileName () {
+			return permissionsFile;
+		}
+
+		private static string GetFullPath () {
+			return GetFilePath () + "/" + GetFileName ();
+		}
+
+		public void Load () {
+			admintokens.Clear ();
+			modules.Clear ();
+
+			if (!File.Exists (GetFullPath ())) {
+				Log.Out ($"Permissions file '{GetFileName ()}' not found, creating.");
+				Save ();
+				return;
+			}
+
+			Log.Out ($"Loading permissions file at '{GetFullPath ()}'");
+
+			XmlDocument xmlDoc = new XmlDocument ();
+
+			try {
+				xmlDoc.Load (GetFullPath ());
+			} catch (XmlException e) {
+				Log.Error ("Failed loading permissions file: " + e.Message);
+				return;
+			}
+
+			XmlNode adminToolsNode = xmlDoc.DocumentElement;
+
+			if (adminToolsNode == null) {
+				Log.Error ("Failed loading permissions file: No DocumentElement found");
+				return;
+			}
+			
+			foreach (XmlNode childNode in adminToolsNode.ChildNodes) {
+				if (childNode.Name == "admintokens") {
+					foreach (XmlNode subChild in childNode.ChildNodes) {
+						if (subChild.NodeType == XmlNodeType.Comment) {
+							continue;
+						}
+
+						if (subChild.NodeType != XmlNodeType.Element) {
+							Log.Warning ("Unexpected XML node found in 'admintokens' section: " + subChild.OuterXml);
+							continue;
+						}
+
+						XmlElement lineItem = (XmlElement) subChild;
+
+						if (!lineItem.HasAttribute ("name")) {
+							Log.Warning ("Ignoring admintoken-entry because of missing 'name' attribute: " +
+							             subChild.OuterXml);
+							continue;
+						}
+
+						if (!lineItem.HasAttribute ("token")) {
+							Log.Warning ("Ignoring admintoken-entry because of missing 'token' attribute: " +
+							             subChild.OuterXml);
+							continue;
+						}
+
+						if (!lineItem.HasAttribute ("permission_level")) {
+							Log.Warning ("Ignoring admintoken-entry because of missing 'permission_level' attribute: " +
+							             subChild.OuterXml);
+							continue;
+						}
+
+						string name = lineItem.GetAttribute ("name");
+						string token = lineItem.GetAttribute ("token");
+						if (!int.TryParse (lineItem.GetAttribute ("permission_level"), out int permissionLevel)) {
+							Log.Warning (
+								"Ignoring admintoken-entry because of invalid (non-numeric) value for 'permission_level' attribute: " +
+								subChild.OuterXml);
+							continue;
+						}
+
+						AddAdmin (name, token, permissionLevel, false);
+					}
+				}
+
+				if (childNode.Name == "permissions") {
+					foreach (XmlNode subChild in childNode.ChildNodes) {
+						if (subChild.NodeType == XmlNodeType.Comment) {
+							continue;
+						}
+
+						if (subChild.NodeType != XmlNodeType.Element) {
+							Log.Warning ("Unexpected XML node found in 'permissions' section: " + subChild.OuterXml);
+							continue;
+						}
+
+						XmlElement lineItem = (XmlElement) subChild;
+
+						if (!lineItem.HasAttribute ("module")) {
+							Log.Warning ("Ignoring permission-entry because of missing 'module' attribute: " +
+							             subChild.OuterXml);
+							continue;
+						}
+
+						if (!lineItem.HasAttribute ("permission_level")) {
+							Log.Warning ("Ignoring permission-entry because of missing 'permission_level' attribute: " +
+							             subChild.OuterXml);
+							continue;
+						}
+
+						if (!int.TryParse (lineItem.GetAttribute ("permission_level"), out int permissionLevel)) {
+							Log.Warning (
+								"Ignoring permission-entry because of invalid (non-numeric) value for 'permission_level' attribute: " +
+								subChild.OuterXml);
+							continue;
+						}
+
+						AddModulePermission (lineItem.GetAttribute ("module"), permissionLevel, false);
+					}
+				}
+			}
+
+			Log.Out ("Loading permissions file done.");
+		}
+
+		public void Save () {
+			fileWatcher.EnableRaisingEvents = false;
+
+			using (StreamWriter sw = new StreamWriter (GetFullPath ())) {
+				sw.WriteLine ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+				sw.WriteLine ("<webpermissions>");
+				sw.WriteLine ();
+				sw.WriteLine ("	<admintokens>");
+				sw.WriteLine (
+					"		<!-- <token name=\"adminuser1\" token=\"supersecrettoken\" permission_level=\"0\" /> -->");
+				foreach ((var _, AdminToken adminToken) in admintokens) {
+					sw.WriteLine ($"		<token name=\"{adminToken.name}\" token=\"{adminToken.token}\" permission_level=\"{adminToken.permissionLevel}\" />");
+				}
+
+				sw.WriteLine ("	</admintokens>");
+				sw.WriteLine ();
+				sw.WriteLine ("	<permissions>");
+				foreach ((var _, WebModulePermission webModulePermission) in modules) {
+					sw.WriteLine ($"		<permission module=\"{webModulePermission.module}\" permission_level=\"{webModulePermission.permissionLevel}\" />");
+				}
+
+				sw.WriteLine ("		<!-- <permission module=\"web.map\" permission_level=\"1000\" /> -->");
+				sw.WriteLine ();
+				sw.WriteLine ("		<!-- <permission module=\"webapi.getlog\" permission_level=\"0\" /> -->");
+				sw.WriteLine (
+					"		<!-- <permission module=\"webapi.executeconsolecommand\" permission_level=\"0\" /> -->");
+				sw.WriteLine ();
+				sw.WriteLine ("		<!-- <permission module=\"webapi.getstats\" permission_level=\"1000\" /> -->");
+				sw.WriteLine ("		<!-- <permission module=\"webapi.getplayersonline\" permission_level=\"1000\" /> -->");
+				sw.WriteLine ();
+				sw.WriteLine (
+					"		<!-- <permission module=\"webapi.getplayerslocation\" permission_level=\"1000\" /> -->");
+				sw.WriteLine ("		<!-- <permission module=\"webapi.viewallplayers\" permission_level=\"1\" /> -->");
+				sw.WriteLine ();
+				sw.WriteLine ("		<!-- <permission module=\"webapi.getlandclaims\" permission_level=\"1000\" /> -->");
+				sw.WriteLine ("		<!-- <permission module=\"webapi.viewallclaims\" permission_level=\"1\" /> -->");
+				sw.WriteLine ();
+				sw.WriteLine ("		<!-- <permission module=\"webapi.getplayerinventory\" permission_level=\"1\" /> -->");
+				sw.WriteLine ();
+				sw.WriteLine ("		<!-- <permission module=\"webapi.gethostilelocation\" permission_level=\"1\" /> -->");
+				sw.WriteLine ("		<!-- <permission module=\"webapi.getanimalslocation\" permission_level=\"1\" /> -->");
+				sw.WriteLine ("	</permissions>");
+				sw.WriteLine ();
+				sw.WriteLine ("</webpermissions>");
+
+				sw.Flush ();
+				sw.Close ();
+			}
+
+			fileWatcher.EnableRaisingEvents = true;
+		}
+
+
+		public class AdminToken {
+			public readonly string name;
+			public readonly int permissionLevel;
+			public readonly string token;
+
+			public AdminToken (string _name, string _token, int _permissionLevel) {
+				name = _name;
+				token = _token;
+				permissionLevel = _permissionLevel;
+			}
+		}
+
+		public struct WebModulePermission {
+			public readonly string module;
+			public readonly int permissionLevel;
+
+			public WebModulePermission (string _module, int _permissionLevel) {
+				module = _module;
+				permissionLevel = _permissionLevel;
+			}
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebUtils.cs
===================================================================
--- binary-improvements2/WebServer/src/WebUtils.cs	(revision 391)
+++ binary-improvements2/WebServer/src/WebUtils.cs	(revision 391)
@@ -0,0 +1,54 @@
+using System;
+using System.Net;
+using System.Text;
+using AllocsFixes.JSON;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
+
+namespace Webserver {
+	public static class WebUtils {
+		public const string MimePlain = "text/plain";
+		public const string MimeHtml = "text/html";
+		public const string MimeJson = "application/json";
+		
+#if ENABLE_PROFILER
+		private static readonly UnityEngine.Profiling.CustomSampler jsonSerializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Serialize");
+		private static readonly UnityEngine.Profiling.CustomSampler netWriteSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Write");
+#endif
+
+		public static void WriteJson (HttpListenerResponse _resp, JsonNode _root, HttpStatusCode _statusCode = HttpStatusCode.OK) {
+#if ENABLE_PROFILER
+			jsonSerializeSampler.Begin ();
+#endif
+			StringBuilder sb = new StringBuilder ();
+			_root.ToString (sb);
+#if ENABLE_PROFILER
+			jsonSerializeSampler.End ();
+			netWriteSampler.Begin ();
+#endif
+			WriteText (_resp, sb.ToString(), _statusCode, MimeJson);
+#if ENABLE_PROFILER
+			netWriteSampler.End ();
+#endif
+		}
+
+		public static void WriteText (HttpListenerResponse _resp, string _text, HttpStatusCode _statusCode = HttpStatusCode.OK, string _mimeType = null) {
+			_resp.StatusCode = (int)_statusCode;
+			_resp.ContentType = _mimeType ?? MimePlain;
+			_resp.ContentEncoding = Encoding.UTF8;
+
+			byte[] buf = Encoding.UTF8.GetBytes (_text);
+			_resp.ContentLength64 = buf.Length;
+			_resp.OutputStream.Write (buf, 0, buf.Length);
+		}
+
+		public static bool IsSslRedirected (HttpListenerRequest _req) {
+			string proto = _req.Headers ["X-Forwarded-Proto"];
+			return !string.IsNullOrEmpty (proto) && proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
+		}
+		
+		public static string GenerateGuid () {
+			return Guid.NewGuid ().ToString ();
+		}
+	}
+}
Index: binary-improvements2/server-fixes.sln
===================================================================
--- binary-improvements2/server-fixes.sln	(revision 390)
+++ binary-improvements2/server-fixes.sln	(revision 391)
@@ -4,11 +4,13 @@
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "7dtd-server-fixes", "7dtd-server-fixes\7dtd-server-fixes.csproj", "{81DA7F87-1A66-4920-AADA-6EAF1971F8D0}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AllocsCommands", "AllocsCommands\AllocsCommands.csproj", "{E273D042-57F9-4E2E-8268-5053527E5287}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandExtensions", "CommandExtensions\CommandExtensions.csproj", "{E273D042-57F9-4E2E-8268-5053527E5287}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAndMapRendering", "MapRendering\WebAndMapRendering.csproj", "{A1847B5F-7BFC-4BCD-94AA-A6C9FB7E7C54}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapRendering", "MapRendering\MapRendering.csproj", "{A1847B5F-7BFC-4BCD-94AA-A6C9FB7E7C54}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceWizards.HttpListener", "SpaceWizards.HttpListener\SpaceWizards.HttpListener.csproj", "{E273D042-57F9-4E2E-8268-5053527E5287}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkersMod", "MarkersMod\MarkersMod.csproj", "{2A008E16-6EB8-4B85-A175-3CB89D9FF4AE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebServer", "WebServer\WebServer.csproj", "{01B5F981-B9FD-4364-8F9E-9127130D2542}"
 EndProject
 Global
@@ -52,4 +54,12 @@
 		{2A008E16-6EB8-4B85-A175-3CB89D9FF4AE}.Release_Profiler|Any CPU.ActiveCfg = Release_Profiler|Any CPU
 		{2A008E16-6EB8-4B85-A175-3CB89D9FF4AE}.Release_Profiler|Any CPU.Build.0 = Release_Profiler|Any CPU
+		{01B5F981-B9FD-4364-8F9E-9127130D2542}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{01B5F981-B9FD-4364-8F9E-9127130D2542}.Release|Any CPU.Build.0 = Release|Any CPU
+		{01B5F981-B9FD-4364-8F9E-9127130D2542}.Release_Version|Any CPU.ActiveCfg = Release|Any CPU
+		{01B5F981-B9FD-4364-8F9E-9127130D2542}.Release_Version|Any CPU.Build.0 = Release|Any CPU
+		{01B5F981-B9FD-4364-8F9E-9127130D2542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{01B5F981-B9FD-4364-8F9E-9127130D2542}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{01B5F981-B9FD-4364-8F9E-9127130D2542}.Release_Profiler|Any CPU.ActiveCfg = Release_Profiler|Any CPU
+		{01B5F981-B9FD-4364-8F9E-9127130D2542}.Release_Profiler|Any CPU.Build.0 = Release_Profiler|Any CPU
 	EndGlobalSection
 	GlobalSection(MonoDevelopProperties) = preSolution
