Index: binary-improvements/7dtd-binaries/README.txt
===================================================================
--- binary-improvements/7dtd-binaries/README.txt	(revision 243)
+++ binary-improvements/7dtd-binaries/README.txt	(revision 244)
@@ -1,3 +1,3 @@
 Put the Assembly-CSharp.dll, Assembly-CSharp-firstpass.dll, LogLibrary.dll, mscorlib.dll,
-System.dll and UnityEngine.dll from your dedicated server in this folder.
+System.dll, System.Xml.dll and UnityEngine.dll from your dedicated server in this folder.
 
Index: binary-improvements/7dtd-server-fixes/7dtd-server-fixes.csproj
===================================================================
--- binary-improvements/7dtd-server-fixes/7dtd-server-fixes.csproj	(revision 243)
+++ binary-improvements/7dtd-server-fixes/7dtd-server-fixes.csproj	(revision 244)
@@ -28,5 +28,5 @@
     <CustomCommands>
       <CustomCommands>
-        <Command type="AfterBuild" command="bash -c &quot;monodis --assembly ${TargetFile} | grep Version &gt; ${TargetDir}/${ProjectName}_version.txt&quot;" />
+        <Command type="AfterBuild" command="bash -c &quot;${SolutionDir}/versions.sh &gt; ${TargetDir}/${ProjectName}_version.txt&quot;" workingdir="${SolutionDir}" />
       </CustomCommands>
     </CustomCommands>
Index: binary-improvements/7dtd-server-fixes/src/AssemblyInfo.cs
===================================================================
--- binary-improvements/7dtd-server-fixes/src/AssemblyInfo.cs	(revision 243)
+++ binary-improvements/7dtd-server-fixes/src/AssemblyInfo.cs	(revision 244)
@@ -18,5 +18,5 @@
 // and "{Major}.{Minor}.{Build}.*" will update just the revision.
 
-[assembly: AssemblyVersion("0.12.0.0")]
+[assembly: AssemblyVersion("0.0.0.0")]
 
 // The following attributes are used to specify the signing key for the assembly, 
Index: binary-improvements/MapRendering/Commands/ReloadWebPermissions.cs
===================================================================
--- binary-improvements/MapRendering/Commands/ReloadWebPermissions.cs	(revision 244)
+++ binary-improvements/MapRendering/Commands/ReloadWebPermissions.cs	(revision 244)
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+
+namespace AllocsFixes.CustomCommands
+{
+	public class ReloadWebPermissions : ConsoleCmdAbstract
+	{
+		public override string GetDescription ()
+		{
+			return "force reload of web permissions file";
+		}
+
+		public override string[] GetCommands ()
+		{
+			return new string[] { "reloadwebpermissions" };
+		}
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo)
+		{
+			try {
+				AllocsFixes.NetConnections.Servers.Web.WebPermissions.Instance.Load ();
+				SdtdConsole.Instance.Output ("Web permissions file reloaded");
+			} catch (Exception e) {
+				Log.Out ("Error in ReloadWebPermissions.Run: " + e);
+			}
+		}
+	}
+}
Index: binary-improvements/MapRendering/Commands/WebPermissionsCmd.cs
===================================================================
--- binary-improvements/MapRendering/Commands/WebPermissionsCmd.cs	(revision 244)
+++ binary-improvements/MapRendering/Commands/WebPermissionsCmd.cs	(revision 244)
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using AllocsFixes.NetConnections.Servers.Web;
+using UnityEngine;
+
+namespace AllocsFixes.CustomCommands
+{
+	public class WebPermissionsCmd : ConsoleCmdAbstract {
+		public override string[] GetCommands () {
+			return new string[] { "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) {
+				switch (_params [0].ToLower ()) {
+					case "add":
+						ExecuteAdd (_params);
+						break;
+					case "remove":
+						ExecuteRemove (_params);
+						break;
+					case "list":
+						ExecuteList ();
+						break;
+					default:
+						SdtdConsole.Instance.Output ("Invalid sub command \"" + _params [0] + "\".");
+						return;
+				}
+			} 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 (string.Format ("{0} added with permission level of {1}.", _params [1], 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 (string.Format ("{0} removed from permissions list.", _params [1]));
+		}
+	
+		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 (string.Format ("  {0,5}: {1}", wmp.permissionLevel, wmp.module));
+			}
+		}
+
+	}
+}
Index: binary-improvements/MapRendering/Commands/WebTokens.cs
===================================================================
--- binary-improvements/MapRendering/Commands/WebTokens.cs	(revision 244)
+++ binary-improvements/MapRendering/Commands/WebTokens.cs	(revision 244)
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+
+using AllocsFixes.NetConnections.Servers.Web;
+using UnityEngine;
+
+namespace AllocsFixes.CustomCommands
+{
+	public class WebTokens : ConsoleCmdAbstract {
+		private static Regex validNameTokenMatcher = new Regex (@"^\w+$");
+
+		public override string[] GetCommands () {
+			return new string[] { "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) {
+				switch (_params [0].ToLower ()) {
+					case "add":
+						ExecuteAdd (_params);
+						break;
+					case "remove":
+						ExecuteRemove (_params);
+						break;
+					case "list":
+						ExecuteList ();
+						break;
+					default:
+						SdtdConsole.Instance.Output ("Invalid sub command \"" + _params [0] + "\".");
+						return;
+				}
+			} 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-improvements/MapRendering/Web/API/GetLandClaims.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetLandClaims.cs	(revision 243)
+++ binary-improvements/MapRendering/Web/API/GetLandClaims.cs	(revision 244)
@@ -9,5 +9,5 @@
 	public class GetLandClaims : WebAPI
 	{
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
 		{
 			string steamid = string.Empty;
@@ -17,5 +17,5 @@
 				steamid = req.QueryString ["steamid"];
 				if (steamid.Length != 17 || !long.TryParse (steamid, out tempLong)) {
-					resp.StatusCode = (int)HttpStatusCode.InternalServerError;
+					resp.StatusCode = (int)HttpStatusCode.BadRequest;
 					Web.SetResponseTextContent (resp, "Invalid SteamID given");
 					return;
Index: binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs	(revision 243)
+++ binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs	(revision 244)
@@ -9,5 +9,5 @@
 	public class GetPlayerInventory : WebAPI
 	{
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
 		{
 			if (req.QueryString ["steamid"] == null) {
Index: binary-improvements/MapRendering/Web/API/GetPlayersLocation.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetPlayersLocation.cs	(revision 243)
+++ binary-improvements/MapRendering/Web/API/GetPlayersLocation.cs	(revision 244)
@@ -9,5 +9,5 @@
 	public class GetPlayersLocation : WebAPI
 	{
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
 		{
 			JSONArray playersJsResult = new JSONArray ();
Index: binary-improvements/MapRendering/Web/API/GetPlayersOnline.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetPlayersOnline.cs	(revision 243)
+++ binary-improvements/MapRendering/Web/API/GetPlayersOnline.cs	(revision 244)
@@ -9,5 +9,5 @@
 	public class GetPlayersOnline : WebAPI
 	{
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
 		{
 			JSONArray players = new JSONArray();
Index: binary-improvements/MapRendering/Web/API/GetStats.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetStats.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/API/GetStats.cs	(revision 244)
@@ -0,0 +1,25 @@
+using AllocsFixes.JSON;
+using AllocsFixes.PersistentData;
+using System;
+using System.Collections.Generic;
+using System.Net;
+
+namespace AllocsFixes.NetConnections.Servers.Web.API
+{
+	public class GetStats : WebAPI
+	{
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
+		{
+			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);
+
+			WriteJSON (resp, result);
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/API/WebAPI.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/WebAPI.cs	(revision 243)
+++ binary-improvements/MapRendering/Web/API/WebAPI.cs	(revision 244)
@@ -16,5 +16,5 @@
 		}
 
-		public abstract void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user);
+		public abstract void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel);
 	}
 }
Index: binary-improvements/MapRendering/Web/ApiHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/ApiHandler.cs	(revision 243)
+++ 	(revision )
@@ -1,56 +1,0 @@
-using AllocsFixes.NetConnections.Servers.Web.API;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public class ApiHandler : PathHandler
-	{
-		private string staticPart;
-		private Dictionary<String, WebAPI> apis = new Dictionary<string, WebAPI> ();
-
-		public ApiHandler (string staticPart)
-		{
-			this.staticPart = staticPart;
-			apis.Add ("getlandclaims", new GetLandClaims ());
-			apis.Add ("getplayersonline", new GetPlayersOnline ());
-			apis.Add ("getplayerslocation", new GetPlayersLocation ());
-			apis.Add ("getplayerinventory", new GetPlayerInventory ());
-		}
-
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
-		{
-			string apiName = req.Url.AbsolutePath.Remove (0, staticPart.Length);
-			if (!AuthorizeForCommand (apiName, user)) {
-				resp.StatusCode = (int)HttpStatusCode.Forbidden;
-			} else {
-				foreach (KeyValuePair<string, WebAPI> kvp in apis) {
-					try {
-						if (apiName.StartsWith (kvp.Key)) {
-							kvp.Value.HandleRequest (req, resp, user);
-							return;
-						}
-					} catch (Exception e) {
-						Log.Out ("Error in ApiHandler.HandleRequest(): Handler threw an exception: " + e);
-						resp.StatusCode = (int)HttpStatusCode.InternalServerError;
-						return;
-					}
-				}
-			}
-	
-			Log.Out ("Error in ApiHandler.HandleRequest(): No handler found for API \"" + apiName + "\"");
-			resp.StatusCode = (int)HttpStatusCode.NotFound;
-		}
-
-		private bool AuthorizeForCommand (string apiName, HttpListenerBasicIdentity user)
-		{
-			return true;
-		}
-
-	}
-
-}
-
Index: binary-improvements/MapRendering/Web/ConnectionHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/ConnectionHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/ConnectionHandler.cs	(revision 244)
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public class ConnectionHandler {
+		private Web parent;
+		private Dictionary<string, WebConnection> connections = new Dictionary<string, WebConnection> ();
+
+		public ConnectionHandler (Web _parent) {
+			parent = _parent;
+		}
+
+		public WebConnection IsLoggedIn (string _sessionId, string _endpoint) {
+			if (!connections.ContainsKey (_sessionId)) {
+				return null;
+			}
+
+			WebConnection con = connections [_sessionId];
+//			if (con.Age.TotalMinutes > parent.sessionTimeoutMinutes) {
+//				connections.Remove (_sessionId);
+//				return null;
+//			}
+
+			if (con.Endpoint != _endpoint) {
+				connections.Remove (_sessionId);
+				return null;
+			}
+
+			con.UpdateUsage ();
+
+			return con;
+		}
+
+		public void LogOut (string _sessionId) {
+			connections.Remove (_sessionId);
+		}
+
+		public WebConnection LogIn (ulong _steamId, string _endpoint) {
+			string sessionId = Guid.NewGuid ().ToString ();
+			WebConnection con = new WebConnection (sessionId, _endpoint, _steamId);
+			connections.Add (sessionId, con);
+			return con;
+		}
+
+		public void SendLine (string line) {
+			foreach (WebConnection wc in connections.Values) {
+				wc.SendLine (line);
+			}
+		}
+
+		public void SendLog (string text, string trace, UnityEngine.LogType type) {
+			foreach (WebConnection wc in connections.Values) {
+				wc.SendLog (text, trace, type);
+			}
+		}
+
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs	(revision 244)
@@ -0,0 +1,64 @@
+using AllocsFixes.NetConnections.Servers.Web.API;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class ApiHandler : PathHandler {
+		private string staticPart;
+		private Dictionary<String, WebAPI> apis = new Dictionary<string, WebAPI> ();
+
+		public ApiHandler (string staticPart, string moduleName = null) : base(moduleName) {
+			this.staticPart = staticPart;
+			addApi ("getlandclaims", new GetLandClaims ());
+			addApi ("getplayersonline", new GetPlayersOnline ());
+			addApi ("getplayerslocation", new GetPlayersLocation ());
+			addApi ("getplayerinventory", new GetPlayerInventory ());
+			addApi ("getstats", new GetStats ());
+		}
+
+		private void addApi (string _apiName, WebAPI _api) {
+			apis.Add (_apiName, _api);
+			WebPermissions.Instance.AddKnownModule ("webapi." + _apiName);
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			string apiName = req.Url.AbsolutePath.Remove (0, staticPart.Length);
+			if (!AuthorizeForCommand (apiName, user, permissionLevel)) {
+				resp.StatusCode = (int)HttpStatusCode.Forbidden;
+				if (user != null) {
+					Log.Out ("ApiHandler: user '{0}' not allowed to execute '{1}'", user.SteamID, apiName);
+				} else {
+					Log.Out ("ApiHandler: unidentified user from '{0}' not allowed to execute '{1}'", req.RemoteEndPoint.Address, apiName);
+				}
+				return;
+			} else {
+				foreach (KeyValuePair<string, WebAPI> kvp in apis) {
+					try {
+						if (apiName.StartsWith (kvp.Key)) {
+							kvp.Value.HandleRequest (req, resp, user, permissionLevel);
+							return;
+						}
+					} catch (Exception e) {
+						Log.Out ("Error in ApiHandler.HandleRequest(): Handler threw an exception: " + e);
+						resp.StatusCode = (int)HttpStatusCode.InternalServerError;
+						return;
+					}
+				}
+			}
+	
+			Log.Out ("Error in ApiHandler.HandleRequest(): No handler found for API \"" + apiName + "\"");
+			resp.StatusCode = (int)HttpStatusCode.NotFound;
+		}
+
+		private bool AuthorizeForCommand (string apiName, WebConnection user, int permissionLevel) {
+			return WebPermissions.Instance.ModuleAllowedWithLevel ("webapi." + apiName, permissionLevel);
+		}
+
+	}
+
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/ItemIconHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/ItemIconHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/ItemIconHandler.cs	(revision 244)
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+
+using UnityEngine;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class ItemIconHandler : PathHandler
+	{
+		private string staticPart;
+		private bool logMissingFiles;
+		private Dictionary<string, byte[]> icons = new Dictionary<string, byte[]> ();
+		private bool loaded = false;
+
+		public ItemIconHandler (string staticPart, bool logMissingFiles, string moduleName = null) : base(moduleName) {
+			this.staticPart = staticPart;
+			this.logMissingFiles = logMissingFiles;
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			if (!loaded) {
+				if (!LoadIcons ()) {
+					resp.StatusCode = (int)HttpStatusCode.NotFound;
+					Log.Out ("Web:IconHandler: Could not load icons");
+					return;
+				}
+			}
+
+			string fn = req.Url.AbsolutePath.Remove (0, staticPart.Length);
+			fn = fn.Remove (fn.LastIndexOf ('.'));
+
+			if (icons.ContainsKey (fn)) {
+				resp.ContentType = MimeType.GetMimeType (".png");
+				resp.ContentLength64 = icons [fn].Length;
+				resp.OutputStream.Write (icons [fn], 0, icons [fn].Length);
+			} else {
+				resp.StatusCode = (int)HttpStatusCode.NotFound;
+				if (logMissingFiles)
+					Log.Out ("Web:IconHandler:FileNotFound: \"" + req.Url.AbsolutePath + "\" ");
+				return;
+			}
+		}
+
+		private bool LoadIcons () {
+			lock (icons) {
+				if (loaded) {
+					return true;
+				}
+
+				GameObject atlasObj = GameObject.Find ("/NGUI Root (2D)/ItemIconAtlas");
+				if (atlasObj == null) {
+					Log.Error ("Web:IconHandler: Atlas object not found");
+					loaded = true;
+					return false;
+				}
+				DynamicUIAtlas atlas = atlasObj.GetComponent<DynamicUIAtlas> ();
+				if (atlas == null) {
+					Log.Error ("Web:IconHandler: Atlas component not found");
+					loaded = true;
+					return false;
+				}
+
+				Texture2D atlasTex = atlas.texture as Texture2D;
+
+				foreach (UISpriteData data in atlas.spriteList) {
+					string name = data.name;
+					Texture2D tex = new Texture2D (data.width, data.height, TextureFormat.ARGB32, false);
+					tex.SetPixels (atlasTex.GetPixels (data.x, atlasTex.height - data.height - data.y, data.width, data.height));
+					byte[] pixData = tex.EncodeToPNG ();
+
+					icons.Add (name, pixData);
+					UnityEngine.Object.Destroy (tex);
+				}
+
+				loaded = true;
+				Log.Out ("Web:IconHandler: Icons loaded");
+
+				return true;
+			}
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/PathHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/PathHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/PathHandler.cs	(revision 244)
@@ -0,0 +1,29 @@
+using System;
+using System.Net;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public abstract class PathHandler
+	{
+		private string moduleName = null;
+		public string ModuleName {
+			get { return moduleName; }
+		}
+
+		protected PathHandler (string _moduleName) {
+			this.moduleName = _moduleName;
+			WebPermissions.Instance.AddKnownModule (_moduleName);
+		}
+
+		public abstract void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel);
+
+		public bool IsAuthorizedForHandler (WebConnection user, int permissionLevel) {
+			if (moduleName != null) {
+				return WebPermissions.Instance.ModuleAllowedWithLevel (moduleName, permissionLevel);
+			} else {
+				return true;
+			}
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs	(revision 244)
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class SessionHandler : PathHandler {
+		private string staticPart;
+		private Web parent;
+		private string header = "";
+		private string footer = "";
+
+		public SessionHandler (string _staticPart, string _dataFolder, Web _parent, string moduleName = null) : base(moduleName) {
+			this.staticPart = _staticPart;
+			this.parent = _parent;
+
+			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 (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			string subpath = req.Url.AbsolutePath.Remove (0, staticPart.Length);
+
+			StringBuilder result = new StringBuilder ();
+			result.Append (header);
+
+			if (subpath.StartsWith ("verify")) {
+				if (user != null) {
+					resp.Redirect ("/static/index.html");
+					return;
+				} else {
+					result.Append ("<h1>Login failed, <a href=\"/static/index.html\">click to return to main page</a>.</h1>");
+				}
+			} else if (subpath.StartsWith ("logout")) {
+				if (user != null) {
+					parent.connectionHandler.LogOut (user.SessionID);
+					Cookie cookie = new Cookie ("sid", "", "/");
+					cookie.Expired = true;
+					resp.AppendCookie (cookie);
+					resp.Redirect ("/static/index.html");
+					return;
+				} else {
+					result.Append ("<h1>Not logged in, <a href=\"/static/index.html\">click to return to main page</a>.</h1>");
+				}
+			} else if (subpath.StartsWith ("login")) {
+				string host = (parent.isSslRedirected ? "https://" : "http://") + req.UserHostName;
+				string url = OpenID.GetOpenIdLoginUrl (host, host + "/session/verify");
+				resp.Redirect (url);
+				return;
+			} else {
+				result.Append ("<h1>Unknown command, <a href=\"/static/index.html\">click to return to main page</a>.</h1>");
+			}
+
+			result.Append (footer);
+
+			resp.ContentType = MimeType.GetMimeType (".html");
+			resp.ContentEncoding = Encoding.UTF8;
+			byte[] buf = Encoding.UTF8.GetBytes (result.ToString ());
+			resp.ContentLength64 = buf.Length;
+			resp.OutputStream.Write (buf, 0, buf.Length);
+		}
+
+	}
+
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/SimpleRedirectHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/SimpleRedirectHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/SimpleRedirectHandler.cs	(revision 244)
@@ -0,0 +1,21 @@
+using System;
+using System.Net;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class SimpleRedirectHandler : PathHandler
+	{
+		string target;
+
+		public SimpleRedirectHandler (string target, string moduleName = null) : base(moduleName)
+		{
+			this.target = target;
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
+		{
+			resp.Redirect (target);
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs	(revision 244)
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class StaticHandler : PathHandler
+	{
+		private string datapath;
+		private string staticPart;
+		private AllocsFixes.FileCache.AbstractCache cache;
+		private bool logMissingFiles;
+
+		public StaticHandler (string staticPart, string filePath, AllocsFixes.FileCache.AbstractCache cache, bool logMissingFiles, string moduleName = null) : base(moduleName)
+		{
+			this.staticPart = staticPart;
+			this.datapath = filePath;
+			this.cache = cache;
+			this.logMissingFiles = logMissingFiles;
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
+		{
+			string fn = req.Url.AbsolutePath.Remove (0, staticPart.Length);
+
+			byte[] content = cache.GetFileContent (datapath + "/" + fn);
+			if (content != null) {
+				resp.ContentType = MimeType.GetMimeType (Path.GetExtension (fn));
+				resp.ContentLength64 = content.Length;
+				resp.OutputStream.Write (content, 0, content.Length);
+			} else {
+				resp.StatusCode = (int)HttpStatusCode.NotFound;
+				if (logMissingFiles)
+					Log.Out ("Web:Static:FileNotFound: \"" + req.Url.AbsolutePath + "\" @ \"" + datapath + "/" + req.Url.AbsolutePath.Remove (0, staticPart.Length) + "\"");
+				return;
+			}
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/UserStatusHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/UserStatusHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/UserStatusHandler.cs	(revision 244)
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Text;
+using AllocsFixes.JSON;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class UserStatusHandler : PathHandler {
+		public UserStatusHandler (string moduleName = null) : base(moduleName) {
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			JSONObject result = new JSONObject ();
+
+			result.Add ("loggedin", new JSONBoolean (user != null));
+			result.Add ("username", new JSONString (user != null ? user.SteamID.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 (WebPermissions.Instance.ModuleAllowedWithLevel (perm.module, permissionLevel)));
+				perms.Add (permObj);
+			}
+			result.Add ("permissions", perms);
+
+			WriteJSON (resp, result);
+		}
+
+		public void WriteJSON (HttpListenerResponse resp, JSONNode root) {
+			byte[] buf = Encoding.UTF8.GetBytes (root.ToString ());
+			resp.ContentLength64 = buf.Length;
+			resp.ContentType = "application/json";
+			resp.ContentEncoding = Encoding.UTF8;
+			resp.OutputStream.Write (buf, 0, buf.Length);
+		}
+
+	}
+
+}
+
Index: binary-improvements/MapRendering/Web/ItemIconHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/ItemIconHandler.cs	(revision 243)
+++ 	(revision )
@@ -1,86 +1,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-
-using UnityEngine;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public class ItemIconHandler : PathHandler
-	{
-		private string staticPart;
-		private bool logMissingFiles;
-		private Dictionary<string, byte[]> icons = new Dictionary<string, byte[]> ();
-		private bool loaded = false;
-
-		public ItemIconHandler (string staticPart, bool logMissingFiles) {
-			this.staticPart = staticPart;
-			this.logMissingFiles = logMissingFiles;
-		}
-
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user) {
-			if (!loaded) {
-				if (!LoadIcons ()) {
-					resp.StatusCode = (int)HttpStatusCode.NotFound;
-					Log.Out ("Web:IconHandler: Could not load icons");
-					return;
-				}
-			}
-
-			string fn = req.Url.AbsolutePath.Remove (0, staticPart.Length);
-			fn = fn.Remove (fn.LastIndexOf ('.'));
-
-			if (icons.ContainsKey (fn)) {
-				resp.ContentType = MimeType.GetMimeType (".png");
-				resp.ContentLength64 = icons [fn].Length;
-				resp.OutputStream.Write (icons [fn], 0, icons [fn].Length);
-			} else {
-				resp.StatusCode = (int)HttpStatusCode.NotFound;
-				if (logMissingFiles)
-					Log.Out ("Web:IconHandler:FileNotFound: \"" + req.Url.AbsolutePath + "\" ");
-				return;
-			}
-		}
-
-		private bool LoadIcons () {
-			lock (icons) {
-				if (loaded) {
-					return true;
-				}
-
-				GameObject atlasObj = GameObject.Find ("/NGUI Root (2D)/ItemIconAtlas");
-				if (atlasObj == null) {
-					Log.Error ("Web:IconHandler: Atlas object not found");
-					loaded = true;
-					return false;
-				}
-				DynamicUIAtlas atlas = atlasObj.GetComponent<DynamicUIAtlas> ();
-				if (atlas == null) {
-					Log.Error ("Web:IconHandler: Atlas component not found");
-					loaded = true;
-					return false;
-				}
-
-				Texture2D atlasTex = atlas.texture as Texture2D;
-
-				foreach (UISpriteData data in atlas.spriteList) {
-					string name = data.name;
-					Texture2D tex = new Texture2D (data.width, data.height, TextureFormat.ARGB32, false);
-					tex.SetPixels (atlasTex.GetPixels (data.x, atlasTex.height - data.height - data.y, data.width, data.height));
-					byte[] pixData = tex.EncodeToPNG ();
-
-					icons.Add (name, pixData);
-					UnityEngine.Object.Destroy (tex);
-				}
-
-				loaded = true;
-				Log.Out ("Web:IconHandler: Icons loaded");
-
-				return true;
-			}
-		}
-	}
-}
-
Index: binary-improvements/MapRendering/Web/OpenID.cs
===================================================================
--- binary-improvements/MapRendering/Web/OpenID.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/OpenID.cs	(revision 244)
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Net;
+using System.Net.Security;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public static class OpenID {
+		private const string STEAM_LOGIN = "https://steamcommunity.com/openid/login";
+		private static Regex steamIdUrlMatcher = new Regex (@"^http:\/\/steamcommunity\.com\/openid\/id\/([0-9]{17,18})");
+
+		static OpenID () {
+			ServicePointManager.ServerCertificateValidationCallback = (srvPoint, certificate, chain, errors) => {
+				if (errors == SslPolicyErrors.None)
+					return true;
+
+				Log.Out ("Steam certificate error: {0}", errors);
+
+				return true;
+			};
+
+		}
+
+		public static string GetOpenIdLoginUrl (string _returnHost, string _returnUrl) {
+			Dictionary<string, string> queryParams = new Dictionary<string, string> ();
+
+			queryParams.Add ("openid.ns", "http://specs.openid.net/auth/2.0");
+			queryParams.Add ("openid.mode", "checkid_setup");
+			queryParams.Add ("openid.return_to", _returnUrl);
+			queryParams.Add ("openid.realm", _returnHost);
+			queryParams.Add ("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select");
+			queryParams.Add ("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select");
+
+			return STEAM_LOGIN + '?' + buildUrlParams (queryParams);
+		}
+
+		public static ulong Validate (HttpListenerRequest _req) {
+			if (getValue (_req, "openid.mode") == "cancel") {
+				Log.Warning ("Steam OpenID login canceled");
+				return 0;
+			}
+			string steamIdString = getValue (_req, "openid.claimed_id");
+			ulong steamId = 0;
+			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");
+				return 0;
+			}
+
+			Dictionary<string, string> queryParams = new Dictionary<string, string> ();
+
+			queryParams.Add ("openid.ns", "http://specs.openid.net/auth/2.0");
+
+			queryParams.Add ("openid.assoc_handle", getValue (_req, "openid.assoc_handle"));
+			queryParams.Add ("openid.signed", getValue (_req, "openid.signed"));
+			queryParams.Add ("openid.sig", getValue (_req, "openid.sig"));
+			queryParams.Add ("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select");
+			queryParams.Add ("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 = null;
+			using (Stream st = response.GetResponseStream ()) {
+				using (StreamReader str = new StreamReader (st)) {
+					responseString = str.ReadToEnd ();
+				}
+			}
+
+			if (responseString.ToLower ().Contains ("is_valid:true")) {
+				return steamId;
+			} else {
+				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 (KeyValuePair<string, string> kvp in _queryParams) {
+				paramsArr [i++] = kvp.Key + "=" + Uri.EscapeDataString (kvp.Value);
+			}
+			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];
+		}
+
+	}
+}
+
Index: binary-improvements/MapRendering/Web/PathHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/PathHandler.cs	(revision 243)
+++ 	(revision )
@@ -1,11 +1,0 @@
-using System;
-using System.Net;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public abstract class PathHandler
-	{
-		public abstract void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user);
-	}
-}
-
Index: binary-improvements/MapRendering/Web/SimpleRedirectHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/SimpleRedirectHandler.cs	(revision 243)
+++ 	(revision )
@@ -1,21 +1,0 @@
-using System;
-using System.Net;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public class SimpleRedirectHandler : PathHandler
-	{
-		string target;
-
-		public SimpleRedirectHandler (string target)
-		{
-			this.target = target;
-		}
-
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
-		{
-			resp.Redirect (target);
-		}
-	}
-}
-
Index: binary-improvements/MapRendering/Web/StaticHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/StaticHandler.cs	(revision 243)
+++ 	(revision )
@@ -1,42 +1,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public class StaticHandler : PathHandler
-	{
-		private string datapath;
-		private string staticPart;
-		private AllocsFixes.FileCache.AbstractCache cache;
-		private bool logMissingFiles;
-
-		public StaticHandler (string staticPart, string filePath, AllocsFixes.FileCache.AbstractCache cache, bool logMissingFiles)
-		{
-			this.staticPart = staticPart;
-			this.datapath = filePath;
-			this.cache = cache;
-			this.logMissingFiles = logMissingFiles;
-		}
-
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
-		{
-			string fn = req.Url.AbsolutePath.Remove (0, staticPart.Length);
-
-			byte[] content = cache.GetFileContent (datapath + "/" + fn);
-			if (content != null) {
-				resp.ContentType = MimeType.GetMimeType (Path.GetExtension (fn));
-				resp.ContentLength64 = content.Length;
-				resp.OutputStream.Write (content, 0, content.Length);
-			} else {
-				resp.StatusCode = (int)HttpStatusCode.NotFound;
-				if (logMissingFiles)
-					Log.Out ("Web:Static:FileNotFound: \"" + req.Url.AbsolutePath + "\" @ \"" + datapath + "/" + req.Url.AbsolutePath.Remove (0, staticPart.Length) + "\"");
-				return;
-			}
-		}
-	}
-}
-
Index: binary-improvements/MapRendering/Web/Web.cs
===================================================================
--- binary-improvements/MapRendering/Web/Web.cs	(revision 243)
+++ binary-improvements/MapRendering/Web/Web.cs	(revision 244)
@@ -1,4 +1,5 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.IO;
 using System.Net;
@@ -9,15 +10,23 @@
 using UnityEngine;
 
+using AllocsFixes.NetConnections.Servers.Web.Handlers;
+
 namespace AllocsFixes.NetConnections.Servers.Web
 {
 	public class Web : IConsoleServer {
+		private const int GUEST_PERMISSION_LEVEL = 2000;
 		private readonly HttpListener _listener = new HttpListener ();
 		private Dictionary<string, PathHandler> handlers = new Dictionary<string, PathHandler> ();
-		private bool authEnabled = false;
-		private string realm = "7dtd Admin Panel";
 		public static int handlingCount = 0;
 		public static int currentHandlers = 0;
 		private string dataFolder;
-		private bool mapEnabled = false;
+		private bool useStaticCache = false;
+
+		public bool isSslRedirected {
+			private set;
+			get;
+		}
+
+		public ConnectionHandler connectionHandler;
 
 		public Web () {
@@ -25,5 +34,5 @@
 				int webPort = GamePrefs.GetInt (EnumGamePrefs.ControlPanelPort);
 				if (webPort < 1 || webPort > 65533) {
-					Log.Out ("Webserver not started (ControlPanelPort not within 1-65534)");
+					Log.Out ("Webserver not started (ControlPanelPort not within 1-65533)");
 					return;
 				}
@@ -33,4 +42,8 @@
 				}
 
+				// TODO: Read from config
+				isSslRedirected = false;
+				useStaticCache = false;
+
 				dataFolder = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) + "/webserver";
 
@@ -47,11 +60,33 @@
 						new SimpleRedirectHandler ("/static/favicon.ico"));
 				handlers.Add (
-						"/static/",
-						new StaticHandler (
-								"/static/",
-								dataFolder,
-								new AllocsFixes.FileCache.DirectAccess (),
-								true)
-				); // TODO: Enable cache
+						"/session/",
+						new SessionHandler (
+									"/session/",
+									dataFolder,
+									this)
+				);
+				handlers.Add (
+						"/userstatus",
+						new UserStatusHandler ()
+				);
+				if (useStaticCache) {
+					handlers.Add (
+							"/static/",
+							new StaticHandler (
+									"/static/",
+									dataFolder,
+									new AllocsFixes.FileCache.SimpleCache (),
+									true)
+					);
+				} else {
+					handlers.Add (
+							"/static/",
+							new StaticHandler (
+									"/static/",
+									dataFolder,
+									new AllocsFixes.FileCache.DirectAccess (),
+									true)
+					);
+				}
 
 				handlers.Add (
@@ -68,16 +103,17 @@
 						GameUtils.GetSaveGameDir () + "/map",
 						MapRendering.MapRendering.GetTileCache (),
-						false)
-				);
-
-				handlers.Add ("/api/", new ApiHandler ("/api/"));
+						false,
+						"web.map")
+				);
+
+				handlers.Add (
+					"/api/",
+					new ApiHandler ("/api/")
+				);
+
+				connectionHandler = new ConnectionHandler (this);
 
 				_listener.Prefixes.Add (String.Format ("http://*:{0}/", webPort + 2));
-				authEnabled = File.Exists (dataFolder + "/protect");
-				if (authEnabled) {
-					_listener.AuthenticationSchemes = AuthenticationSchemes.Basic;
-				}
 				_listener.Start ();
-				_listener.Realm = realm;
 
 				SdtdConsole.Instance.RegisterServer (this);
@@ -85,5 +121,5 @@
 				_listener.BeginGetContext (new AsyncCallback (HandleRequest), _listener);
 
-				Log.Out ("Started Webserver on " + (webPort + 2) + " (authentication " + (authEnabled ? "enabled" : "disabled") + ")");
+				Log.Out ("Started Webserver on " + (webPort + 2));
 			} catch (Exception e) {
 				Log.Out ("Error in Web.ctor: " + e);
@@ -98,27 +134,48 @@
 				_listener.BeginGetContext (new AsyncCallback (HandleRequest), _listener);
 				try {
-					ctx.Response.ProtocolVersion = new Version ("1.0");
-
-					HttpListenerBasicIdentity user = Authorize (ctx);
-
-					if (!authEnabled || (user.Name.ToLower ().Equals ("admin") && user.Password.Equals (GamePrefs.GetString (EnumGamePrefs.ControlPanelPassword)))) {
-						if (ctx.Request.Url.AbsolutePath.Length < 2) {
-							handlers ["/index.htm"].HandleRequest (ctx.Request, ctx.Response, user);
-							return;
-						} else {
-							foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
-								if (ctx.Request.Url.AbsolutePath.StartsWith (kvp.Key)) {
-									kvp.Value.HandleRequest (ctx.Request, ctx.Response, user);
-									return;
+					HttpListenerRequest request = ctx.Request;
+					HttpListenerResponse response = ctx.Response;
+
+					response.ProtocolVersion = new Version ("1.1");
+
+					WebConnection conn;
+					int permissionLevel = DoAuthentication (request, out conn);
+
+
+					//Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
+
+
+					if (conn != null) {
+						Cookie cookie = new Cookie ("sid", conn.SessionID, "/");
+						cookie.Expired = false;
+						cookie.Expires = new DateTime (2020, 1, 1);
+						cookie.HttpOnly = true;
+						cookie.Secure = false;
+						response.AppendCookie (cookie);
+					}
+
+					if (request.Url.AbsolutePath.Length < 2) {
+						handlers ["/index.htm"].HandleRequest (request, response, conn, permissionLevel);
+						return;
+					} else {
+						foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
+							if (request.Url.AbsolutePath.StartsWith (kvp.Key)) {
+								if (!kvp.Value.IsAuthorizedForHandler (conn, permissionLevel)) {
+									response.StatusCode = (int)HttpStatusCode.Forbidden;
+									if (conn != null) {
+										Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", conn.SteamID, kvp.Value.ModuleName);
+									} else {
+										Log.Out ("Web.HandleRequest: unidentified user from '{0}' not allowed to access '{1}'", request.RemoteEndPoint.Address, kvp.Value.ModuleName);
+									}
+								} else {
+									kvp.Value.HandleRequest (request, response, conn, permissionLevel);
 								}
+								return;
 							}
 						}
-
-						Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + ctx.Request.Url.AbsolutePath + "\"");
-						ctx.Response.StatusCode = (int)HttpStatusCode.NotFound;
-					} else {
-						ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
-						ctx.Response.Headers ["WWW-Authenticate"] = "Basic realm=\"" + realm + "\"";
-					}
+					}
+
+					Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + request.Url.AbsolutePath + "\"");
+					response.StatusCode = (int)HttpStatusCode.NotFound;
 				} catch (IOException e) {
 					if (e.InnerException is SocketException) {
@@ -131,5 +188,5 @@
 				} finally {
 					if (ctx != null) {
-						ctx.Response.OutputStream.Close ();
+						ctx.Response.Close ();
 					}
 					Interlocked.Decrement (ref currentHandlers);
@@ -138,10 +195,42 @@
 		}
 
-		private HttpListenerBasicIdentity Authorize (HttpListenerContext ctx) {
-			try {
-				return (HttpListenerBasicIdentity)ctx.User.Identity;
-			} catch (NullReferenceException) {
-				return null;
-			}
+		private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
+			_con = null;
+
+			string sessionId = null;
+			if (_req.Cookies ["sid"] != null) {
+				sessionId = _req.Cookies ["sid"].Value;
+			}
+
+			if (!string.IsNullOrEmpty (sessionId)) {
+				WebConnection con = connectionHandler.IsLoggedIn (sessionId, _req.RemoteEndPoint.Address.ToString ());
+				if (con != null) {
+					_con = con;
+					return GameManager.Instance.adminTools.GetAdminToolsClientInfo (_con.SteamID.ToString ()).PermissionLevel;
+				}
+			}
+
+			if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
+				WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"], _req.QueryString ["admintoken"]);
+				if (admin != null) {
+					return admin.permissionLevel;
+				} else {
+					Log.Warning ("Invalid Admintoken used from " + _req.RemoteEndPoint.ToString ());
+				}
+			}
+
+			if (_req.Url.AbsolutePath.StartsWith ("/session/verify")) {
+				ulong id = OpenID.Validate (_req);
+				if (id > 0) {
+					WebConnection con = connectionHandler.LogIn (id, _req.RemoteEndPoint.Address.ToString ());
+					_con = con;
+					//Log.Out ("Logged in with session id: {0}", con.SessionID);
+					return GameManager.Instance.adminTools.GetAdminToolsClientInfo (id.ToString ()).PermissionLevel;
+				} else {
+					Log.Out ("Steam OpenID login failed from {0}", _req.RemoteEndPoint.ToString ());
+				}
+			}
+
+			return GUEST_PERMISSION_LEVEL;
 		}
 
@@ -156,13 +245,9 @@
 
 		public void SendLine (string line) {
-			try {
-				//Log.Out ("NOT IMPLEMENTED: Web.WriteToClient");
-			} catch (Exception e) {
-				Log.Out ("Error in Web.WriteToClient: " + e);
-			}
+			connectionHandler.SendLine (line);
 		}
 
 		public void SendLog (string text, string trace, UnityEngine.LogType type) {
-			//throw new System.NotImplementedException ();
+			connectionHandler.SendLog (text, trace, type);
 		}
 
Index: binary-improvements/MapRendering/Web/WebConnection.cs
===================================================================
--- binary-improvements/MapRendering/Web/WebConnection.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/WebConnection.cs	(revision 244)
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+
+using UnityEngine;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public class WebConnection : ConsoleConnectionAbstract {
+		private string sessionId;
+		private string endpoint;
+		private ulong steamId;
+		private DateTime login;
+		private DateTime lastAction;
+
+		private List<string> outputLines = new List<string> ();
+		private List<LogLine> logLines = new List<LogLine> ();
+
+		public string SessionID {
+			get { return sessionId; }
+		}
+
+		public string Endpoint {
+			get { return endpoint; }
+		}
+
+		public ulong SteamID {
+			get { return steamId; }
+		}
+
+		public TimeSpan Age {
+			get { return DateTime.Now - lastAction; }
+		}
+
+		public WebConnection (string _sessionId, string _endpoint, ulong _steamId) {
+			this.sessionId = _sessionId;
+			this.endpoint = _endpoint;
+			this.steamId = _steamId;
+			this.login = DateTime.Now;
+			this.lastAction = this.login;
+		}
+
+		public void UpdateUsage () {
+			this.lastAction = DateTime.Now;
+		}
+
+		public override string GetDescription () {
+			return "WebPanel from " + endpoint;
+		}
+
+		public override void SendLine (string _text) {
+			outputLines.Add (_text);
+		}
+
+		public override void SendLines (List<string> _output) {
+			outputLines.AddRange (_output);
+		}
+
+		public override void SendLog (string _msg, string _trace, LogType _type) {
+			LogLine ll = new LogLine ();
+			ll.message = _msg;
+			ll.trace = _trace;
+			ll.type = _type;
+			logLines.Add (ll);
+		}
+
+		private struct LogLine {
+			public string message;
+			public string trace;
+			public LogType type;
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/WebPermissions.cs
===================================================================
--- binary-improvements/MapRendering/Web/WebPermissions.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/WebPermissions.cs	(revision 244)
@@ -0,0 +1,325 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public class WebPermissions {
+		private static WebPermissions instance = null;
+
+		public static WebPermissions Instance {
+			get {
+				lock (typeof(WebPermissions)) {
+					if (instance == null) {
+						instance = new WebPermissions ();
+					}
+					return instance;
+				}
+			}
+		}
+
+		private const string PERMISSIONS_FILE = "webpermissions.xml";
+		Dictionary<string, AdminToken> admintokens;
+		Dictionary<string, WebModulePermission> modules;
+		Dictionary<string, WebModulePermission> knownModules = new Dictionary<string, WebModulePermission> ();
+		WebModulePermission defaultModulePermission = new WebModulePermission ("", 0);
+		FileSystemWatcher fileWatcher;
+
+		public WebPermissions () {
+			Directory.CreateDirectory (GetFilePath ());
+			InitFileWatcher ();
+			Load ();
+		}
+
+		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];
+			} else {
+				return null;
+			}
+		}
+
+		public WebModulePermission GetModulePermission (string _module) {
+			if (modules.ContainsKey (_module.ToLower ())) {
+				return modules [_module.ToLower ()];
+			}
+			return 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.Values.CopyTo (result, 0);
+			return result;
+		}
+	
+
+		// Commands
+		public void AddModulePermission (string _module, int _permissionLevel, bool _save = true) {
+			WebModulePermission p = new WebModulePermission (_module.ToLower (), _permissionLevel);
+			lock (this) {
+				modules [_module] = p;
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public void AddKnownModule (string _module) {
+			if (!string.IsNullOrEmpty (_module)) {
+				lock (this) {
+					knownModules.Add (_module, new WebModulePermission (_module, 0));
+				}
+			}
+		}
+
+		public bool IsKnownModule (string _module) {
+			if (!string.IsNullOrEmpty (_module)) {
+				lock (this) {
+					return knownModules.ContainsKey (_module);
+				}
+			} else {
+				return false;
+			}
+		}
+	
+		public void RemoveModulePermission (string _module, bool _save = true) {
+			lock (this) {
+				modules.Remove (_module.ToLower ());
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public List<WebModulePermission> GetModules () {
+			List<WebModulePermission> result = new List<WebModulePermission> ();
+			foreach (string module in knownModules.Keys) {
+				if (modules.ContainsKey (module)) {
+					result.Add (modules [module]);
+				} else {
+					result.Add (knownModules [module]);
+				}
+			}
+
+			return result;
+		}
+	
+
+		//IO Tasks
+
+		private void InitFileWatcher () {
+			fileWatcher = new FileSystemWatcher (GetFilePath (), GetFileName ());
+			fileWatcher.Changed += new FileSystemEventHandler (OnFileChanged);
+			fileWatcher.Created += new FileSystemEventHandler (OnFileChanged);
+			fileWatcher.Deleted += new FileSystemEventHandler (OnFileChanged);
+			fileWatcher.EnableRaisingEvents = true;
+		}
+
+		private void OnFileChanged (object source, FileSystemEventArgs e) {
+			Log.Out ("Reloading " + PERMISSIONS_FILE);
+			Load ();
+		}
+
+		private string GetFilePath () {
+			return GamePrefs.GetString (EnumGamePrefs.SaveGameFolder);
+		}
+
+		private string GetFileName () {
+			return PERMISSIONS_FILE;
+		}
+
+		private string GetFullPath () {
+			return GetFilePath () + "/" + GetFileName ();
+		}
+
+		public void Load () {
+			admintokens = new Dictionary<string, AdminToken> ();
+			modules = new Dictionary<string, WebModulePermission> ();
+
+			if (!Utils.FileExists (GetFullPath ())) {
+				Log.Out (string.Format ("Permissions file '{0}' not found, creating.", GetFileName ()));
+				Save ();
+				return;
+			}
+
+			Log.Out (string.Format ("Loading permissions file at '{0}'", GetFullPath ()));
+
+			XmlDocument xmlDoc = new XmlDocument ();
+
+			try {
+				xmlDoc.Load (GetFullPath ());
+			} catch (XmlException e) {
+				Log.Error (string.Format ("Failed loading permissions file: {0}", e.Message));
+				return;
+			}
+
+			XmlNode adminToolsNode = xmlDoc.DocumentElement;
+
+			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");
+						int permissionLevel = 2000;
+						if (!int.TryParse (lineItem.GetAttribute ("permission_level"), out 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;
+						}
+					
+						int permissionLevel = 0;
+						if (!int.TryParse (lineItem.GetAttribute ("permission_level"), out permissionLevel)) {
+							Log.Warning ("Ignoring permission-entry because of invalid (non-numeric) value for 'permission_level' attribute: " + subChild.OuterXml);
+							continue;
+						}
+
+						AddModulePermission (lineItem.GetAttribute ("module").ToLower (), 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 ("    <admintokens>");
+				sw.WriteLine ("        <!-- <token name=\"adminuser1\" token=\"supersecrettoken\" permission_level=\"0\" /> -->");
+				foreach (AdminToken at in admintokens.Values) {
+					sw.WriteLine (string.Format ("        <token name=\"{0}\" token=\"{1}\" permission_level=\"{2}\" />", at.name, at.token, at.permissionLevel));
+				}
+				sw.WriteLine ("    </admintokens>");
+				sw.WriteLine ();
+				sw.WriteLine ("    <permissions>");
+				sw.WriteLine ("        <!-- <permission module=\"webapi.executeconsolecommand\" permission_level=\"0\" /> -->");
+				sw.WriteLine ("        <!-- <permission module=\"webapi.getplayersonline\" permission_level=\"1\" /> -->");
+				sw.WriteLine ("        <!-- <permission module=\"web.map\" permission_level=\"1000\" /> -->");
+				foreach (WebModulePermission wap in modules.Values) {
+					sw.WriteLine (string.Format ("        <permission module=\"{0}\" permission_level=\"{1}\" />", wap.module, wap.permissionLevel));
+				}
+				sw.WriteLine ("    </permissions>");
+				sw.WriteLine ();
+				sw.WriteLine ("</webpermissions>");
+			
+				sw.Flush ();
+				sw.Close ();
+			}
+
+			fileWatcher.EnableRaisingEvents = true;
+		}
+
+
+		
+		public class AdminToken {
+			public string name;
+			public string token;
+			public int permissionLevel;
+
+			public AdminToken (string _name, string _token, int _permissionLevel) {
+				name = _name;
+				token = _token;
+				permissionLevel = _permissionLevel;
+			}
+		}
+
+		public struct WebModulePermission {
+			public string module;
+			public int permissionLevel;
+
+			public WebModulePermission (string _module, int _permissionLevel) {
+				module = _module;
+				permissionLevel = _permissionLevel;
+			}
+		}
+
+
+	}
+}
+
Index: binary-improvements/MapRendering/WebAndMapRendering.csproj
===================================================================
--- binary-improvements/MapRendering/WebAndMapRendering.csproj	(revision 243)
+++ binary-improvements/MapRendering/WebAndMapRendering.csproj	(revision 244)
@@ -38,4 +38,8 @@
       <Private>False</Private>
     </Reference>
+    <Reference Include="System.Xml">
+      <HintPath>..\7dtd-binaries\System.Xml.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
   </ItemGroup>
   <ItemGroup>
@@ -48,9 +52,5 @@
     <Compile Include="API.cs" />
     <Compile Include="Web\Web.cs" />
-    <Compile Include="Web\PathHandler.cs" />
-    <Compile Include="Web\StaticHandler.cs" />
-    <Compile Include="Web\SimpleRedirectHandler.cs" />
     <Compile Include="Web\MimeType.cs" />
-    <Compile Include="Web\ApiHandler.cs" />
     <Compile Include="Web\API\GetPlayersOnline.cs" />
     <Compile Include="Web\API\WebAPI.cs" />
@@ -59,5 +59,20 @@
     <Compile Include="Web\API\GetLandClaims.cs" />
     <Compile Include="Commands\webstat.cs" />
-    <Compile Include="Web\ItemIconHandler.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\WebPermissions.cs" />
+    <Compile Include="Web\Handlers\ApiHandler.cs" />
+    <Compile Include="Web\Handlers\ItemIconHandler.cs" />
+    <Compile Include="Web\Handlers\PathHandler.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" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
@@ -71,4 +86,5 @@
   <ItemGroup>
     <Folder Include="Commands\" />
+    <Folder Include="Web\Handlers\" />
   </ItemGroup>
   <ItemGroup>
Index: binary-improvements/bundle_creation/makefile
===================================================================
--- binary-improvements/bundle_creation/makefile	(revision 243)
+++ binary-improvements/bundle_creation/makefile	(revision 244)
@@ -4,5 +4,5 @@
 
 VERSIONFILE=../bin/Mods/Allocs_CommonFunc/7dtd-server-fixes_version.txt
-VERSION=$(shell cat ${VERSIONFILE} | grep "Version" | cut -d\  -f8)
+VERSION=$(shell cat ${VERSIONFILE} | grep "Combined" | cut -d\  -f2)
 ARCHIVENAME=server_fixes_v${VERSION}.tar.gz
 
Index: binary-improvements/server-fixes.userprefs
===================================================================
--- binary-improvements/server-fixes.userprefs	(revision 243)
+++ binary-improvements/server-fixes.userprefs	(revision 244)
@@ -1,14 +1,16 @@
 ﻿<Properties>
   <MonoDevelop.Ide.Workspace ActiveConfiguration="Release_Version" />
-  <MonoDevelop.Ide.Workbench ActiveDocument="MapRendering/Web/ApiHandler.cs">
+  <MonoDevelop.Ide.Workbench ActiveDocument="7dtd-server-fixes/src/AssemblyInfo.cs">
     <Files>
       <File FileName="MapRendering/MapRendering/Constants.cs" Line="1" Column="1" />
-      <File FileName="7dtd-server-fixes/src/PersistentData/Player.cs" Line="130" Column="40" />
       <File FileName="7dtd-server-fixes/src/AssemblyInfo.cs" Line="20" Column="35" />
-      <File FileName="7dtd-server-fixes/ModInfo.xml" Line="7" Column="20" />
+      <File FileName="7dtd-server-fixes/ModInfo.xml" Line="2" Column="6" />
       <File FileName="AllocsCommands/ModInfo.xml" Line="7" Column="20" />
       <File FileName="MapRendering/ModInfo.xml" Line="7" Column="20" />
-      <File FileName="MapRendering/Web/ApiHandler.cs" Line="31" Column="3" />
-      <File FileName="7dtd-server-fixes/src/API.cs" Line="12" Column="29" />
+      <File FileName="MapRendering/Web/Web.cs" Line="245" Column="3" />
+      <File FileName="MapRendering/Web/WebPermissions.cs" Line="156" Column="25" />
+      <File FileName="MapRendering/Web/Handlers/SessionHandler.cs" Line="16" Column="123" />
+      <File FileName="MapRendering/Web/Handlers/ApiHandler.cs" Line="34" Column="1" />
+      <File FileName="MapRendering/Web/API/ExecuteConsoleCommand.cs" Line="36" Column="114" />
     </Files>
     <Pads>
Index: binary-improvements/versions.sh
===================================================================
--- binary-improvements/versions.sh	(revision 244)
+++ binary-improvements/versions.sh	(revision 244)
@@ -0,0 +1,10 @@
+#!/bin/bash
+VERSIONS=""
+for F in `find bin -name ModInfo.xml | sort`
+do
+	xmlstarlet sel -T -t -v /xml/ModInfo/Name/@value -o ": " -v /xml/ModInfo/Version/@value -n $F
+	VERSIONS="${VERSIONS}`xmlstarlet sel -T -t -v /xml/ModInfo/Version/@value $F`_"
+done
+VERSIONS=${VERSIONS:0:-1}
+echo "Combined: $VERSIONS"
+
Index: binary-improvements/webserver/css/style.css
===================================================================
--- binary-improvements/webserver/css/style.css	(revision 243)
+++ binary-improvements/webserver/css/style.css	(revision 244)
@@ -1,14 +1,83 @@
-html, body {
-	height: 100%;
-	margin: 0px;
-	padding: 0px;
-	background-color: #230000;
+/* Generic page layout */
+
+body {
+	margin: 0;
+	padding: 0;
 }
-#map {
-	height: 100%;
-	margin: 0px;
-	padding: 0px;
-	background-color: #230000;
+
+.adminwrapper {
+	width: 100%;
+	height: 100vh;
+	background-color: #ccc;
 }
+
+.adminmenu,
+.admincontent {
+	position: absolute;
+	top: 0;
+	bottom: 0;
+}
+
+.adminmenu {
+	width: 200px;
+	left: 0;
+	background-color: purple;
+}
+
+.adminmenu .current_tab {
+	font-weight: bold;
+	text-transform: uppercase;
+}
+
+.adminmenu .menu_button a {
+	color: #000;
+}
+
+.adminmenu #userstate {
+	position: absolute;
+	bottom: 5px;
+	left: 5px;
+	right: 5px;
+	background-color: green;
+}
+
+.adminmenu #userstate #username {
+	padding-left: 10px;
+}
+
+.adminmenu #userstate > div {
+	display: none;
+}
+
+.admincontent {
+	position: absolute;
+	right: 0;
+	left: 200px;
+	background-color: #fff;
+}
+
+.admincontent .contenttab {
+	position: absolute;
+	top: 0;
+	right: 0;
+	left: 0px;
+	display: none;
+}
+
+.admincontent .current_tab {
+	display: block;
+}
+
+/* Individual tabs */
+
+.adminmap {
+	background-color: #260040;
+	bottom: 0;
+}
+
+
+
+/* Inventory dialog overlay */
+
 #info {
 	background-color: #aaaaaa;
@@ -25,6 +94,6 @@
 }
 .invField {
-	width: 60px;
-	height: 42px;
+	width: 58px;
+	height: 40px;
 	padding: 1px 4px;
 	margin: 0px;
@@ -58,11 +127,14 @@
 }
 
-.control-reloadtiles a,
-.control-reloadtiles a:hover {
-	padding: 1px 5px 1px 5px;
-	width: auto;
+
+/* Map controls */
+
+
+.webmap-control a,
+.webmap-control a:hover {
+	text-decoration: none;
 }
 
-.control-coordinates {
+.webmap-control {
 	box-shadow: 0 1px 5px rgba(0,0,0,0.4);
 	background: #fff;
@@ -70,4 +142,5 @@
 	padding: 6px 10px 6px 6px;
 	color: #333;
+	white-space: nowrap;
 }
 
Index: binary-improvements/webserver/index.html
===================================================================
--- binary-improvements/webserver/index.html	(revision 243)
+++ binary-improvements/webserver/index.html	(revision 244)
@@ -28,4 +28,11 @@
 	<link rel="stylesheet" href="leaflet/minimap/Control.MiniMap.css" />
 	<script type="text/javascript" src="leaflet/minimap/Control.MiniMap.js"></script>
+	
+	<!-- Own JS stuff -->
+	<script type="text/javascript" src="js/leaflet.regionlayer.js"></script>
+	<script type="text/javascript" src="js/leaflet.control.coordinates.js"></script>
+	<script type="text/javascript" src="js/leaflet.control.reloadtiles.js"></script>
+	<script type="text/javascript" src="js/leaflet.control.gametime.js"></script>
+	<script type="text/javascript" src="js/util.js"></script>
 
 	<!-- Own stylesheet -->
@@ -34,5 +41,36 @@
 </head>
 <body>
-	<div id="map"></div>
+
+
+	<div class="adminwrapper">
+		<div class="adminmenu">
+			<ul>
+				<li><a href="#tab_map" data-permission="web.map">Map</a></li>
+				<li><a href="#tab_log" data-permission="web.log">Log</a></li>
+			</ul>
+			
+			<div id="userstate">
+				<div id="userstate_loggedin">
+					Logged in as:<br/>
+					<a id="username" href="" target="_blank"></a><br/>
+					<a href="/session/logout">Sign out</a>
+				</div>
+				<div id="userstate_loggedout">
+					Not logged in<br/>
+					<center>
+					<a href="/session/login">
+						<img src="img/steamlogin.png" title="Sign in through Steam">
+					</a>
+					</center>
+				</div>
+			</div>
+		</div>
+		<div class="admincontent">
+			<div id="tab_map" class="adminmap"></div>
+			<div id="tab_log" class="adminlog"></div>
+		</div>
+	</div>
+	
+
 	<div id="playerInventoryDialog" title="Player inventory">
 		Player: <span id="invPlayerName"></span>
@@ -43,6 +81,7 @@
 		</table>
 	</div>
-
+	
 	<script type="text/javascript" src="js/index.js"></script>
 </body>
 </html>
+
Index: binary-improvements/webserver/js/index.js
===================================================================
--- binary-improvements/webserver/js/index.js	(revision 243)
+++ binary-improvements/webserver/js/index.js	(revision 244)
@@ -2,8 +2,10 @@
 // Constants
 
-var REGIONSIZE = 512;
-var CHUNKSIZE = 16;
-var TILESIZE = 128;
-var MAXZOOM = 4;
+var mapinfo = {
+	regionsize: 512,
+	chunksize: 16,
+	tilesize: 128,
+	maxzoom: 4
+}
 
 var BAG_COLS = 8;
@@ -13,4 +15,6 @@
 var INV_ITEM_HEIGHT = 40;
 
+var userdata = false;
+
 
 function initMap() {
@@ -21,12 +25,12 @@
 		project: function (latlng) {
 			return new L.Point(
-				(latlng.lat) / Math.pow(2, MAXZOOM),
-				(latlng.lng) / Math.pow(2, MAXZOOM) );
+				(latlng.lat) / Math.pow(2, mapinfo.maxzoom),
+				(latlng.lng) / Math.pow(2, mapinfo.maxzoom) );
 		},
 		
 		unproject: function (point) {
 			return new L.LatLng(
-				point.x * Math.pow(2, MAXZOOM),
-				point.y * Math.pow(2, MAXZOOM) );
+				point.x * Math.pow(2, mapinfo.maxzoom),
+				point.y * Math.pow(2, mapinfo.maxzoom) );
 		}
 	};
@@ -41,25 +45,4 @@
 	});
 
-	var CoordToChunk = function(latlng) {
-		var x = Math.floor(((latlng.lat + 16777216) / CHUNKSIZE) - (16777216 / CHUNKSIZE));
-		var y = Math.floor(((latlng.lng + 16777216) / CHUNKSIZE) - (16777216 / CHUNKSIZE));
-		return L.latLng(x, y);
-	}
-
-	var CoordToRegion = function(latlng) {
-		var x = Math.floor(((latlng.lat + 16777216) / REGIONSIZE) - (16777216 / REGIONSIZE));
-		var y = Math.floor(((latlng.lng + 16777216) / REGIONSIZE) - (16777216 / REGIONSIZE));
-		return L.latLng(x, y);
-	}
-
-	var FormatCoord = function(latlng) {
-		return "" +
-			Math.abs(latlng.lng) + (latlng.lng>=0 ? " N" : " S") + " / " +
-			Math.abs(latlng.lat) + (latlng.lat>=0 ? " E" : " W");
-	}
-
-	var FormatRegionFileName = function(latlng) {
-		return "r." + latlng.lat + "." + latlng.lng + ".7rg";
-	}
 
 
@@ -68,23 +51,23 @@
 	// Map and basic tile layers
 
-	var tileTime = new Date().getTime();
-
-	map = L.map('map', {
+	var initTime = new Date().getTime();
+
+	map = L.map('tab_map', {
 		zoomControl: false, // Added by Zoomslider
 		zoomsliderControl: true,
 		attributionControl: false,
 		crs: SDTD_CRS
-	}).setView([0, 0], Math.max(0, MAXZOOM-5));
+	}).setView([0, 0], Math.max(0, mapinfo.maxzoom - 5));
 
 	var tileLayer = L.tileLayer('../map/{z}/{x}/{y}.png?t={time}', {
-		maxZoom:MAXZOOM+1,
-		minZoom: Math.max(0, MAXZOOM-5),
-		maxNativeZoom: MAXZOOM,
-		tileSize: TILESIZE,
+		maxZoom: mapinfo.maxzoom + 1,
+		minZoom: Math.max(0, mapinfo.maxzoom - 5),
+		maxNativeZoom: mapinfo.maxzoom,
+		tileSize: mapinfo.tilesize,
 		continuousWorld: true,
 		tms: true,
 		unloadInvisibleTiles: false,
-		time: function() { return tileTime; }
-	}).addTo(map);
+		time: initTime
+	});
 	
 	// TileLayer w/ TMS=true fix for zoomlevel >= 8
@@ -94,169 +77,16 @@
 
 	var tileLayerMiniMap = L.tileLayer('../map/{z}/{x}/{y}.png?t={time}', {
-		maxZoom: MAXZOOM,
+		maxZoom: mapinfo.maxzoom,
 		minZoom: 0,
-		maxNativeZoom: MAXZOOM,
-		tileSize: TILESIZE,
+		maxNativeZoom: mapinfo.maxzoom,
+		tileSize: mapinfo.tilesize,
 		continuousWorld: true,
 		tms: true,
 		unloadInvisibleTiles: false,
-		time: function() { return tileTime; }
-	});
-
-	var regionLayer = L.tileLayer.canvas({
-		maxZoom: MAXZOOM+1,
-		minZoom: 0,
-		maxNativeZoom: MAXZOOM+1,
-		tileSize: TILESIZE,
-		continuousWorld: true
-	});
-
-	regionLayer.drawTile = function(canvas, tilePoint, zoom) {
-		var blockWorldSize = TILESIZE * Math.pow(2, MAXZOOM-zoom);
-		var tileLeft = tilePoint.x * blockWorldSize;
-		var tileBottom = (-1-tilePoint.y) * blockWorldSize;
-		var blockPos = L.latLng(tileLeft, tileBottom);
-	
-		var ctx = canvas.getContext('2d');
-	
-		ctx.strokeStyle = "lightblue";
-		ctx.fillStyle = "lightblue";
-		ctx.lineWidth = 1;
-		ctx.font="14px Arial";
-	
-		var lineCount = blockWorldSize / REGIONSIZE;
-		if (lineCount >= 1) {
-			var pos = 0;
-			while (pos < TILESIZE) {
-				// Vertical
-				ctx.beginPath();
-				ctx.moveTo(pos, 0);
-				ctx.lineTo(pos, TILESIZE);
-				ctx.stroke();
-			
-				// Horizontal
-				ctx.beginPath();
-				ctx.moveTo(0, pos);
-				ctx.lineTo(TILESIZE, pos);
-				ctx.stroke();
-
-				pos += TILESIZE / lineCount;
-			}
-			ctx.fillText(FormatRegionFileName(CoordToRegion(blockPos)), 4, TILESIZE-5);
-		} else {
-			if ((tileLeft % REGIONSIZE) == 0) {
-				// Vertical
-				ctx.beginPath();
-				ctx.moveTo(0, 0);
-				ctx.lineTo(0, TILESIZE);
-				ctx.stroke();
-			}
-			if ((tileBottom % REGIONSIZE) == 0) {
-				// Horizontal
-				ctx.beginPath();
-				ctx.moveTo(0, TILESIZE);
-				ctx.lineTo(TILESIZE, TILESIZE);
-				ctx.stroke();
-			}
-			if ((tileLeft % REGIONSIZE) == 0 && (tileBottom % REGIONSIZE) == 0) {
-				ctx.fillText(FormatRegionFileName(CoordToRegion(blockPos)), 4, TILESIZE-5);
-			}
-		}
-
-	}
-
-
-	// ===============================================================================================
-	// Reload control
-
-	L.Control.ReloadTiles = L.Control.extend({
-		options: {
-			position: 'bottomleft'
-		},
-
-		onAdd: function (map) {
-			var name = 'control-reloadtiles',
-			    container = L.DomUtil.create('div', name + ' leaflet-bar');
-
-			this._map = map;
-
-			this._reloadbutton = this._createButton(
-				"Reload tiles", "Reload tiles",
-				name + "-btn", container, this._reload, this);
-
-			return container;
-		},
-
-		onRemove: function (map) {
-		},
-
-		_reload: function (e) {
-			tileTime = new Date().getTime();
-			tileLayer.redraw();
-			tileLayerMiniMap.redraw();
-		},
-
-		_createButton: function (html, title, className, container, fn, context) {
-			var link = L.DomUtil.create('a', className, container);
-			link.innerHTML = html;
-			link.href = '#';
-			link.title = title;
-
-			var stop = L.DomEvent.stopPropagation;
-
-			L.DomEvent
-			    .on(link, 'click', stop)
-			    .on(link, 'mousedown', stop)
-			    .on(link, 'dblclick', stop)
-			    .on(link, 'click', L.DomEvent.preventDefault)
-			    .on(link, 'click', fn, context)
-			    .on(link, 'click', this._refocusOnMap, context);
-
-			return link;
-		}
-
-	});
-
-	new L.Control.ReloadTiles({
-	}).addTo(map);
-
-
-	// ===============================================================================================
-	// Coordinates control
-	//	<div id="info">
-	//		MouseCoords: <span id="pos"></span>
-	//	</div>
-
-	L.Control.Coordinates = L.Control.extend({
-		options: {
-			position: 'bottomleft'
-		},
-
-		onAdd: function (map) {
-			var name = 'control-coordinates',
-			    container = L.DomUtil.create('div', name + ' leaflet-bar');
-		
-			container.innerHTML = "- N / - E"
-
-			this._map = map;
-			this._div = container;
-
-			map.on('mousemove', this._onMouseMove, this);
-
-			return container;
-		},
-
-		onRemove: function (map) {
-		},
-	
-		_onMouseMove: function (e) {
-			this._div.innerHTML = FormatCoord(e.latlng);
-		}
-
-
-	});
-
-	new L.Control.Coordinates({
-	}).addTo(map);
+		time: initTime
+	});
+
+
+
 
 
@@ -268,5 +98,5 @@
 	var playersOnlineMarkerGroup = L.layerGroup();
 	var playersOfflineMarkerGroup = L.markerClusterGroup({
-		maxClusterRadius: function(zoom) { return zoom == MAXZOOM ? 10 : 50; }
+		maxClusterRadius: function(zoom) { return zoom == mapinfo.maxzoom ? 10 : 50; }
 	});
 
@@ -274,5 +104,5 @@
 	var landClaimsGroup = L.layerGroup();
 	var landClaimsClusterGroup = L.markerClusterGroup({
-		disableClusteringAtZoom: MAXZOOM,
+		disableClusteringAtZoom: mapinfo.maxzoom,
 		singleMarkerMode: true,
 		maxClusterRadius: 50
@@ -287,20 +117,38 @@
 	};
 
-	var overlays = {
-		"Land claims" : landClaimsGroup,
-		"Players (offline) (<span id='mapControlOfflineCount'>0</span>)" : playersOfflineMarkerGroup,
-		"Players (online) (<span id='mapControlOnlineCount'>0</span>)" : playersOnlineMarkerGroup,
-		"Region files": regionLayer,
-	};
-
+	var layerControl = L.control.layers(baseLayers, null, {
+		collapsed: false
+	});
 	
-	L.control.layers(baseLayers, overlays, {
-		collapsed: false
-	}).addTo(map);
-
-	var miniMap = new L.Control.MiniMap(tileLayerMiniMap, {
-		zoomLevelOffset: -6,
-		toggleDisplay: true
-	}).addTo(map);
+	var layerCount = 0;
+
+
+	if (hasPermission ("web.map")) {
+		tileLayer.addTo(map);
+		new L.Control.Coordinates({}).addTo(map);
+		new L.Control.ReloadTiles({layers: [tileLayer, tileLayerMiniMap]}).addTo(map);
+		layerControl.addOverlay (GetRegionLayer (mapinfo), "Region files");
+		var miniMap = new L.Control.MiniMap(tileLayerMiniMap, {
+			zoomLevelOffset: -6,
+			toggleDisplay: true
+		}).addTo(map);
+		
+		if (hasPermission ("webapi.getstats")) {
+			new L.Control.GameTime({}).addTo(map);
+		}
+	}
+	if (hasPermission ("webapi.getlandclaims")) {
+		layerControl.addOverlay (landClaimsGroup, "Land claims");
+		layerCount++;
+	}
+	if (hasPermission ("webapi.getplayerslocation")) {
+		layerControl.addOverlay (playersOfflineMarkerGroup, "Players (offline) (<span id='mapControlOfflineCount'>0</span>)");
+		layerControl.addOverlay (playersOnlineMarkerGroup, "Players (online) (<span id='mapControlOnlineCount'>0</span>)");
+		layerCount++;
+	}
+
+	if (layerCount > 0) {
+		layerControl.addTo(map);
+	}
 
 
@@ -392,6 +240,8 @@
 			} else {
 				marker = L.marker([val.position.x, val.position.z]).bindPopup(
-					"Player: " + val.name + "<br/>" +
-					"<a class='inventoryButton' data-steamid='"+val.steamid+"'>Show inventory</a>"
+					"Player: " + val.name +
+					(hasPermission ("webapi.getplayerinventory") ?
+						"<br/><a class='inventoryButton' data-steamid='"+val.steamid+"'>Show inventory</a>"
+						: "")
 				);
 				playersMappingList[val.steamid] = { online: !val.online };
@@ -431,5 +281,7 @@
 	}
 
-	window.setTimeout(updatePlayerEvent, 500);
+	if (hasPermission ("webapi.getplayerslocation")) {
+		window.setTimeout(updatePlayerEvent, 0);
+	}
 
 
@@ -443,6 +295,6 @@
 	
 		var claimPower = Math.floor(Math.log(data.claimsize) / Math.LN2);
-		var maxClusterZoomUnlimited = MAXZOOM - (claimPower - 3);
-		var maxClusterZoomLimitedMax = Math.min(maxClusterZoomUnlimited, MAXZOOM+1);
+		var maxClusterZoomUnlimited = mapinfo.maxzoom - (claimPower - 3);
+		var maxClusterZoomLimitedMax = Math.min(maxClusterZoomUnlimited, mapinfo.maxzoom+1);
 		maxZoomForCluster = Math.max(maxClusterZoomLimitedMax, 0);
 	
@@ -516,14 +368,59 @@
 
 
-$.getJSON( "../map/mapinfo.json")
-.done(function(data) {
-	TILESIZE = data.blockSize;
-	MAXZOOM = data.maxZoom;
-})
-.fail(function(jqxhr, textStatus, error) {
-	console.log("Error fetching map information");
-})
-.always(function() {
-	initMap();
-});
-
+
+function doTabs () {
+	$(".adminmenu > ul > li").addClass ("menu_button");
+	$(".admincontent > div").addClass ("contenttab");
+	$(".adminmenu .menu_button").first ().addClass ("current_tab");
+	$(".menu_button").on ('click.action', null, function (event) {
+		var menuElement = $(this);
+		var linkElement = menuElement.children ("a");
+		var linkName = linkElement.attr ("href");
+		
+		$("*").removeClass ("current_tab");
+		menuElement.addClass ("current_tab");
+		$(linkName).addClass ("current_tab");
+	});
+	
+	$(".adminmenu .menu_button").first ().click ();
+}
+
+function initMapInfo () {
+	$.getJSON( "../map/mapinfo.json")
+	.done(function(data) {
+		mapinfo.tilesize = data.blockSize;
+		mapinfo.maxzoom = data.maxZoom;
+	})
+	.fail(function(jqxhr, textStatus, error) {
+		console.log("Error fetching map information");
+	})
+	.always(function() {
+		initMap();
+	});
+}
+
+function initUser () {
+	$.getJSON( "../userstatus")
+	.done(function(data) {
+		userdata = data;
+		
+		var userdataDiv = $("#userstate");
+		if (userdata.loggedin == true) {
+			var data = userdataDiv.children ("#userstate_loggedin");
+			data.attr ("style", "display: block");
+			data.children ("#username").attr ("href", "http://steamcommunity.com/profiles/" + userdata.username);
+			data.children ("#username").html (userdata.username);
+		} else {
+			var data = userdataDiv.children ("#userstate_loggedout");
+			data.attr ("style", "display: block");
+		}
+
+		initMapInfo ();
+	})
+	.fail(function(jqxhr, textStatus, error) {
+		console.log("Error fetching user data");
+	})
+}
+
+doTabs ();
+initUser ();
Index: binary-improvements/webserver/js/leaflet.control.coordinates.js
===================================================================
--- binary-improvements/webserver/js/leaflet.control.coordinates.js	(revision 244)
+++ binary-improvements/webserver/js/leaflet.control.coordinates.js	(revision 244)
@@ -0,0 +1,59 @@
+L.Control.Coordinates = L.Control.extend({
+	options: {
+		position: 'bottomleft'
+	},
+
+	onAdd: function (map) {
+		var name = 'control-coordinates',
+		    container = L.DomUtil.create('div', name + ' webmap-control');
+	
+		container.innerHTML = "Mouse pos: - N / - E<br/>Last click: - N / - E"
+		L.DomEvent.on (container, 'mousemove', L.DomEvent.stopPropagation);
+
+		this._map = map;
+		this._div = container;
+
+		map.on('mousemove', this._onMouseMove, this);
+		map.on('mouseout', this._onMouseOut, this);
+		map.on('click', this._onClick, this);
+
+		return container;
+	},
+
+	onRemove: function (map) {
+	},
+
+	_onMouseMove: function (e) {
+		this.lastPos = e.latlng;
+		this._updateText ();
+	},
+	
+	_onMouseOut: function (e) {
+		this.lastPos = false;
+		this._updateText ();
+	},
+
+	_onClick: function (e) {
+		this.lastClick = e.latlng;
+		this._updateText ();
+	},
+	
+	_updateText: function (e) {
+		this._div.innerHTML = "Mouse pos: " + this._formatCoord(this.lastPos) + "<br/>" +
+				"Last click: " + this._formatCoord(this.lastClick);
+	},
+
+	_formatCoord: function(latlng) {
+		if (latlng == false)
+			return "- N / - E";
+		else
+			return "" +
+				Math.abs(latlng.lng).toFixed(0) + (latlng.lng>=0 ? " N" : " S") + " / " +
+				Math.abs(latlng.lat).toFixed(0) + (latlng.lat>=0 ? " E" : " W");
+	},
+	
+	lastPos: false,
+	lastClick: false
+
+});
+
Index: binary-improvements/webserver/js/leaflet.control.gametime.js
===================================================================
--- binary-improvements/webserver/js/leaflet.control.gametime.js	(revision 244)
+++ binary-improvements/webserver/js/leaflet.control.gametime.js	(revision 244)
@@ -0,0 +1,47 @@
+L.Control.GameTime = L.Control.extend({
+	options: {
+		position: 'bottomright'
+	},
+
+	onAdd: function (map) {
+		var name = 'control-gametime',
+		    container = L.DomUtil.create('div', name + ' webmap-control');
+	
+		container.innerHTML = "Loading ..."
+		L.DomEvent.on (container, 'mousemove', L.DomEvent.stopPropagation);
+
+		this._map = map;
+		this._div = container;
+
+		window.setTimeout($.proxy(this._updateGameTimeEvent, this), 0);
+
+		return container;
+	},
+
+	onRemove: function (map) {
+	},
+
+	_updateGameTimeEvent: function() {
+		var div = this._div;
+		$.getJSON( "../api/getstats")
+		.done(function(data) {
+			var time = "Day " + data.gametime.days + ", ";
+			if (data.gametime.hours < 10)
+				time += "0";
+			time += data.gametime.hours;
+			time += ":";
+			if (data.gametime.minutes < 10)
+				time += "0";
+			time += data.gametime.minutes;
+			div.innerHTML = time;
+		})
+		.fail(function(jqxhr, textStatus, error) {
+			console.log("Error fetching game stats");
+		})
+		.always(function() {
+		});
+		window.setTimeout($.proxy(this._updateGameTimeEvent, this), 2000);
+	}
+
+});
+
Index: binary-improvements/webserver/js/leaflet.control.reloadtiles.js
===================================================================
--- binary-improvements/webserver/js/leaflet.control.reloadtiles.js	(revision 244)
+++ binary-improvements/webserver/js/leaflet.control.reloadtiles.js	(revision 244)
@@ -0,0 +1,54 @@
+L.Control.ReloadTiles = L.Control.extend({
+	options: {
+		position: 'bottomleft',
+		layers: []
+	},
+
+	onAdd: function (map) {
+		var name = 'control-reloadtiles',
+		    container = L.DomUtil.create('div', name + ' webmap-control');
+
+		L.DomEvent.on (container, 'mousemove', L.DomEvent.stopPropagation);
+
+		this._map = map;
+
+		this._reloadbutton = this._createButton(
+			"Reload tiles", "Reload tiles",
+			name + "-btn", container, this._reload, this);
+
+		return container;
+	},
+
+	onRemove: function (map) {
+	},
+
+	_reload: function (e) {
+		var newTileTime = new Date().getTime();
+		
+		for (var i = 0; i < this.options.layers.length; i++) {
+			this.options.layers [i].options.time = newTileTime;
+			this.options.layers [i].redraw ();
+		}
+	},
+
+	_createButton: function (html, title, className, container, fn, context) {
+		var link = L.DomUtil.create('a', className, container);
+		link.innerHTML = html;
+		link.href = '#';
+		link.title = title;
+
+		var stop = L.DomEvent.stopPropagation;
+
+		L.DomEvent
+		    .on(link, 'click', stop)
+		    .on(link, 'mousedown', stop)
+		    .on(link, 'dblclick', stop)
+		    .on(link, 'click', L.DomEvent.preventDefault)
+		    .on(link, 'click', fn, context)
+		    .on(link, 'click', this._refocusOnMap, context);
+		   
+		return link;
+	}
+
+});
+
Index: binary-improvements/webserver/js/leaflet.regionlayer.js
===================================================================
--- binary-improvements/webserver/js/leaflet.regionlayer.js	(revision 244)
+++ binary-improvements/webserver/js/leaflet.regionlayer.js	(revision 244)
@@ -0,0 +1,70 @@
+function GetRegionLayer (mapinfo) {
+	var FormatRegionFileName = function(latlng) {
+		return "r." + latlng.lat + "." + latlng.lng + ".7rg";
+	}
+
+	var regionLayer = L.tileLayer.canvas({
+		maxZoom: mapinfo.maxzoom + 1,
+		minZoom: 0,
+		maxNativeZoom: mapinfo.maxzoom + 1,
+		tileSize: mapinfo.tilesize,
+		continuousWorld: true
+	});
+
+	regionLayer.drawTile = function(canvas, tilePoint, zoom) {
+		var blockWorldSize = mapinfo.tilesize * Math.pow(2, mapinfo.maxzoom - zoom);
+		var tileLeft = tilePoint.x * blockWorldSize;
+		var tileBottom = (-1-tilePoint.y) * blockWorldSize;
+		var blockPos = L.latLng(tileLeft, tileBottom);
+
+		var ctx = canvas.getContext('2d');
+
+		ctx.strokeStyle = "lightblue";
+		ctx.fillStyle = "lightblue";
+		ctx.lineWidth = 1;
+		ctx.font="14px Arial";
+
+		var lineCount = blockWorldSize / mapinfo.regionsize;
+		if (lineCount >= 1) {
+			var pos = 0;
+			while (pos < mapinfo.tilesize) {
+				// Vertical
+				ctx.beginPath();
+				ctx.moveTo(pos, 0);
+				ctx.lineTo(pos, mapinfo.tilesize);
+				ctx.stroke();
+		
+				// Horizontal
+				ctx.beginPath();
+				ctx.moveTo(0, pos);
+				ctx.lineTo(mapinfo.tilesize, pos);
+				ctx.stroke();
+
+				pos += mapinfo.tilesize / lineCount;
+			}
+			ctx.fillText(FormatRegionFileName(CoordToRegion(blockPos)), 4, mapinfo.tilesize-5);
+		} else {
+			if ((tileLeft % mapinfo.regionsize) == 0) {
+				// Vertical
+				ctx.beginPath();
+				ctx.moveTo(0, 0);
+				ctx.lineTo(0, mapinfo.tilesize);
+				ctx.stroke();
+			}
+			if ((tileBottom % mapinfo.regionsize) == 0) {
+				// Horizontal
+				ctx.beginPath();
+				ctx.moveTo(0, mapinfo.tilesize);
+				ctx.lineTo(mapinfo.tilesize, mapinfo.tilesize);
+				ctx.stroke();
+			}
+			if ((tileLeft % mapinfo.regionsize) == 0 && (tileBottom % mapinfo.regionsize) == 0) {
+				ctx.fillText(FormatRegionFileName(CoordToRegion(blockPos)), 4, mapinfo.tilesize-5);
+			}
+		}
+
+	}
+	
+	return regionLayer;
+}
+
Index: binary-improvements/webserver/js/util.js
===================================================================
--- binary-improvements/webserver/js/util.js	(revision 244)
+++ binary-improvements/webserver/js/util.js	(revision 244)
@@ -0,0 +1,21 @@
+var CoordToChunk = function(latlng) {
+	var x = Math.floor(((latlng.lat + 16777216) / mapinfo.chunksize) - (16777216 / mapinfo.chunksize));
+	var y = Math.floor(((latlng.lng + 16777216) / mapinfo.chunksize) - (16777216 / mapinfo.chunksize));
+	return L.latLng(x, y);
+}
+
+var CoordToRegion = function(latlng) {
+	var x = Math.floor(((latlng.lat + 16777216) / mapinfo.regionsize) - (16777216 / mapinfo.regionsize));
+	var y = Math.floor(((latlng.lng + 16777216) / mapinfo.regionsize) - (16777216 / mapinfo.regionsize));
+	return L.latLng(x, y);
+}
+
+function hasPermission (modulename) {
+	for (var i = 0; i < userdata.permissions.length; i++) {
+		if (userdata.permissions [i].module == modulename) {
+			return userdata.permissions [i].allowed;
+		}
+	}
+	return false;
+}
+
Index: binary-improvements/webserver/sessionfooter.tmpl
===================================================================
--- binary-improvements/webserver/sessionfooter.tmpl	(revision 244)
+++ binary-improvements/webserver/sessionfooter.tmpl	(revision 244)
@@ -0,0 +1,2 @@
+</body>
+</html>
Index: binary-improvements/webserver/sessionheader.tmpl
===================================================================
--- binary-improvements/webserver/sessionheader.tmpl	(revision 244)
+++ binary-improvements/webserver/sessionheader.tmpl	(revision 244)
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="UTF-8">
+	<title>7dtd map browser</title>
+
+	<!-- Own stylesheet -->
+	<link rel="stylesheet" href="css/style.css" media="screen" type="text/css" />
+
+</head>
+<body>
+
