diff --git a/WebMap/Config.cs b/WebMap/Config.cs new file mode 100644 index 0000000..e67d841 --- /dev/null +++ b/WebMap/Config.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using TinyJson; + +namespace WebMap { + + static class WebMapConfig { + + public static int TEXTURE_SIZE = 2048; + public static int PIXEL_SIZE = 12; + public static float EXPLORE_RADIUS = 100f; + public static float UPDATE_FOG_TEXTURE_INTERVAL = 1f; + public static float SAVE_FOG_TEXTURE_INTERVAL = 30f; + public static int MAX_PINS_PER_USER = 50; + + public static int SERVER_PORT = 3000; + public static double PLAYER_UPDATE_INTERVAL = 0.5; + public static bool CACHE_SERVER_FILES = false; + + public static TValue GetValueOrDefault( + this IDictionary dictionary, TKey key, TValue defaultValue) { + + TValue value; + return dictionary.TryGetValue(key, out value) ? value : defaultValue; + } + + public static void readConfigFile(string configFile) { + string fileJson = ""; + try { + fileJson = File.ReadAllText(configFile); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO READ CONFIG FILE AT: " + configFile); + System.Environment.Exit(1); + } + + var configJson = (Dictionary)fileJson.FromJson(); + + if (configJson == null) { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE CONFIG FILE AT: " + configFile + " . INVALID SYNTAX?"); + System.Environment.Exit(1); + } + + try { + TEXTURE_SIZE = (int)configJson.GetValueOrDefault("texture_size", 2048); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE texture_size VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + + try { + PIXEL_SIZE = (int)configJson.GetValueOrDefault("pixel_size", 12); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE pixel_size VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + + try { + EXPLORE_RADIUS = (float)Convert.ToDouble(configJson.GetValueOrDefault("explore_radius", 100f)); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE explore_radius VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + + try { + UPDATE_FOG_TEXTURE_INTERVAL = (float)Convert.ToDouble(configJson.GetValueOrDefault("update_fog_texture_interval", 1f)); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE update_fog_texture_interval VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + + try { + SAVE_FOG_TEXTURE_INTERVAL = (float)Convert.ToDouble(configJson.GetValueOrDefault("save_fog_texture_interval", 30f)); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE save_fog_texture_interval VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + + try { + MAX_PINS_PER_USER = (int)configJson.GetValueOrDefault("max_pins_per_user", 50); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE max_pins_per_user VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + + try { + SERVER_PORT = (int)configJson.GetValueOrDefault("server_port", 3000); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE server_port VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + + try { + PLAYER_UPDATE_INTERVAL = Convert.ToDouble(configJson.GetValueOrDefault("player_update_interval", 0.5)); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE player_update_interval VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + + try { + CACHE_SERVER_FILES = (bool)configJson.GetValueOrDefault("cache_server_files", true); + } catch { + System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE cache_server_files VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); + } + } + + public static string makeClientConfigJSON() { + var sb = new StringBuilder(); + sb.Length = 0; + + sb.Append("{"); + sb.Append($"\"texture_size\":{TEXTURE_SIZE},"); + sb.Append($"\"pixel_size\":{PIXEL_SIZE},"); + sb.Append($"\"explore_radius\":{EXPLORE_RADIUS}"); + sb.Append("}"); + + return sb.ToString(); + } + } +} diff --git a/WebMap/JSONParser.cs b/WebMap/JSONParser.cs new file mode 100644 index 0000000..61524f9 --- /dev/null +++ b/WebMap/JSONParser.cs @@ -0,0 +1,373 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; + +// https://github.com/zanders3/json + +namespace TinyJson +{ + // Really simple JSON parser in ~300 lines + // - Attempts to parse JSON files with minimal GC allocation + // - Nice and simple "[1,2,3]".FromJson>() API + // - Classes and structs can be parsed too! + // class Foo { public int Value; } + // "{\"Value\":10}".FromJson() + // - Can parse JSON without type information into Dictionary and List e.g. + // "[1,2,3]".FromJson().GetType() == typeof(List) + // "{\"Value\":10}".FromJson().GetType() == typeof(Dictionary) + // - No JIT Emit support to support AOT compilation on iOS + // - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead. + // - Only public fields and property setters on classes/structs will be written to + // + // Limitations: + // - No JIT Emit support to parse structures quickly + // - Limited to parsing <2GB JSON files (due to int.MaxValue) + // - Parsing of abstract classes or interfaces is NOT supported and will throw an exception. + public static class JSONParser + { + [ThreadStatic] static Stack> splitArrayPool; + [ThreadStatic] static StringBuilder stringBuilder; + [ThreadStatic] static Dictionary> fieldInfoCache; + [ThreadStatic] static Dictionary> propertyInfoCache; + + public static T FromJson(this string json) + { + // Initialize, if needed, the ThreadStatic variables + if (propertyInfoCache == null) propertyInfoCache = new Dictionary>(); + if (fieldInfoCache == null) fieldInfoCache = new Dictionary>(); + if (stringBuilder == null) stringBuilder = new StringBuilder(); + if (splitArrayPool == null) splitArrayPool = new Stack>(); + + //Remove all whitespace not within strings to make parsing simpler + stringBuilder.Length = 0; + for (int i = 0; i < json.Length; i++) + { + char c = json[i]; + if (c == '"') + { + i = AppendUntilStringEnd(true, i, json); + continue; + } + if (char.IsWhiteSpace(c)) + continue; + + stringBuilder.Append(c); + } + + //Parse the thing! + return (T)ParseValue(typeof(T), stringBuilder.ToString()); + } + + static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) + { + stringBuilder.Append(json[startIdx]); + for (int i = startIdx + 1; i < json.Length; i++) + { + if (json[i] == '\\') + { + if (appendEscapeCharacter) + stringBuilder.Append(json[i]); + stringBuilder.Append(json[i + 1]); + i++;//Skip next character as it is escaped + } + else if (json[i] == '"') + { + stringBuilder.Append(json[i]); + return i; + } + else + stringBuilder.Append(json[i]); + } + return json.Length - 1; + } + + //Splits { :, : } and [ , ] into a list of strings + static List Split(string json) + { + List splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List(); + splitArray.Clear(); + if (json.Length == 2) + return splitArray; + int parseDepth = 0; + stringBuilder.Length = 0; + for (int i = 1; i < json.Length - 1; i++) + { + switch (json[i]) + { + case '[': + case '{': + parseDepth++; + break; + case ']': + case '}': + parseDepth--; + break; + case '"': + i = AppendUntilStringEnd(true, i, json); + continue; + case ',': + case ':': + if (parseDepth == 0) + { + splitArray.Add(stringBuilder.ToString()); + stringBuilder.Length = 0; + continue; + } + break; + } + + stringBuilder.Append(json[i]); + } + + splitArray.Add(stringBuilder.ToString()); + + return splitArray; + } + + internal static object ParseValue(Type type, string json) + { + if (type == typeof(string)) + { + if (json.Length <= 2) + return string.Empty; + StringBuilder parseStringBuilder = new StringBuilder(json.Length); + for (int i = 1; i < json.Length - 1; ++i) + { + if (json[i] == '\\' && i + 1 < json.Length - 1) + { + int j = "\"\\nrtbf/".IndexOf(json[i + 1]); + if (j >= 0) + { + parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); + ++i; + continue; + } + if (json[i + 1] == 'u' && i + 5 < json.Length - 1) + { + UInt32 c = 0; + if (UInt32.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c)) + { + parseStringBuilder.Append((char)c); + i += 5; + continue; + } + } + } + parseStringBuilder.Append(json[i]); + } + return parseStringBuilder.ToString(); + } + if (type.IsPrimitive) + { + var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); + return result; + } + if (type == typeof(decimal)) + { + decimal result; + decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); + return result; + } + if (json == "null") + { + return null; + } + if (type.IsEnum) + { + if (json[0] == '"') + json = json.Substring(1, json.Length - 2); + try + { + return Enum.Parse(type, json, false); + } + catch + { + return 0; + } + } + if (type.IsArray) + { + Type arrayType = type.GetElementType(); + if (json[0] != '[' || json[json.Length - 1] != ']') + return null; + + List elems = Split(json); + Array newArray = Array.CreateInstance(arrayType, elems.Count); + for (int i = 0; i < elems.Count; i++) + newArray.SetValue(ParseValue(arrayType, elems[i]), i); + splitArrayPool.Push(elems); + return newArray; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + Type listType = type.GetGenericArguments()[0]; + if (json[0] != '[' || json[json.Length - 1] != ']') + return null; + + List elems = Split(json); + var list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count }); + for (int i = 0; i < elems.Count; i++) + list.Add(ParseValue(listType, elems[i])); + splitArrayPool.Push(elems); + return list; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type keyType, valueType; + { + Type[] args = type.GetGenericArguments(); + keyType = args[0]; + valueType = args[1]; + } + + //Refuse to parse dictionary keys that aren't of type string + if (keyType != typeof(string)) + return null; + //Must be a valid dictionary element + if (json[0] != '{' || json[json.Length - 1] != '}') + return null; + //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON + List elems = Split(json); + if (elems.Count % 2 != 0) + return null; + + var dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 }); + for (int i = 0; i < elems.Count; i += 2) + { + if (elems[i].Length <= 2) + continue; + string keyValue = elems[i].Substring(1, elems[i].Length - 2); + object val = ParseValue(valueType, elems[i + 1]); + dictionary[keyValue] = val; + } + return dictionary; + } + if (type == typeof(object)) + { + return ParseAnonymousValue(json); + } + if (json[0] == '{' && json[json.Length - 1] == '}') + { + return ParseObject(type, json); + } + + return null; + } + + static object ParseAnonymousValue(string json) + { + if (json.Length == 0) + return null; + if (json[0] == '{' && json[json.Length - 1] == '}') + { + List elems = Split(json); + if (elems.Count % 2 != 0) + return null; + var dict = new Dictionary(elems.Count / 2); + for (int i = 0; i < elems.Count; i += 2) + dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]); + return dict; + } + if (json[0] == '[' && json[json.Length - 1] == ']') + { + List items = Split(json); + var finalList = new List(items.Count); + for (int i = 0; i < items.Count; i++) + finalList.Add(ParseAnonymousValue(items[i])); + return finalList; + } + if (json[0] == '"' && json[json.Length - 1] == '"') + { + string str = json.Substring(1, json.Length - 2); + return str.Replace("\\", string.Empty); + } + if (char.IsDigit(json[0]) || json[0] == '-') + { + if (json.Contains(".")) + { + double result; + double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); + return result; + } + else + { + int result; + int.TryParse(json, out result); + return result; + } + } + if (json == "true") + return true; + if (json == "false") + return false; + // handles json == "null" as well as invalid JSON + return null; + } + + static Dictionary CreateMemberNameDictionary(T[] members) where T : MemberInfo + { + Dictionary nameToMember = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < members.Length; i++) + { + T member = members[i]; + if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) + continue; + + string name = member.Name; + if (member.IsDefined(typeof(DataMemberAttribute), true)) + { + DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); + if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) + name = dataMemberAttribute.Name; + } + + nameToMember.Add(name, member); + } + + return nameToMember; + } + + static object ParseObject(Type type, string json) + { + object instance = FormatterServices.GetUninitializedObject(type); + + //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON + List elems = Split(json); + if (elems.Count % 2 != 0) + return instance; + + Dictionary nameToField; + Dictionary nameToProperty; + if (!fieldInfoCache.TryGetValue(type, out nameToField)) + { + nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + fieldInfoCache.Add(type, nameToField); + } + if (!propertyInfoCache.TryGetValue(type, out nameToProperty)) + { + nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + propertyInfoCache.Add(type, nameToProperty); + } + + for (int i = 0; i < elems.Count; i += 2) + { + if (elems[i].Length <= 2) + continue; + string key = elems[i].Substring(1, elems[i].Length - 2); + string value = elems[i + 1]; + + FieldInfo fieldInfo; + PropertyInfo propertyInfo; + if (nameToField.TryGetValue(key, out fieldInfo)) + fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); + else if (nameToProperty.TryGetValue(key, out propertyInfo)) + propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); + } + + return instance; + } + } +} diff --git a/WebMap/MapDataServer.cs b/WebMap/MapDataServer.cs index 3dbc3a4..4208ddb 100644 --- a/WebMap/MapDataServer.cs +++ b/WebMap/MapDataServer.cs @@ -2,7 +2,6 @@ using System.Text; using System.Collections.Generic; using System.IO; -using System.Threading; using System.Reflection; using WebSocketSharp.Net; @@ -27,10 +26,6 @@ public WebSocketHandler() {} } public class MapDataServer { - static int SERVER_PORT = 3000; - static double PLAYER_UPDATE_RATE = 0.5; - static bool CACHE_FILES = false; - private HttpServer httpServer; private string publicRoot; private Dictionary fileCache; @@ -50,7 +45,7 @@ public class MapDataServer { }; public MapDataServer() { - httpServer = new HttpServer(SERVER_PORT); + httpServer = new HttpServer(WebMapConfig.SERVER_PORT); httpServer.AddWebSocketService("/"); httpServer.KeepClean = true; @@ -70,7 +65,7 @@ public MapDataServer() { if (dataString.Length > 0) { webSocketHandler.Sessions.Broadcast("players\n" + dataString); } - }, null, TimeSpan.Zero, TimeSpan.FromSeconds(PLAYER_UPDATE_RATE)); + }, null, TimeSpan.Zero, TimeSpan.FromSeconds(WebMapConfig.PLAYER_UPDATE_INTERVAL)); publicRoot = Path.Combine(System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "web"); @@ -115,7 +110,7 @@ private void ServeStaticFiles(HttpRequestEventArgs e) { var filePath = Path.Combine(publicRoot, requestedFile); try { requestedFileBytes = File.ReadAllBytes(filePath); - if (CACHE_FILES) { + if (WebMapConfig.CACHE_SERVER_FILES) { fileCache.Add(requestedFile, requestedFileBytes); } } catch (Exception ex) { @@ -143,8 +138,17 @@ private bool ProcessSpecialRoutes(HttpRequestEventArgs e) { var req = e.Request; var res = e.Response; var rawRequestPath = req.RawUrl; + byte[] textBytes; switch(rawRequestPath) { + case "/config": + res.Headers.Add(HttpResponseHeader.CacheControl, "no-cache"); + res.ContentType = "application/json"; + res.StatusCode = 200; + textBytes = Encoding.UTF8.GetBytes(WebMapConfig.makeClientConfigJSON()); + res.ContentLength64 = textBytes.Length; + res.Close(textBytes, true); + return true; case "/map": // Doing things this way to make the full map harder to accidentally see. res.Headers.Add(HttpResponseHeader.CacheControl, "public, max-age=604800, immutable"); @@ -166,7 +170,7 @@ private bool ProcessSpecialRoutes(HttpRequestEventArgs e) { res.ContentType = "text/csv"; res.StatusCode = 200; var text = String.Join("\n", pins); - var textBytes = Encoding.UTF8.GetBytes(text); + textBytes = Encoding.UTF8.GetBytes(text); res.ContentLength64 = textBytes.Length; res.Close(textBytes, true); return true; @@ -178,7 +182,7 @@ public void ListenAsync() { httpServer.Start(); if (httpServer.IsListening) { - Debug.Log($"~~~ HTTP Server Listening on port {SERVER_PORT} ~~~"); + Debug.Log($"~~~ HTTP Server Listening on port {WebMapConfig.SERVER_PORT} ~~~"); } else { Debug.Log("!!! HTTP Server Failed To Start !!!"); } diff --git a/WebMap/WebMap.cs b/WebMap/WebMap.cs index 537e25a..1290e2a 100644 --- a/WebMap/WebMap.cs +++ b/WebMap/WebMap.cs @@ -18,12 +18,6 @@ namespace WebMap { //This is the main declaration of our plugin class. BepInEx searches for all classes inheriting from BaseUnityPlugin to initialize on startup. //BaseUnityPlugin itself inherits from MonoBehaviour, so you can use this as a reference for what you can declare and use in your plugin class: https://docs.unity3d.com/ScriptReference/MonoBehaviour.html public class WebMap : BaseUnityPlugin { - static readonly int TEXTURE_SIZE = 2048; - static readonly int PIXEL_SIZE = 12; - static readonly float EXPLORE_RADIUS = 100f; - static readonly float UPDATE_FOG_TEXTURE_INTERVAL = 1f; - static readonly float SAVE_FOG_TEXTURE_INTERVAL = 30f; - static readonly int MAX_PINS_PER_USER = 50; static readonly DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); static readonly HashSet ALLOWED_PINS = new HashSet { "dot", "fire", "mine", "house", "cave" }; @@ -38,6 +32,9 @@ public void Awake() { var harmony = new Harmony("com.kylepaulsen.valheim.webmap"); Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), (string) null); + var pluginPath = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + WebMapConfig.readConfigFile(Path.Combine(pluginPath, "config.json")); + string[] arguments = Environment.GetCommandLineArgs(); var worldName = ""; for (var t = 0; t < arguments.Length; t++) { @@ -47,8 +44,6 @@ public void Awake() { } } - var pluginPath = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - var mapDataPath = Path.Combine(pluginPath, "map_data"); Directory.CreateDirectory(mapDataPath); worldDataPath = Path.Combine(mapDataPath, worldName); @@ -66,14 +61,14 @@ public void Awake() { var fogImagePath = Path.Combine(worldDataPath, "fog.png"); try { - var fogTexture = new Texture2D(TEXTURE_SIZE, TEXTURE_SIZE); + var fogTexture = new Texture2D(WebMapConfig.TEXTURE_SIZE, WebMapConfig.TEXTURE_SIZE); var fogBytes = File.ReadAllBytes(fogImagePath); fogTexture.LoadImage(fogBytes); mapDataServer.fogTexture = fogTexture; } catch (Exception e) { Debug.Log("Failed to read fog image data from disk... Making new fog image..." + e.Message); - var fogTexture = new Texture2D(TEXTURE_SIZE, TEXTURE_SIZE, TextureFormat.RGB24, false); - var fogColors = new Color32[TEXTURE_SIZE * TEXTURE_SIZE]; + var fogTexture = new Texture2D(WebMapConfig.TEXTURE_SIZE, WebMapConfig.TEXTURE_SIZE, TextureFormat.RGB24, false); + var fogColors = new Color32[WebMapConfig.TEXTURE_SIZE * WebMapConfig.TEXTURE_SIZE]; for (var t = 0; t < fogColors.Length; t++) { fogColors[t] = Color.black; } @@ -88,8 +83,8 @@ public void Awake() { } } - InvokeRepeating("UpdateFogTexture", UPDATE_FOG_TEXTURE_INTERVAL, UPDATE_FOG_TEXTURE_INTERVAL); - InvokeRepeating("SaveFogTexture", SAVE_FOG_TEXTURE_INTERVAL, SAVE_FOG_TEXTURE_INTERVAL); + InvokeRepeating("UpdateFogTexture", WebMapConfig.UPDATE_FOG_TEXTURE_INTERVAL, WebMapConfig.UPDATE_FOG_TEXTURE_INTERVAL); + InvokeRepeating("SaveFogTexture", WebMapConfig.SAVE_FOG_TEXTURE_INTERVAL, WebMapConfig.SAVE_FOG_TEXTURE_INTERVAL); var mapPinsFile = Path.Combine(worldDataPath, "pins.csv"); try { @@ -101,9 +96,9 @@ public void Awake() { } public void UpdateFogTexture() { - int pixelExploreRadius = (int)Mathf.Ceil(EXPLORE_RADIUS / PIXEL_SIZE); + int pixelExploreRadius = (int)Mathf.Ceil(WebMapConfig.EXPLORE_RADIUS / WebMapConfig.PIXEL_SIZE); int pixelExploreRadiusSquared = pixelExploreRadius * pixelExploreRadius; - var halfTextureSize = TEXTURE_SIZE / 2; + var halfTextureSize = WebMapConfig.TEXTURE_SIZE / 2; mapDataServer.players.ForEach(player => { if (player.m_publicRefPos) { @@ -113,11 +108,11 @@ public void UpdateFogTexture() { } catch {} if (zdoData != null) { var pos = zdoData.GetPosition(); - var pixelX = Mathf.RoundToInt(pos.x / PIXEL_SIZE + halfTextureSize); - var pixelY = Mathf.RoundToInt(pos.z / PIXEL_SIZE + halfTextureSize); + var pixelX = Mathf.RoundToInt(pos.x / WebMapConfig.PIXEL_SIZE + halfTextureSize); + var pixelY = Mathf.RoundToInt(pos.z / WebMapConfig.PIXEL_SIZE + halfTextureSize); for (var y = pixelY - pixelExploreRadius; y <= pixelY + pixelExploreRadius; y++) { for (var x = pixelX - pixelExploreRadius; x <= pixelX + pixelExploreRadius; x++) { - if (y >= 0 && x >= 0 && y < TEXTURE_SIZE && x < TEXTURE_SIZE) { + if (y >= 0 && x >= 0 && y < WebMapConfig.TEXTURE_SIZE && x < WebMapConfig.TEXTURE_SIZE) { var xDiff = pixelX - x; var yDiff = pixelY - y; var currentExploreRadiusSquared = xDiff * xDiff + yDiff * yDiff; @@ -245,20 +240,20 @@ static void Postfix(ZoneSystem __instance) { } Debug.Log("BUILD MAP!"); - int num = TEXTURE_SIZE / 2; - float num2 = PIXEL_SIZE / 2f; - Color32[] colorArray = new Color32[TEXTURE_SIZE * TEXTURE_SIZE]; - Color32[] treeMaskArray = new Color32[TEXTURE_SIZE * TEXTURE_SIZE]; - float[] heightArray = new float[TEXTURE_SIZE * TEXTURE_SIZE]; - for (int i = 0; i < TEXTURE_SIZE; i++) { - for (int j = 0; j < TEXTURE_SIZE; j++) { - float wx = (float)(j - num) * PIXEL_SIZE + num2; - float wy = (float)(i - num) * PIXEL_SIZE + num2; + int num = WebMapConfig.TEXTURE_SIZE / 2; + float num2 = WebMapConfig.PIXEL_SIZE / 2f; + Color32[] colorArray = new Color32[WebMapConfig.TEXTURE_SIZE * WebMapConfig.TEXTURE_SIZE]; + Color32[] treeMaskArray = new Color32[WebMapConfig.TEXTURE_SIZE * WebMapConfig.TEXTURE_SIZE]; + float[] heightArray = new float[WebMapConfig.TEXTURE_SIZE * WebMapConfig.TEXTURE_SIZE]; + for (int i = 0; i < WebMapConfig.TEXTURE_SIZE; i++) { + for (int j = 0; j < WebMapConfig.TEXTURE_SIZE; j++) { + float wx = (float)(j - num) * WebMapConfig.PIXEL_SIZE + num2; + float wy = (float)(i - num) * WebMapConfig.PIXEL_SIZE + num2; Heightmap.Biome biome = WorldGenerator.instance.GetBiome(wx, wy); float biomeHeight = WorldGenerator.instance.GetBiomeHeight(biome, wx, wy); - colorArray[i * TEXTURE_SIZE + j] = GetPixelColor(biome); - treeMaskArray[i * TEXTURE_SIZE + j] = GetMaskColor(wx, wy, biomeHeight, biome); - heightArray[i * TEXTURE_SIZE + j] = biomeHeight; + colorArray[i * WebMapConfig.TEXTURE_SIZE + j] = GetPixelColor(biome); + treeMaskArray[i * WebMapConfig.TEXTURE_SIZE + j] = GetMaskColor(wx, wy, biomeHeight, biome); + heightArray[i * WebMapConfig.TEXTURE_SIZE + j] = biomeHeight; } } @@ -269,11 +264,11 @@ static void Postfix(ZoneSystem __instance) { for (var t = 0; t < colorArray.Length; t++) { var h = heightArray[t]; - var tUp = t - TEXTURE_SIZE; + var tUp = t - WebMapConfig.TEXTURE_SIZE; if (tUp < 0) { tUp = t; } - var tDown = t + TEXTURE_SIZE; + var tDown = t + WebMapConfig.TEXTURE_SIZE; if (tDown > colorArray.Length - 1) { tDown = t; } @@ -308,7 +303,7 @@ static void Postfix(ZoneSystem __instance) { newColors[t] = new Color(ans.r * surfaceLight, ans.g * surfaceLight, ans.b * surfaceLight, ans.a); } - var newTexture = new Texture2D(TEXTURE_SIZE, TEXTURE_SIZE, TextureFormat.RGBA32, false); + var newTexture = new Texture2D(WebMapConfig.TEXTURE_SIZE, WebMapConfig.TEXTURE_SIZE, TextureFormat.RGBA32, false); newTexture.SetPixels(newColors); byte[] pngBytes = newTexture.EncodeToPNG(); @@ -373,7 +368,7 @@ static void Prefix(RoutedRPCData data) { mapDataServer.AddPin(steamid, pinId, pinType, userName, pos, safePinsText); var usersPins = mapDataServer.pins.FindAll(pin => pin.StartsWith(steamid)); - var numOverflowPins = usersPins.Count - MAX_PINS_PER_USER; + var numOverflowPins = usersPins.Count - WebMapConfig.MAX_PINS_PER_USER; for (var t = numOverflowPins; t > 0; t--) { var pinIdx = mapDataServer.pins.FindIndex(pin => pin.StartsWith(steamid)); mapDataServer.RemovePin(pinIdx); diff --git a/WebMap/WebMap.csproj b/WebMap/WebMap.csproj index 07874f1..2b0f287 100644 --- a/WebMap/WebMap.csproj +++ b/WebMap/WebMap.csproj @@ -26,6 +26,7 @@ + ..\libs\0Harmony.dll @@ -60,6 +61,8 @@ + + diff --git a/WebMap/config.json b/WebMap/config.json new file mode 100644 index 0000000..c27d219 --- /dev/null +++ b/WebMap/config.json @@ -0,0 +1,30 @@ +{ + "// lines that look like this are comments and dont do anything.": 0, + + "// http server listens on below port. Default: 3000": 0, + "server_port": 3000, + + "// A larger explore_radius reveals the map more quickly. Default: 100": 0, + "explore_radius": 100, + + "// How many pins each client is allowed to make before old ones start being deleted. Default: 50": 0, + "max_pins_per_user": 50, + + "// How often do we send position data to web browsers in seconds. Default: 0.5": 0, + "player_update_interval": 0.5, + + "// How often do we update the fog texture on the server in seconds. Default: 1": 0, + "update_fog_texture_interval": 1, + + "// How often do we save the fog texture in seconds. Default: 30": 0, + "save_fog_texture_interval": 30, + + "// Should the server cache web files to be more performant? Default: true": 0, + "cache_server_files": false, + + "// How large is the map texture? Probably dont change this. Default: 2048": 0, + "texture_size": 2048, + + "// How many in game units does a map pixel represent? Probably dont change this. Default: 12": 0, + "pixel_size": 12 +} diff --git a/WebMap/web-src/index.js b/WebMap/web-src/index.js index bb3a85c..49a16a7 100644 --- a/WebMap/web-src/index.js +++ b/WebMap/web-src/index.js @@ -1,3 +1,4 @@ +import constants from "./constants"; import websocket from "./websocket"; import map from "./map"; import players from "./players"; @@ -17,13 +18,21 @@ const fetchFog = () => new Promise((res) => { fogImage.src = 'fog'; }); +const fetchConfig = fetch('/config').then(res => res.json()).then(config => { + constants.CANVAS_WIDTH = config.texture_size || 2048; + constants.CANVAS_HEIGHT = config.texture_size || 2048; + constants.PIXEL_SIZE = config.pixel_size || 12; + constants.EXPLORE_RADIUS = config.explore_radius || 100; +}); + const setup = async () => { websocket.init(); players.init(); await Promise.all([ fetchMap(), - fetchFog() + fetchFog(), + fetchConfig ]); map.init({