Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimWebMap v1.0.2
plugins/ValheimWebMap/ValheimWebMap.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")] [assembly: AssemblyCompany("ValheimWebMap")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.2.0")] [assembly: AssemblyInformationalVersion("1.0.2+c5abb89911bd19e841a4b489d21810950a4dfb08")] [assembly: AssemblyProduct("ValheimWebMap")] [assembly: AssemblyTitle("ValheimWebMap")] [assembly: AssemblyVersion("1.0.2.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimWebMap { internal sealed class ExploredMask { public const int Size = 2048; private const int RegionGrid = 128; private const string FileMagic = "VWME"; private const int FileVersion = 1; private readonly float _half; private readonly float _cell; private readonly float _regionCell; private readonly byte[] _cells = new byte[4194304]; private readonly int[] _regionVersion = new int[16384]; private int _version = 1; private int _exploredCount; public int Version => Volatile.Read(in _version); public bool Dirty { get; private set; } public float CellSize => _cell; public float ExploredPercent { get { double num = Math.PI * Math.Pow(10000f / _cell, 2.0); return Math.Min(100f, (float)(100.0 * (double)Volatile.Read(in _exploredCount) / num)); } } public ExploredMask(float halfSize) { _half = halfSize; _cell = halfSize * 2f / 2048f; _regionCell = halfSize * 2f / 128f; } public int Reveal(float wx, float wz, float radius) { int num = (int)Math.Ceiling(radius / _cell); float num2 = radius * radius; int num3 = (int)Math.Floor((wx + _half) / _cell); int num4 = (int)Math.Floor((wz + _half) / _cell); int num5 = 0; for (int i = Math.Max(num4 - num, 0); i <= Math.Min(num4 + num, 2047); i++) { float num6 = 0f - _half + ((float)i + 0.5f) * _cell - wz; for (int j = Math.Max(num3 - num, 0); j <= Math.Min(num3 + num, 2047); j++) { float num7 = 0f - _half + ((float)j + 0.5f) * _cell - wx; if (!(num7 * num7 + num6 * num6 > num2)) { num5 += Set(j, i); } } } if (num5 > 0) { Touch(wx - radius, wz - radius, wx + radius, wz + radius); } return num5; } public int RevealRect(float minX, float minZ, float maxX, float maxZ) { int num = Clamp((int)Math.Floor((minX + _half) / _cell)); int num2 = Clamp((int)Math.Ceiling((maxX + _half) / _cell) - 1); int num3 = Clamp((int)Math.Floor((minZ + _half) / _cell)); int num4 = Clamp((int)Math.Ceiling((maxZ + _half) / _cell) - 1); int num5 = 0; for (int i = num3; i <= num4; i++) { for (int j = num; j <= num2; j++) { num5 += Set(j, i); } } if (num5 > 0) { Touch(minX, minZ, maxX, maxZ); } return num5; } private int Set(int i, int j) { int num = j * 2048 + i; if (_cells[num] != 0) { return 0; } _cells[num] = byte.MaxValue; _exploredCount++; return 1; } private void Touch(float minX, float minZ, float maxX, float maxZ) { int num = Interlocked.Increment(ref _version); Dirty = true; int num2 = ClampRegion((int)Math.Floor((minX + _half) / _regionCell)); int num3 = ClampRegion((int)Math.Floor((maxX + _half) / _regionCell)); int num4 = ClampRegion((int)Math.Floor((minZ + _half) / _regionCell)); int num5 = ClampRegion((int)Math.Floor((maxZ + _half) / _regionCell)); for (int i = num4; i <= num5; i++) { for (int j = num2; j <= num3; j++) { _regionVersion[i * 128 + j] = num; } } } public int RegionVersion(float minX, float minZ, float maxX, float maxZ) { int num = ClampRegion((int)Math.Floor((minX + _half) / _regionCell)); int num2 = ClampRegion((int)Math.Floor((maxX + _half) / _regionCell)); int num3 = ClampRegion((int)Math.Floor((minZ + _half) / _regionCell)); int num4 = ClampRegion((int)Math.Floor((maxZ + _half) / _regionCell)); int num5 = 0; for (int i = num3; i <= num4; i++) { for (int j = num; j <= num2; j++) { num5 = Math.Max(num5, Volatile.Read(in _regionVersion[i * 128 + j])); } } return num5; } public float Sample(float wx, float wz) { float num = _cell * 0.5f; return (SampleBilinear(wx - num, wz - num) + SampleBilinear(wx + num, wz - num) + SampleBilinear(wx - num, wz + num) + SampleBilinear(wx + num, wz + num)) * 0.25f; } private float SampleBilinear(float wx, float wz) { float num = (wx + _half) / _cell - 0.5f; float num2 = (wz + _half) / _cell - 0.5f; int num3 = (int)Math.Floor(num); int num4 = (int)Math.Floor(num2); float num5 = num - (float)num3; float num6 = num2 - (float)num4; int num7 = Clamp(num3 + 1); int num8 = Clamp(num4 + 1); num3 = Clamp(num3); num4 = Clamp(num4); float num9 = (float)(int)_cells[num4 * 2048 + num3] * (1f - num5) + (float)(int)_cells[num4 * 2048 + num7] * num5; float num10 = (float)(int)_cells[num8 * 2048 + num3] * (1f - num5) + (float)(int)_cells[num8 * 2048 + num7] * num5; return (num9 * (1f - num6) + num10 * num6) / 255f; } public bool AnyExplored(float minX, float minZ, float maxX, float maxZ) { int num = Clamp((int)Math.Floor((minX + _half) / _cell)); int num2 = Clamp((int)Math.Ceiling((maxX + _half) / _cell)); int num3 = Clamp((int)Math.Floor((minZ + _half) / _cell)); int num4 = Clamp((int)Math.Ceiling((maxZ + _half) / _cell)); for (int i = num3; i <= num4; i++) { int num5 = i * 2048; for (int j = num; j <= num2; j++) { if (_cells[num5 + j] != 0) { return true; } } } return false; } private static int Clamp(int v) { if (v >= 0) { if (v < 2048) { return v; } return 2047; } return 0; } private static int ClampRegion(int v) { if (v >= 0) { if (v < 128) { return v; } return 127; } return 0; } public void Save(string path) { string text = path + ".tmp"; Directory.CreateDirectory(Path.GetDirectoryName(path)); using (FileStream output = new FileStream(text, FileMode.Create, FileAccess.Write)) { using BinaryWriter binaryWriter = new BinaryWriter(output); binaryWriter.Write("VWME"); binaryWriter.Write(1); binaryWriter.Write(2048); binaryWriter.Write(_exploredCount); byte[] array = PngEncoder.Zlib(_cells); binaryWriter.Write(array.Length); binaryWriter.Write(array); } if (File.Exists(path)) { File.Delete(path); } File.Move(text, path); Dirty = false; } public bool TryLoad(string path) { if (!File.Exists(path)) { return false; } using (FileStream input = new FileStream(path, FileMode.Open, FileAccess.Read)) { using BinaryReader binaryReader = new BinaryReader(input); if (binaryReader.ReadString() != "VWME") { throw new InvalidDataException("not an explored-mask file"); } if (binaryReader.ReadInt32() != 1) { throw new InvalidDataException("unsupported explored-mask version"); } if (binaryReader.ReadInt32() != 2048) { throw new InvalidDataException("explored-mask size mismatch"); } int exploredCount = binaryReader.ReadInt32(); int count = binaryReader.ReadInt32(); byte[] array = PngEncoder.Unzlib(binaryReader.ReadBytes(count), 4194304); Buffer.BlockCopy(array, 0, _cells, 0, array.Length); _exploredCount = exploredCount; } Touch(0f - _half, 0f - _half, _half, _half); Dirty = false; return true; } } internal sealed class JsonWriter { private readonly StringBuilder _sb; private bool _needsComma; public JsonWriter(int capacity = 256) { _sb = new StringBuilder(capacity); } public JsonWriter BeginObject() { Separator(); _sb.Append('{'); _needsComma = false; return this; } public JsonWriter EndObject() { _sb.Append('}'); _needsComma = true; return this; } public JsonWriter BeginArray() { Separator(); _sb.Append('['); _needsComma = false; return this; } public JsonWriter EndArray() { _sb.Append(']'); _needsComma = true; return this; } public JsonWriter Key(string name) { Separator(); WriteString(name); _sb.Append(':'); _needsComma = false; return this; } public JsonWriter Value(string value) { Separator(); if (value == null) { _sb.Append("null"); } else { WriteString(value); } _needsComma = true; return this; } public JsonWriter Value(bool value) { Separator(); _sb.Append(value ? "true" : "false"); _needsComma = true; return this; } public JsonWriter Value(long value) { Separator(); _sb.Append(value.ToString(CultureInfo.InvariantCulture)); _needsComma = true; return this; } public JsonWriter Value(double value, int decimals) { Separator(); if (double.IsNaN(value) || double.IsInfinity(value)) { value = 0.0; } _sb.Append(value.ToString("F" + decimals, CultureInfo.InvariantCulture)); _needsComma = true; return this; } public JsonWriter Prop(string key, string value) { return Key(key).Value(value); } public JsonWriter Prop(string key, bool value) { return Key(key).Value(value); } public JsonWriter Prop(string key, long value) { return Key(key).Value(value); } public JsonWriter Prop(string key, double value, int decimals) { return Key(key).Value(value, decimals); } private void Separator() { if (_needsComma) { _sb.Append(','); } } private void WriteString(string s) { _sb.Append('"'); foreach (char c in s) { switch (c) { case '"': _sb.Append("\\\""); continue; case '\\': _sb.Append("\\\\"); continue; case '\n': _sb.Append("\\n"); continue; case '\r': _sb.Append("\\r"); continue; case '\t': _sb.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder = _sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4")); } else { _sb.Append(c); } } _sb.Append('"'); } public override string ToString() { return _sb.ToString(); } } internal sealed class MapAtlas { public const int TileSize = 256; public int Id { get; } public int Size { get; } public float HalfSize { get; } public int NativeZoom { get; } public byte[][] Levels { get; } public MapAtlas(int id, int size, float halfSize, byte[] level0) { if (size < 256 || (size & (size - 1)) != 0) { throw new ArgumentException("Atlas size must be a power of two and at least " + 256, "size"); } if (level0.Length != size * size * 3) { throw new ArgumentException("Pixel buffer does not match size", "level0"); } Id = id; Size = size; HalfSize = halfSize; NativeZoom = Log2(size / 256); Levels = new byte[NativeZoom + 1][]; Levels[0] = level0; for (int i = 1; i <= NativeZoom; i++) { Levels[i] = Downsample(Levels[i - 1], size >> i - 1); } } public int LevelSize(int level) { return Size >> level; } public static int Log2(int v) { int num = 0; while (v > 1) { v >>= 1; num++; } return num; } private static byte[] Downsample(byte[] src, int srcSize) { int num = srcSize / 2; byte[] array = new byte[num * num * 3]; for (int i = 0; i < num; i++) { int num2 = i * 2 * srcSize * 3; int num3 = num2 + srcSize * 3; int num4 = i * num * 3; for (int j = 0; j < num; j++) { int num5 = j * 6; for (int k = 0; k < 3; k++) { int num6 = src[num2 + num5 + k] + src[num2 + num5 + 3 + k] + src[num3 + num5 + k] + src[num3 + num5 + 3 + k]; array[num4 + j * 3 + k] = (byte)(num6 + 2 >> 2); } } } return array; } } internal static class MapRenderer { public const int FormatVersion = 1; private const float WaterLevel = 30f; private const float WaterEdgeSqr = 110250000f; private static readonly Vector3 SunDirection; private static readonly float FlatShade; private const string CacheMagic = "VWMB"; public static byte[] Render(int size, float halfSize, int threads, Action<float> progress, Func<bool> cancelled) { WorldGenerator wg = WorldGenerator.instance; if (wg == null) { throw new InvalidOperationException("WorldGenerator is not initialised"); } int num = size * size; float[] height = new float[num]; byte[] biome = new byte[num]; byte[] forest = new byte[num]; byte[] lava = new byte[num]; float mpp = halfSize * 2f / (float)size; RunRows(size, threads, cancelled, delegate(int row) { SampleRow(wg, row, size, halfSize, mpp, height, biome, forest, lava); }, delegate(float done) { progress(done * 0.92f); }); if (cancelled()) { return null; } byte[] rgb = new byte[num * 3]; RunRows(size, threads, cancelled, delegate(int row) { ComposeRow(row, size, mpp, height, biome, forest, lava, rgb); }, delegate(float done) { progress(0.92f + done * 0.08f); }); if (cancelled()) { return null; } progress(1f); return rgb; } private static void RunRows(int rows, int threads, Func<bool> cancelled, Action<int> work, Action<float> progress) { threads = Math.Max(1, threads); int nextRow = -1; int done = 0; Exception failure = null; Thread[] array = new Thread[threads]; for (int i = 0; i < threads; i++) { array[i] = new Thread((ThreadStart)delegate { try { while (true) { int num2 = Interlocked.Increment(ref nextRow); if (num2 >= rows || cancelled() || Volatile.Read(in failure) != null) { break; } work(num2); int num3 = Interlocked.Increment(ref done); if ((num3 & 0xF) == 0) { progress((float)num3 / (float)rows); } } } catch (Exception value) { Interlocked.CompareExchange(ref failure, value, null); } }) { IsBackground = true, Name = "WebMap render " + i, Priority = ThreadPriority.BelowNormal }; array[i].Start(); } Thread[] array2 = array; for (int num = 0; num < array2.Length; num++) { array2[num].Join(); } if (failure != null) { throw new Exception("Map render failed: " + failure.Message, failure); } } private static void SampleRow(WorldGenerator wg, int y, int size, float halfSize, float mpp, float[] height, byte[] biome, byte[] forest, byte[] lava) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Invalid comparison between Unknown and I4 //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Invalid comparison between Unknown and I4 //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Invalid comparison between Unknown and I4 //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Invalid comparison between Unknown and I4 //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Invalid comparison between Unknown and I4 //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Invalid comparison between Unknown and I4 //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Unknown result type (might be due to invalid IL or missing references) float num = halfSize - ((float)y + 0.5f) * mpp; int num2 = y * size; Color val = default(Color); for (int i = 0; i < size; i++) { float num3 = 0f - halfSize + ((float)i + 0.5f) * mpp; int num4 = num2 + i; if (num3 * num3 + num * num > 110250000f) { height[num4] = -400f; biome[num4] = 8; continue; } Biome biome2 = wg.GetBiome(num3, num, 0.02f, false); float num5 = (height[num4] = wg.GetBiomeHeight(biome2, num3, num, ref val, false, true)); biome[num4] = BiomeId.From(biome2); if (num5 < 30f) { continue; } float num6 = 0f; if ((int)biome2 <= 8) { if ((int)biome2 != 1) { if ((int)biome2 == 8) { num6 = 0.4f + 0.6f * Mathf.Clamp01((1.2f - WorldGenerator.GetForestFactor(new Vector3(num3, 0f, num))) / 0.5f); } } else { num6 = Mathf.Clamp01((1.15f - WorldGenerator.GetForestFactor(new Vector3(num3, 0f, num))) / 0.25f); } } else if ((int)biome2 != 16) { if ((int)biome2 != 32) { if ((int)biome2 == 512) { num6 = 1f - Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(1.1f, 1.3f, WorldGenerator.GetForestFactor(new Vector3(num3, 0f, num)))); } } else { lava[num4] = (byte)(Mathf.Clamp01(val.a) * 255f); } } else { num6 = Mathf.Clamp01((0.8f - WorldGenerator.GetForestFactor(new Vector3(num3, 0f, num))) / 0.2f); } forest[num4] = (byte)(num6 * 255f); } } private static void ComposeRow(int y, int size, float mpp, float[] height, byte[] biome, byte[] forest, byte[] lava, byte[] rgb) { int num = y * size; int num2 = Math.Max(y - 1, 0) * size; int num3 = Math.Min(y + 1, size - 1) * size; float num4 = 1f / (2f * mpp); for (int i = 0; i < size; i++) { int num5 = num + i; float num6 = height[num5]; byte b = biome[num5]; float t = (float)(int)forest[num5] / 255f; int num7 = Math.Max(i - 1, 0); int num8 = Math.Min(i + 1, size - 1); float num9 = (height[num + num8] - height[num + num7]) * num4; float num10 = (height[num2 + i] - height[num3 + i]) * num4; float num11 = Mathf.Sqrt(num9 * num9 + num10 * num10); Rgb rgb2 = b switch { 1 => Rgb.Lerp(Palette.Meadows, Palette.MeadowsForest, t), 4 => Rgb.Lerp(Palette.BlackForest, Palette.BlackForestDense, t), 2 => Palette.Swamp, 3 => Rgb.Lerp(Palette.MountainSnow, Palette.MountainRock, Mathf.Clamp01((num11 - 0.7f) / 0.9f)), 5 => Rgb.Lerp(Palette.Plains, Palette.PlainsForest, t), 9 => Rgb.Lerp(Palette.Mistlands, Palette.MistlandsForest, t), 6 => Rgb.Lerp(Palette.Ashlands, Palette.Lava, Mathf.Sqrt((float)(int)lava[num5] / 255f)), 7 => Rgb.Lerp(Palette.DeepNorth, Palette.MountainRock, Mathf.Clamp01((num11 - 0.9f) / 1f) * 0.6f), 8 => Palette.WaterDeep, _ => Palette.Unknown, }; if (num6 < 30f) { float num12 = 30f - num6; float t2 = Mathf.Sqrt(Mathf.Clamp01(num12 / 50f)); rgb2 = Rgb.Lerp(Rgb.Lerp(Palette.WaterShallow, Palette.WaterDeep, t2), t: Mathf.Clamp01(1f - num12 / 4f) * 0.5f, b: rgb2); num9 *= 0.3f; num10 *= 0.3f; } else if (num6 < 31.5f && (b == 1 || b == 4 || b == 5 || b == 9)) { rgb2 = Rgb.Lerp(rgb2, Palette.Sand, Mathf.Clamp01((31.5f - num6) / 1.5f) * 0.7f); } float num13 = 0f - num9; float num14 = 1f; float num15 = 0f - num10; float num16 = 1f / Mathf.Sqrt(num13 * num13 + num14 * num14 + num15 * num15); float num17 = (num13 * SunDirection.x + num14 * SunDirection.y + num15 * SunDirection.z) * num16; float f = (0.5f + 0.6f * Mathf.Clamp01(num17)) / FlatShade; rgb2 = rgb2.Scale(f); int num18 = num5 * 3; rgb[num18] = rgb2.R; rgb[num18 + 1] = rgb2.G; rgb[num18 + 2] = rgb2.B; } } public static void SaveCache(string path, byte[] rgb, int size, World world) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) string text = path + ".tmp"; Directory.CreateDirectory(Path.GetDirectoryName(path)); using (FileStream output = new FileStream(text, FileMode.Create, FileAccess.Write)) { using BinaryWriter binaryWriter = new BinaryWriter(output); binaryWriter.Write("VWMB"); binaryWriter.Write(1); binaryWriter.Write(size); binaryWriter.Write(world.m_seed); binaryWriter.Write(world.m_worldGenVersion); binaryWriter.Write(((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString()); byte[] array = PngEncoder.Zlib(rgb); binaryWriter.Write(array.Length); binaryWriter.Write(array); } if (File.Exists(path)) { File.Delete(path); } File.Move(text, path); } public static byte[] TryLoadCache(string path, int size, World world) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) if (!File.Exists(path)) { return null; } try { using FileStream input = new FileStream(path, FileMode.Open, FileAccess.Read); using BinaryReader binaryReader = new BinaryReader(input); if (binaryReader.ReadString() != "VWMB") { return null; } if (binaryReader.ReadInt32() != 1) { return null; } if (binaryReader.ReadInt32() != size) { return null; } if (binaryReader.ReadInt32() != world.m_seed) { return null; } if (binaryReader.ReadInt32() != world.m_worldGenVersion) { return null; } if (binaryReader.ReadString() != ((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString()) { return null; } int count = binaryReader.ReadInt32(); return PngEncoder.Unzlib(binaryReader.ReadBytes(count), size * size * 3); } catch (Exception) { return null; } } static MapRenderer() { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) Vector3 val = new Vector3(-1f, 1.6f, 1f); SunDirection = ((Vector3)(ref val)).normalized; FlatShade = 0.5f + 0.6f * SunDirection.y; } } internal sealed class MapSession : IDisposable { public const float HalfSize = 10240f; private const int ProvisionalResolution = 1024; private static readonly FieldInfo GeneratedZonesField = typeof(ZoneSystem).GetField("m_generatedZones", BindingFlags.Instance | BindingFlags.NonPublic); private readonly PluginConfig _cfg; private readonly ManualLogSource _log; private readonly World _world; private readonly string _dataDir; private readonly string _exploredPath; private readonly ExploredMask _mask; private readonly TileService _tiles; private readonly PlayerHistory _history; private readonly string _epoch = DateTime.UtcNow.Ticks.ToString("x"); private readonly List<PlayerEntry> _players = new List<PlayerEntry>(); private readonly Thread _renderThread; private volatile bool _stop; private volatile string _stateJson; private volatile string _renderError; private float _renderProgress; private int _atlasCounter; private float _sinceUpdate; private float _sinceSave; public string WorldName => _world.m_name; public TileService Tiles => _tiles; public string StateJson => _stateJson; public string HistoryJson { get { if (_history == null || !_cfg.ShowHistory.Value) { return "{\"players\":[]}"; } return _history.Json; } } public MapSession(PluginConfig cfg, ManualLogSource log, World world) { _cfg = cfg; _log = log; _world = world; _dataDir = Path.Combine(string.IsNullOrEmpty(cfg.DataDirectory.Value) ? Path.Combine(Paths.ConfigPath, "ValheimWebMap") : cfg.DataDirectory.Value, SafeFileName(world.m_name)); Directory.CreateDirectory(_dataDir); _exploredPath = Path.Combine(_dataDir, "explored.bin"); _mask = new ExploredMask(10240f); try { if (_mask.TryLoad(_exploredPath)) { _log.LogInfo((object)$"Loaded exploration data ({_mask.ExploredPercent:F1}% of the world explored)"); } } catch (Exception ex) { _log.LogWarning((object)("Could not read " + _exploredPath + ": " + ex.Message + ". Starting with an empty map.")); } _tiles = new TileService(_mask, 10240f, cfg.MaxZoom.Value, _epoch); if (cfg.RevealGeneratedZones.Value) { RevealGeneratedZones(); } if (cfg.TrackSessions.Value) { HistoryOptions options = new HistoryOptions { TrackDeaths = cfg.TrackDeaths.Value, RetentionDays = cfg.HistoryRetentionDays.Value, ShowSessionCount = cfg.ShowSessionCount.Value, ShowPlayTime = cfg.ShowPlayTime.Value, ShowDeaths = cfg.ShowDeathCounts.Value, ShowLastSeen = cfg.ShowLastSeen.Value, RecentSessions = cfg.RecentSessions.Value }; string text = Path.Combine(_dataDir, "history.json"); try { _history = new PlayerHistory(text, options, DateTime.UtcNow); if (_history.PlayerCount > 0) { _log.LogInfo((object)("Loaded play history for " + _history.PlayerCount + " player(s)")); } } catch (Exception ex2) { _log.LogWarning((object)("Could not read " + text + ": " + ex2.Message + ". Starting a new history.")); _history = new PlayerHistory(text + ".new", options, DateTime.UtcNow); } } _stateJson = BuildState(); _renderThread = new Thread(RenderWorker) { IsBackground = true, Name = "WebMap render", Priority = ThreadPriority.BelowNormal }; _renderThread.Start(); } private void RevealGeneratedZones() { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) ZoneSystem instance = ZoneSystem.instance; HashSet<Vector2s> hashSet = (((Object)(object)instance != (Object)null && GeneratedZonesField != null) ? (GeneratedZonesField.GetValue(instance) as HashSet<Vector2s>) : null); if (hashSet == null) { _log.LogWarning((object)"Could not read generated zones from ZoneSystem; skipping reveal of visited zones."); return; } int value = _cfg.RevealGeneratedZonesMargin.Value; float zoneSize = instance.m_zoneSize; int num = 0; int num2 = 0; foreach (Vector2s item in hashSet) { bool flag = true; for (int i = -value; i <= value && flag; i++) { for (int j = -value; j <= value; j++) { if (!hashSet.Contains(new Vector2s(item.x + j, item.y + i))) { flag = false; break; } } } if (flag) { float num3 = (float)item.x * zoneSize; float num4 = (float)item.y * zoneSize; int num5 = _mask.RevealRect(num3 - zoneSize / 2f, num4 - zoneSize / 2f, num3 + zoneSize / 2f, num4 + zoneSize / 2f); if (num5 > 0) { num++; num2 += num5; } } } if (num > 0) { _log.LogInfo((object)$"Revealed {num} previously visited zones ({_mask.ExploredPercent:F1}% of the world explored)"); } } private void RenderWorker() { try { int value = _cfg.Resolution.Value; int num = ((_cfg.RenderThreads.Value > 0) ? _cfg.RenderThreads.Value : Math.Max(1, Environment.ProcessorCount / 2)); string path = Path.Combine(_dataDir, "basemap_" + value + ".bin"); byte[] array = MapRenderer.TryLoadCache(path, value, _world); if (array != null) { _log.LogInfo((object)("Loaded cached world map (" + value + " px)")); PublishAtlas(value, array); _renderProgress = 1f; return; } if (value > 1024) { Stopwatch stopwatch = Stopwatch.StartNew(); byte[] array2 = MapRenderer.Render(1024, 10240f, num, delegate { }, () => _stop); if (array2 == null || _stop) { return; } PublishAtlas(1024, array2); _log.LogInfo((object)$"Rendered preview world map ({1024} px) in {stopwatch.Elapsed.TotalSeconds:F1}s; rendering {value} px map with {num} thread(s)"); } Stopwatch stopwatch2 = Stopwatch.StartNew(); array = MapRenderer.Render(value, 10240f, num, delegate(float p) { _renderProgress = p; }, () => _stop); if (array != null && !_stop) { PublishAtlas(value, array); _renderProgress = 1f; _log.LogInfo((object)$"Rendered world map ({value} px) in {stopwatch2.Elapsed.TotalSeconds:F1}s"); MapRenderer.SaveCache(path, array, value, _world); } } catch (Exception ex) { _renderError = ex.Message; _log.LogError((object)("World map render failed: " + ex)); } } private void PublishAtlas(int size, byte[] rgb) { int id = Interlocked.Increment(ref _atlasCounter); _tiles.Atlas = new MapAtlas(id, size, 10240f, rgb); } public void Tick(float dt) { _sinceUpdate += dt; _sinceSave += dt; if (_sinceUpdate >= _cfg.UpdateInterval.Value) { _sinceUpdate = 0f; UpdatePlayers(); } if (_sinceSave >= _cfg.SaveInterval.Value) { _sinceSave = 0f; SaveIfDirty(); } } private void UpdatePlayers() { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return; } PlayerTracker.Collect(instance, _players); float value = _cfg.ExploreRadius.Value; foreach (PlayerEntry player in _players) { if (player.HasCharacter && !player.Dead) { Vector3 position = player.Position; if (!(((Vector3)(ref position)).sqrMagnitude < 1f)) { _mask.Reveal(player.Position.x, player.Position.z, value); } } } if (_history != null) { EnvMan instance2 = EnvMan.instance; _history.Update(_players, DateTime.UtcNow, ((Object)(object)instance2 != (Object)null) ? instance2.GetDay() : 0); } _stateJson = BuildState(); } private string BuildState() { //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_02b6: Unknown result type (might be due to invalid IL or missing references) //IL_02d0: Unknown result type (might be due to invalid IL or missing references) MapAtlas atlas = _tiles.Atlas; JsonWriter jsonWriter = new JsonWriter(512 + _players.Count * 160); jsonWriter.BeginObject(); jsonWriter.Prop("world", _world.m_name); jsonWriter.Prop("mapReady", atlas != null); jsonWriter.Prop("renderProgress", _renderProgress, 3); jsonWriter.Prop("renderError", _renderError); jsonWriter.Prop("nativeZoom", atlas?.NativeZoom ?? 0); jsonWriter.Prop("epoch", _epoch); jsonWriter.Prop("mapId", atlas?.Id ?? 0); jsonWriter.Prop("exploreVersion", _mask.Version); if (_cfg.ShowExploredPercent.Value) { jsonWriter.Prop("exploredPercent", _mask.ExploredPercent, 2); } EnvMan instance = EnvMan.instance; if ((Object)(object)instance != (Object)null && _cfg.ShowDayAndTime.Value) { jsonWriter.Prop("day", instance.GetDay()); jsonWriter.Prop("timeOfDay", instance.GetDayFraction(), 4); } jsonWriter.Key("players").BeginArray(); foreach (PlayerEntry player in _players) { if (!player.Visible && !_cfg.ShowHiddenPlayers.Value) { continue; } jsonWriter.BeginObject(); jsonWriter.Prop("id", player.Id); jsonWriter.Prop("name", player.Name); jsonWriter.Prop("visible", player.Visible); if (_cfg.ShowDeadStatus.Value) { jsonWriter.Prop("dead", player.Dead); } if (_history != null && _history.TryGetLive(player.Id, out var sessionStart, out var sessionDeaths, out var totalDeaths)) { if (_cfg.ShowTimeOnline.Value) { jsonWriter.Prop("since", PlayerHistory.Iso(sessionStart)); } if (_cfg.ShowDeathCounts.Value && _cfg.TrackDeaths.Value) { jsonWriter.Prop("sessionDeaths", sessionDeaths); jsonWriter.Prop("deaths", totalDeaths); } } if (player.Visible) { jsonWriter.Prop("x", player.Position.x, 1); jsonWriter.Prop("z", player.Position.z, 1); jsonWriter.Prop("y", player.Position.y, 1); if (_cfg.ShowHeading.Value) { jsonWriter.Prop("yaw", player.Yaw, 0); } if (_cfg.ShowBiome.Value) { jsonWriter.Prop("biome", player.Biome); } } jsonWriter.EndObject(); } jsonWriter.EndArray(); jsonWriter.EndObject(); return jsonWriter.ToString(); } public void SaveIfDirty() { if (_mask.Dirty) { try { _mask.Save(_exploredPath); } catch (Exception ex) { _log.LogWarning((object)("Could not save exploration data: " + ex.Message)); } } if (_history != null && _history.Dirty) { try { _history.Save(); } catch (Exception ex2) { _log.LogWarning((object)("Could not save play history: " + ex2.Message)); } } } public void Dispose() { _stop = true; SaveIfDirty(); } private static string SafeFileName(string name) { char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); char[] array = name.ToCharArray(); for (int i = 0; i < array.Length; i++) { if (Array.IndexOf(invalidFileNameChars, array[i]) >= 0) { array[i] = '_'; } } string text = new string(array).Trim(); if (text.Length != 0) { return text; } return "world"; } } internal struct Rgb { public byte R; public byte G; public byte B; public Rgb(int r, int g, int b) { R = (byte)r; G = (byte)g; B = (byte)b; } public static Rgb Lerp(Rgb a, Rgb b, float t) { if (t <= 0f) { return a; } if (t >= 1f) { return b; } return new Rgb((int)((float)(int)a.R + (float)(b.R - a.R) * t + 0.5f), (int)((float)(int)a.G + (float)(b.G - a.G) * t + 0.5f), (int)((float)(int)a.B + (float)(b.B - a.B) * t + 0.5f)); } public Rgb Scale(float f) { return new Rgb(Clamp((float)(int)R * f), Clamp((float)(int)G * f), Clamp((float)(int)B * f)); } private static int Clamp(float v) { if (!(v < 0f)) { if (!(v > 255f)) { return (int)(v + 0.5f); } return 255; } return 0; } } internal static class BiomeId { public const byte None = 0; public const byte Meadows = 1; public const byte Swamp = 2; public const byte Mountain = 3; public const byte BlackForest = 4; public const byte Plains = 5; public const byte AshLands = 6; public const byte DeepNorth = 7; public const byte Ocean = 8; public const byte Mistlands = 9; public static byte From(Biome biome) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Invalid comparison between Unknown and I4 //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected I4, but got Unknown //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Invalid comparison between Unknown and I4 //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Invalid comparison between Unknown and I4 //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Invalid comparison between Unknown and I4 //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Invalid comparison between Unknown and I4 //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 if ((int)biome <= 16) { switch (biome - 1) { default: if ((int)biome != 8) { if ((int)biome != 16) { break; } return 5; } return 4; case 0: return 1; case 1: return 2; case 3: return 3; case 2: break; } } else if ((int)biome <= 64) { if ((int)biome == 32) { return 6; } if ((int)biome == 64) { return 7; } } else { if ((int)biome == 256) { return 8; } if ((int)biome == 512) { return 9; } } return 0; } public static string Name(Biome biome) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Invalid comparison between Unknown and I4 //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected I4, but got Unknown //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Invalid comparison between Unknown and I4 //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Invalid comparison between Unknown and I4 //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Invalid comparison between Unknown and I4 //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Invalid comparison between Unknown and I4 //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 if ((int)biome <= 16) { switch (biome - 1) { default: if ((int)biome != 8) { if ((int)biome != 16) { break; } return "Plains"; } return "Black Forest"; case 0: return "Meadows"; case 1: return "Swamp"; case 3: return "Mountain"; case 2: break; } } else if ((int)biome <= 64) { if ((int)biome == 32) { return "Ashlands"; } if ((int)biome == 64) { return "Deep North"; } } else { if ((int)biome == 256) { return "Ocean"; } if ((int)biome == 512) { return "Mistlands"; } } return ""; } } internal static class Palette { public static readonly Rgb Meadows = new Rgb(126, 165, 92); public static readonly Rgb MeadowsForest = new Rgb(84, 128, 66); public static readonly Rgb BlackForest = new Rgb(60, 92, 56); public static readonly Rgb BlackForestDense = new Rgb(42, 66, 42); public static readonly Rgb Swamp = new Rgb(104, 92, 68); public static readonly Rgb MountainSnow = new Rgb(230, 234, 238); public static readonly Rgb MountainRock = new Rgb(150, 150, 152); public static readonly Rgb Plains = new Rgb(196, 176, 102); public static readonly Rgb PlainsForest = new Rgb(150, 142, 76); public static readonly Rgb Mistlands = new Rgb(92, 86, 102); public static readonly Rgb MistlandsForest = new Rgb(66, 74, 84); public static readonly Rgb Ashlands = new Rgb(112, 56, 46); public static readonly Rgb Lava = new Rgb(236, 110, 36); public static readonly Rgb DeepNorth = new Rgb(214, 226, 238); public static readonly Rgb Sand = new Rgb(204, 192, 150); public static readonly Rgb WaterShallow = new Rgb(62, 128, 164); public static readonly Rgb WaterDeep = new Rgb(22, 50, 92); public static readonly Rgb Fog = new Rgb(18, 20, 26); public static readonly Rgb Unknown = new Rgb(90, 90, 90); } internal sealed class SessionRecord { public string Character; public DateTime Start; public DateTime End; public int Deaths; [JsonIgnore] public double Seconds => Math.Max(0.0, (End - Start).TotalSeconds); } internal sealed class DeathRecord { public DateTime Time; public int Day; public string Character; } internal sealed class PlayerRecord { public string Id; public string Name; public List<SessionRecord> Sessions = new List<SessionRecord>(); public List<DeathRecord> Deaths = new List<DeathRecord>(); [JsonIgnore] public double TotalSeconds { get { double num = 0.0; foreach (SessionRecord session in Sessions) { num += session.Seconds; } return num; } } [JsonIgnore] public DateTime LastSeen { get { if (Sessions.Count <= 0) { return DateTime.MinValue; } return Sessions[Sessions.Count - 1].End; } } } internal sealed class HistoryFile { public int Version = 1; public List<PlayerRecord> Players = new List<PlayerRecord>(); } internal sealed class HistoryOptions { public bool TrackDeaths = true; public int RetentionDays; public bool ShowSessionCount = true; public bool ShowPlayTime = true; public bool ShowDeaths = true; public bool ShowLastSeen = true; public int RecentSessions = 30; } internal sealed class PlayerHistory { private sealed class Live { public PlayerRecord Player; public SessionRecord Session; public string Character = ""; public bool DeathCounted; } private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { DateTimeZoneHandling = (DateTimeZoneHandling)1, DateFormatHandling = (DateFormatHandling)0, Formatting = (Formatting)1 }; private readonly string _path; private readonly HistoryOptions _options; private readonly HistoryFile _file; private readonly Dictionary<string, PlayerRecord> _byId = new Dictionary<string, PlayerRecord>(); private readonly Dictionary<long, Live> _live = new Dictionary<long, Live>(); private readonly List<long> _gone = new List<long>(); private volatile string _json; private bool _dirty; public string Json => _json; public bool Dirty => _dirty; public int PlayerCount => _file.Players.Count; public PlayerHistory(string path) : this(path, new HistoryOptions(), DateTime.UtcNow) { } public PlayerHistory(string path, HistoryOptions options, DateTime now) { _path = path; _options = options; _file = Load(path) ?? new HistoryFile(); if (options.RetentionDays > 0) { Prune(now.AddDays(-options.RetentionDays)); } foreach (PlayerRecord player in _file.Players) { if (!string.IsNullOrEmpty(player.Id)) { _byId[player.Id] = player; } } _json = BuildJson(); } private void Prune(DateTime cutoff) { int num = 0; int num2 = 0; foreach (PlayerRecord player in _file.Players) { num += player.Sessions.Count + player.Deaths.Count; player.Sessions.RemoveAll((SessionRecord s) => s.End < cutoff); player.Deaths.RemoveAll((DeathRecord d) => d.Time < cutoff); num2 += player.Sessions.Count + player.Deaths.Count; } _file.Players.RemoveAll((PlayerRecord p) => p.Sessions.Count == 0 && p.Deaths.Count == 0); if (num2 != num) { _dirty = true; } } public bool TryGetLive(long peerId, out DateTime sessionStart, out int sessionDeaths, out int totalDeaths) { if (_live.TryGetValue(peerId, out var value)) { sessionStart = value.Session.Start; sessionDeaths = value.Session.Deaths; totalDeaths = value.Player.Deaths.Count; return true; } sessionStart = DateTime.MinValue; sessionDeaths = 0; totalDeaths = 0; return false; } public void Update(List<PlayerEntry> players, DateTime now, int day) { bool flag = false; foreach (PlayerEntry player in players) { if (!_live.TryGetValue(player.Id, out var value)) { value = new Live { Player = RecordFor(player.Key, player.Name) }; value.Session = new SessionRecord { Character = player.Name, Start = now, End = now }; value.Player.Sessions.Add(value.Session); value.Player.Name = player.Name; _live[player.Id] = value; flag = true; } value.Session.End = now; if (player.HasCharacter && player.CharacterKey != value.Character) { if (value.Character.Length > 0 && !value.DeathCounted) { RecordDeath(value, now, day); flag = true; } value.Character = player.CharacterKey; value.DeathCounted = false; } if (player.Dead && !value.DeathCounted) { RecordDeath(value, now, day); flag = true; } } _gone.Clear(); foreach (KeyValuePair<long, Live> item in _live) { bool flag2 = false; foreach (PlayerEntry player2 in players) { if (player2.Id == item.Key) { flag2 = true; break; } } if (!flag2) { _gone.Add(item.Key); } } foreach (long item2 in _gone) { _live[item2].Session.End = now; _live.Remove(item2); flag = true; } if (flag || _live.Count > 0) { _dirty = true; _json = BuildJson(); } } private void RecordDeath(Live live, DateTime now, int day) { live.DeathCounted = true; if (_options.TrackDeaths) { live.Session.Deaths++; live.Player.Deaths.Add(new DeathRecord { Time = now, Day = day, Character = live.Session.Character }); } } private PlayerRecord RecordFor(string key, string name) { if (_byId.TryGetValue(key, out var value)) { return value; } value = new PlayerRecord { Id = key, Name = name }; _byId[key] = value; _file.Players.Add(value); return value; } private string BuildJson() { List<PlayerRecord> list = new List<PlayerRecord>(_file.Players); list.Sort((PlayerRecord a, PlayerRecord b) => b.LastSeen.CompareTo(a.LastSeen)); JsonWriter jsonWriter = new JsonWriter(512 + list.Count * 400); jsonWriter.BeginObject(); jsonWriter.Key("players").BeginArray(); foreach (PlayerRecord item in list) { bool value = false; foreach (Live value2 in _live.Values) { if (value2.Player == item) { value = true; break; } } jsonWriter.BeginObject(); jsonWriter.Prop("name", item.Name); jsonWriter.Prop("online", value); if (_options.ShowSessionCount) { jsonWriter.Prop("sessions", item.Sessions.Count); } if (_options.ShowPlayTime) { jsonWriter.Prop("playSeconds", (long)item.TotalSeconds); } if (_options.ShowDeaths && _options.TrackDeaths) { jsonWriter.Prop("deaths", item.Deaths.Count); } if (_options.ShowLastSeen) { jsonWriter.Prop("lastSeen", (item.LastSeen == DateTime.MinValue) ? null : Iso(item.LastSeen)); } if (_options.RecentSessions > 0) { jsonWriter.Key("recent").BeginArray(); int num = item.Sessions.Count - 1; int num2 = 0; while (num >= 0 && num2 < _options.RecentSessions) { SessionRecord sessionRecord = item.Sessions[num]; jsonWriter.BeginObject(); jsonWriter.Prop("character", sessionRecord.Character); jsonWriter.Prop("start", Iso(sessionRecord.Start)); jsonWriter.Prop("end", Iso(sessionRecord.End)); if (_options.ShowPlayTime) { jsonWriter.Prop("seconds", (long)sessionRecord.Seconds); } if (_options.ShowDeaths && _options.TrackDeaths) { jsonWriter.Prop("deaths", sessionRecord.Deaths); } jsonWriter.EndObject(); num--; num2++; } jsonWriter.EndArray(); } jsonWriter.EndObject(); } jsonWriter.EndArray(); jsonWriter.EndObject(); return jsonWriter.ToString(); } public static string Iso(DateTime t) { return t.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'"); } public void Save() { string text = _path + ".tmp"; Directory.CreateDirectory(Path.GetDirectoryName(_path)); File.WriteAllText(text, JsonConvert.SerializeObject((object)_file, Settings)); if (File.Exists(_path)) { File.Delete(_path); } File.Move(text, _path); _dirty = false; } private static HistoryFile Load(string path) { if (!File.Exists(path)) { return null; } HistoryFile historyFile = JsonConvert.DeserializeObject<HistoryFile>(File.ReadAllText(path), Settings); if (historyFile == null) { return null; } if (historyFile.Players == null) { historyFile.Players = new List<PlayerRecord>(); } foreach (PlayerRecord player in historyFile.Players) { if (player.Sessions == null) { player.Sessions = new List<SessionRecord>(); } if (player.Deaths == null) { player.Deaths = new List<DeathRecord>(); } } return historyFile; } } internal struct PlayerEntry { public long Id; public string Key; public string Name; public bool Visible; public bool HasCharacter; public string CharacterKey; public bool Dead; public Vector3 Position; public float Yaw; public string Biome; } internal static class PlayerTracker { public const string HostKey = "host"; private static readonly FieldInfo HostPublicRefPos = typeof(ZNet).GetField("m_publicReferencePosition", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly Dictionary<long, string> KeyByPeer = new Dictionary<long, string>(); public static void Collect(ZNet znet, List<PlayerEntry> into) { //IL_01d7: Unknown result type (might be due to invalid IL or missing references) //IL_01dc: Unknown result type (might be due to invalid IL or missing references) //IL_0236: Unknown result type (might be due to invalid IL or missing references) //IL_023b: Unknown result type (might be due to invalid IL or missing references) //IL_0267: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Unknown result type (might be due to invalid IL or missing references) //IL_0276: Unknown result type (might be due to invalid IL or missing references) //IL_027b: Unknown result type (might be due to invalid IL or missing references) //IL_027f: Unknown result type (might be due to invalid IL or missing references) //IL_029b: Unknown result type (might be due to invalid IL or missing references) //IL_02a2: Unknown result type (might be due to invalid IL or missing references) //IL_02af: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) into.Clear(); ZDOMan instance = ZDOMan.instance; WorldGenerator instance2 = WorldGenerator.instance; Quaternion rotation; foreach (ZNetPeer peer in znet.GetPeers()) { if (peer != null && peer.IsReady() && !peer.m_server && !string.IsNullOrEmpty(peer.m_playerName)) { bool flag = !((ZDOID)(ref peer.m_characterID)).IsNone(); ZDO val = ((flag && instance != null) ? instance.GetZDO(peer.m_characterID) : null); PlayerEntry playerEntry = new PlayerEntry { Id = peer.m_uid, Key = KeyFor(peer), Name = peer.m_playerName, HasCharacter = flag, CharacterKey = (flag ? ((object)Unsafe.As<ZDOID, ZDOID>(ref peer.m_characterID)/*cast due to .constrained prefix*/).ToString() : ""), Dead = (val != null && val.GetBool(ZDOVars.s_dead, false)), Visible = (flag && peer.m_publicRefPos), Position = ((val != null) ? val.GetPosition() : peer.m_refPos) }; float yaw; if (val == null) { yaw = 0f; } else { rotation = val.GetRotation(); yaw = ((Quaternion)(ref rotation)).eulerAngles.y; } playerEntry.Yaw = yaw; PlayerEntry item = playerEntry; item.Biome = ((instance2 != null && flag) ? BiomeId.Name(instance2.GetBiome(item.Position.x, item.Position.z, 0.02f, false)) : ""); into.Add(item); } } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { Vector3 position = ((Component)localPlayer).transform.position; bool visible = HostPublicRefPos != null && (bool)HostPublicRefPos.GetValue(znet); PlayerEntry playerEntry = new PlayerEntry { Id = 0L, Key = "host", Name = localPlayer.GetPlayerName(), HasCharacter = true, CharacterKey = ((object)((Character)localPlayer).GetZDOID()/*cast due to .constrained prefix*/).ToString(), Dead = ((Character)localPlayer).IsDead(), Visible = visible, Position = position }; rotation = ((Component)localPlayer).transform.rotation; playerEntry.Yaw = ((Quaternion)(ref rotation)).eulerAngles.y; playerEntry.Biome = ((instance2 != null) ? BiomeId.Name(instance2.GetBiome(position.x, position.z, 0.02f, false)) : ""); into.Add(playerEntry); } if (KeyByPeer.Count > 256) { KeyByPeer.Clear(); } } private static string KeyFor(ZNetPeer peer) { if (KeyByPeer.TryGetValue(peer.m_uid, out var value)) { return value; } try { value = ((peer.m_socket != null) ? peer.m_socket.GetHostName() : null); } catch (Exception) { value = null; } if (string.IsNullOrEmpty(value) || value == "0") { value = "name:" + peer.m_playerName; } KeyByPeer[peer.m_uid] = value; return value; } } [BepInPlugin("com.valheimwebmap", "Valheim Web Map", "1.0.2")] public sealed class Plugin : BaseUnityPlugin, IMapApi { private PluginConfig _cfg; private WebServer _server; private MapSession _session; private string _emptyState; private void Awake() { _cfg = new PluginConfig(((BaseUnityPlugin)this).Config); _emptyState = new JsonWriter().BeginObject().Prop("world", null).Prop("mapReady", value: false) .Key("players") .BeginArray() .EndArray() .EndObject() .ToString(); } private void Update() { if (_session == null) { if (WorldIsReady()) { StartSession(); } } else if ((Object)(object)ZNet.instance == (Object)null) { EndSession(); } else { _session.Tick(Time.unscaledDeltaTime); } } private static bool WorldIsReady() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return false; } if (WorldGenerator.instance == null || (Object)(object)ZoneSystem.instance == (Object)null) { return false; } World world = ZNet.World; if (world != null && world.m_biomeData != null) { return world.m_biomeData.IsReady; } return false; } private void StartSession() { try { _session = new MapSession(_cfg, ((BaseUnityPlugin)this).Logger, ZNet.World); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Tracking world '" + _session.WorldName + "'")); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Could not start map session: " + ex)); ((Behaviour)this).enabled = false; return; } if (_server != null) { return; } _server = new WebServer(((BaseUnityPlugin)this).Logger, this); try { _server.Start(_cfg.Host.Value, _cfg.Port.Value); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)("Could not start the web server on port " + _cfg.Port.Value + ": " + ex2.Message)); _server.Dispose(); _server = null; } } private void EndSession() { if (_session != null) { _session.Dispose(); _session = null; _server?.Dispose(); _server = null; ((BaseUnityPlugin)this).Logger.LogInfo((object)"World closed; map tracking stopped"); } } private void OnDestroy() { EndSession(); } private void OnApplicationQuit() { EndSession(); } public string InfoJson() { JsonWriter jsonWriter = new JsonWriter(); jsonWriter.BeginObject(); jsonWriter.Prop("world", _session?.WorldName); jsonWriter.Prop("mapHalfSize", 10240.0, 0); jsonWriter.Prop("worldRadius", 10000.0, 0); jsonWriter.Prop("tileSize", 256L); jsonWriter.Prop("maxZoom", _cfg.MaxZoom.Value); jsonWriter.Prop("updateInterval", _cfg.UpdateInterval.Value, 2); jsonWriter.Prop("version", "1.0.2"); jsonWriter.Key("features").BeginObject(); jsonWriter.Prop("history", _cfg.ShowHistory.Value && _cfg.TrackSessions.Value); jsonWriter.Prop("hiddenPlayers", _cfg.ShowHiddenPlayers.Value); jsonWriter.Prop("biome", _cfg.ShowBiome.Value); jsonWriter.Prop("coordinates", _cfg.ShowCoordinates.Value); jsonWriter.Prop("heading", _cfg.ShowHeading.Value); jsonWriter.Prop("timeOnline", _cfg.ShowTimeOnline.Value && _cfg.TrackSessions.Value); jsonWriter.Prop("deadStatus", _cfg.ShowDeadStatus.Value); jsonWriter.Prop("deathCounts", _cfg.ShowDeathCounts.Value && _cfg.TrackDeaths.Value && _cfg.TrackSessions.Value); jsonWriter.Prop("dayTime", _cfg.ShowDayAndTime.Value); jsonWriter.Prop("explored", _cfg.ShowExploredPercent.Value); jsonWriter.EndObject(); jsonWriter.EndObject(); return jsonWriter.ToString(); } public string StateJson() { MapSession session = _session; if (session == null) { return _emptyState; } return session.StateJson; } public string HistoryJson() { MapSession session = _session; if (session == null) { return "{\"players\":[]}"; } return session.HistoryJson; } public bool TryGetTile(int z, int x, int y, out byte[] png, out string etag) { MapSession session = _session; if (session == null) { png = null; etag = null; return false; } return session.Tiles.TryGetTile(z, x, y, out png, out etag); } } internal sealed class PluginConfig { public readonly ConfigEntry<int> Port; public readonly ConfigEntry<string> Host; public readonly ConfigEntry<int> Resolution; public readonly ConfigEntry<int> RenderThreads; public readonly ConfigEntry<int> MaxZoom; public readonly ConfigEntry<float> ExploreRadius; public readonly ConfigEntry<bool> RevealGeneratedZones; public readonly ConfigEntry<int> RevealGeneratedZonesMargin; public readonly ConfigEntry<float> UpdateInterval; public readonly ConfigEntry<string> DataDirectory; public readonly ConfigEntry<float> SaveInterval; public readonly ConfigEntry<bool> TrackSessions; public readonly ConfigEntry<bool> TrackDeaths; public readonly ConfigEntry<int> HistoryRetentionDays; public readonly ConfigEntry<bool> ShowHiddenPlayers; public readonly ConfigEntry<bool> ShowBiome; public readonly ConfigEntry<bool> ShowCoordinates; public readonly ConfigEntry<bool> ShowHeading; public readonly ConfigEntry<bool> ShowTimeOnline; public readonly ConfigEntry<bool> ShowDeadStatus; public readonly ConfigEntry<bool> ShowDeathCounts; public readonly ConfigEntry<bool> ShowDayAndTime; public readonly ConfigEntry<bool> ShowExploredPercent; public readonly ConfigEntry<bool> ShowHistory; public readonly ConfigEntry<bool> ShowSessionCount; public readonly ConfigEntry<bool> ShowPlayTime; public readonly ConfigEntry<bool> ShowLastSeen; public readonly ConfigEntry<int> RecentSessions; public PluginConfig(ConfigFile file) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Expected O, but got Unknown //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Expected O, but got Unknown //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Expected O, but got Unknown //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Expected O, but got Unknown //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Expected O, but got Unknown //IL_0225: Unknown result type (might be due to invalid IL or missing references) //IL_022f: Expected O, but got Unknown //IL_03c3: Unknown result type (might be due to invalid IL or missing references) //IL_03cd: Expected O, but got Unknown Port = file.Bind<int>("Web", "Port", 3000, "TCP port the built-in web server listens on."); Host = file.Bind<string>("Web", "Host", "*", "Address to bind. '*' binds every interface. On Windows a non-administrator process may need a specific address (for example 'localhost' or '127.0.0.1') or a URL reservation via netsh."); Resolution = file.Bind<int>("Map", "Resolution", 4096, new ConfigDescription("Pixels across the rendered world map. 4096 is 5 m per pixel (the in-game map is 12 m per pixel). Doubling it quadruples render time and memory.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[4] { 1024, 2048, 4096, 8192 }), Array.Empty<object>())); RenderThreads = file.Bind<int>("Map", "RenderThreads", 0, "Worker threads for rendering the world map on startup. 0 picks half the CPU cores, at least one."); MaxZoom = file.Bind<int>("Map", "MaxZoom", 7, new ConfigDescription("Deepest zoom level offered in the browser; levels past the native resolution are upscaled.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(3, 9), Array.Empty<object>())); ExploreRadius = file.Bind<float>("Exploration", "ExploreRadius", 100f, "Metres revealed around a player, matching the in-game map's reveal radius."); RevealGeneratedZones = file.Bind<bool>("Exploration", "RevealGeneratedZones", true, "On startup, reveal every zone the world save has already generated. Zones are only generated near players, so this recovers exploration from before the mod was installed."); RevealGeneratedZonesMargin = file.Bind<int>("Exploration", "RevealGeneratedZonesMargin", 1, new ConfigDescription("The game generates zones a little further out than the map reveals. A zone is revealed only when all zones within this many zones of it are generated too, which trims that extra ring.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3), Array.Empty<object>())); UpdateInterval = file.Bind<float>("Players", "UpdateInterval", 1f, new ConfigDescription("Seconds between player position samples.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 10f), Array.Empty<object>())); DataDirectory = file.Bind<string>("Storage", "DataDirectory", "", "Where the rendered map and exploration data are kept. Empty uses BepInEx/config/ValheimWebMap/<world>."); SaveInterval = file.Bind<float>("Storage", "SaveInterval", 60f, new ConfigDescription("Seconds between writes of the exploration data when it changed.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 3600f), Array.Empty<object>())); TrackSessions = file.Bind<bool>("History", "TrackSessions", true, "Record a play session (start, end, character) for every connection in history.json."); TrackDeaths = file.Bind<bool>("History", "TrackDeaths", true, "Record player deaths in history.json. Requires TrackSessions."); HistoryRetentionDays = file.Bind<int>("History", "RetentionDays", 0, new ConfigDescription("Drop sessions and deaths older than this many days when the world loads. 0 keeps everything.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3650), Array.Empty<object>())); ShowHiddenPlayers = file.Bind<bool>("Display", "ShowHiddenPlayers", true, "List online players who do not share their position (name only, no location)."); ShowBiome = file.Bind<bool>("Display", "ShowBiome", true, "Show the biome a visible player is in."); ShowCoordinates = file.Bind<bool>("Display", "ShowCoordinates", true, "Show a visible player's world coordinates as text in the player list."); ShowHeading = file.Bind<bool>("Display", "ShowHeading", true, "Show which way a visible player is facing."); ShowTimeOnline = file.Bind<bool>("Display", "ShowTimeOnline", true, "Show how long each online player has been connected. Requires TrackSessions."); ShowDeadStatus = file.Bind<bool>("Display", "ShowDeadStatus", true, "Mark players who are currently dead."); ShowDeathCounts = file.Bind<bool>("Display", "ShowDeathCounts", true, "Show death counts for online players and in the history. Requires TrackDeaths."); ShowDayAndTime = file.Bind<bool>("Display", "ShowDayAndTime", true, "Show the in-game day and time of day."); ShowExploredPercent = file.Bind<bool>("Display", "ShowExploredPercent", true, "Show how much of the world has been explored."); ShowHistory = file.Bind<bool>("Display", "ShowHistory", true, "Offer the History tab and the /api/history endpoint. Requires TrackSessions."); ShowSessionCount = file.Bind<bool>("Display", "ShowSessionCount", true, "History: show how many sessions each player has had."); ShowPlayTime = file.Bind<bool>("Display", "ShowPlayTime", true, "History: show each player's total play time."); ShowLastSeen = file.Bind<bool>("Display", "ShowLastSeen", true, "History: show when each player was last online."); RecentSessions = file.Bind<int>("Display", "RecentSessions", 30, new ConfigDescription("History: how many recent sessions to list per player. 0 hides the per-session list.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 365), Array.Empty<object>())); } } internal static class PngEncoder { private static readonly byte[] Signature = new byte[8] { 137, 80, 78, 71, 13, 10, 26, 10 }; private static readonly uint[] CrcTable = BuildCrcTable(); public static byte[] EncodeRgb(byte[] rgb, int width, int height) { int num = width * 3; byte[] array = new byte[(num + 1) * height]; for (int i = 0; i < height; i++) { int num2 = i * (num + 1); array[num2] = 0; Buffer.BlockCopy(rgb, i * num, array, num2 + 1, num); } byte[] array2 = Zlib(array); byte[] array3 = new byte[13]; WriteBigEndian(array3, 0, width); WriteBigEndian(array3, 4, height); array3[8] = 8; array3[9] = 2; array3[10] = 0; array3[11] = 0; array3[12] = 0; using MemoryStream memoryStream = new MemoryStream(array2.Length + 128); memoryStream.Write(Signature, 0, Signature.Length); WriteChunk(memoryStream, "IHDR", array3); WriteChunk(memoryStream, "IDAT", array2); WriteChunk(memoryStream, "IEND", new byte[0]); return memoryStream.ToArray(); } public static byte[] Zlib(byte[] data) { using MemoryStream memoryStream = new MemoryStream(data.Length / 2 + 64); memoryStream.WriteByte(120); memoryStream.WriteByte(156); using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true)) { deflateStream.Write(data, 0, data.Length); } uint num = Adler32(data); memoryStream.WriteByte((byte)(num >> 24)); memoryStream.WriteByte((byte)(num >> 16)); memoryStream.WriteByte((byte)(num >> 8)); memoryStream.WriteByte((byte)num); return memoryStream.ToArray(); } public static byte[] Unzlib(byte[] data, int expectedLength) { if (data.Length < 6) { throw new InvalidDataException("zlib stream too short"); } byte[] array = new byte[expectedLength]; using MemoryStream stream = new MemoryStream(data, 2, data.Length - 6); using DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress); int i; int num; for (i = 0; i < expectedLength; i += num) { num = deflateStream.Read(array, i, expectedLength - i); if (num <= 0) { break; } } if (i != expectedLength) { throw new InvalidDataException("zlib stream shorter than expected"); } return array; } private static void WriteChunk(Stream s, string type, byte[] data) { byte[] array = new byte[4]; WriteBigEndian(array, 0, data.Length); s.Write(array, 0, 4); byte[] array2 = new byte[4]; for (int i = 0; i < 4; i++) { array2[i] = (byte)type[i]; } s.Write(array2, 0, 4); s.Write(data, 0, data.Length); uint crc = Crc32(array2, 0, 4, uint.MaxValue); crc = Crc32(data, 0, data.Length, crc) ^ 0xFFFFFFFFu; byte[] array3 = new byte[4]; WriteBigEndian(array3, 0, (int)crc); s.Write(array3, 0, 4); } private static void WriteBigEndian(byte[] buf, int offset, int value) { buf[offset] = (byte)(value >> 24); buf[offset + 1] = (byte)(value >> 16); buf[offset + 2] = (byte)(value >> 8); buf[offset + 3] = (byte)value; } private static uint[] BuildCrcTable() { uint[] array = new uint[256]; for (uint num = 0u; num < 256; num++) { uint num2 = num; for (int i = 0; i < 8; i++) { num2 = (((num2 & 1) != 0) ? (0xEDB88320u ^ (num2 >> 1)) : (num2 >> 1)); } array[num] = num2; } return array; } private static uint Crc32(byte[] buf, int offset, int count, uint crc) { for (int i = offset; i < offset + count; i++) { crc = CrcTable[(crc ^ buf[i]) & 0xFF] ^ (crc >> 8); } return crc; } private static uint Adler32(byte[] data) { uint num = 1u; uint num2 = 0u; int i = 0; while (i < data.Length) { for (int num3 = Math.Min(i + 5552, data.Length); i < num3; i++) { num += data[i]; num2 += num; } num %= 65521; num2 %= 65521; } return (num2 << 16) | num; } } internal sealed class TileService { private sealed class CachedTile { public int AtlasId; public int RegionVersion; public byte[] Png; public string ETag; } private const int T = 256; private readonly ExploredMask _mask; private readonly float _half; private readonly int _maxZoom; private readonly string _epoch; private readonly object _lock = new object(); private readonly Dictionary<long, CachedTile> _cache = new Dictionary<long, CachedTile>(); private readonly byte[] _fogPng; private volatile MapAtlas _atlas; public MapAtlas Atlas { get { return _atlas; } set { _atlas = value; lock (_lock) { _cache.Clear(); } } } public TileService(ExploredMask mask, float halfSize, int maxZoom, string epoch) { _mask = mask; _half = halfSize; _maxZoom = maxZoom; _epoch = epoch; byte[] array = new byte[196608]; for (int i = 0; i < 65536; i++) { array[i * 3] = Palette.Fog.R; array[i * 3 + 1] = Palette.Fog.G; array[i * 3 + 2] = Palette.Fog.B; } _fogPng = PngEncoder.EncodeRgb(array, 256, 256); } public bool TryGetTile(int z, int x, int y, out byte[] png, out string etag) { png = null; etag = null; MapAtlas atlas = _atlas; if (atlas == null || z < 0 || z > _maxZoom) { return false; } int num = 1 << z; if (x < 0 || y < 0 || x >= num || y >= num) { return false; } float num2 = _half * 2f / (float)(256 << z); float num3 = 0f - _half + (float)(x * 256) * num2; float num4 = num3 + 256f * num2; float num5 = _half - (float)(y * 256) * num2; float num6 = num5 - 256f * num2; float num7 = _mask.CellSize * 2f; if (!_mask.AnyExplored(num3 - num7, num6 - num7, num4 + num7, num5 + num7)) { png = _fogPng; etag = "\"" + _epoch + "-fog\""; return true; } int num8 = _mask.RegionVersion(num3 - num7, num6 - num7, num4 + num7, num5 + num7); long key = ((long)z << 56) | ((long)x << 28) | y; lock (_lock) { if (_cache.TryGetValue(key, out var value) && value.AtlasId == atlas.Id && value.RegionVersion == num8) { png = value.Png; etag = value.ETag; return true; } } byte[] rgb = Compose(atlas, z, x, y, num2, num3, num5); png = PngEncoder.EncodeRgb(rgb, 256, 256); etag = "\"" + _epoch + "-" + atlas.Id + "-" + num8 + "\""; lock (_lock) { if (_cache.Count > 4096) { _cache.Clear(); } _cache[key] = new CachedTile { AtlasId = atlas.Id, RegionVersion = num8, Png = png, ETag = etag }; } return true; } private byte[] Compose(MapAtlas atlas, int z, int tx, int ty, float mpp, float minX, float maxZ) { int num = 256 << z; byte[] array = new byte[196608]; byte[] array2; int num3; int num4; if (atlas.Size >= num) { int num2 = MapAtlas.Log2(atlas.Size / num); array2 = atlas.Levels[num2]; num3 = atlas.LevelSize(num2); num4 = 0; } else { array2 = atlas.Levels[0]; num3 = atlas.Size; num4 = MapAtlas.Log2(num / atlas.Size); } Rgb fog = Palette.Fog; for (int i = 0; i < 256; i++) { int num5 = ty * 256 + i >> num4; float wz = maxZ - ((float)i + 0.5f) * mpp; for (int j = 0; j < 256; j++) { int num6 = tx * 256 + j >> num4; float wx = minX + ((float)j + 0.5f) * mpp; int num7 = (i * 256 + j) * 3; float num8 = _mask.Sample(wx, wz); if (num8 <= 0.001f) { array[num7] = fog.R; array[num7 + 1] = fog.G; array[num7 + 2] = fog.B; continue; } int num9 = (num5 * num3 + num6) * 3; if (num8 >= 0.999f) { array[num7] = array2[num9]; array[num7 + 1] = array2[num9 + 1]; array[num7 + 2] = array2[num9 + 2]; } else { float num10 = 1f - num8; array[num7] = (byte)((float)(int)array2[num9] * num8 + (float)(int)fog.R * num10 + 0.5f); array[num7 + 1] = (byte)((float)(int)array2[num9 + 1] * num8 + (float)(int)fog.G * num10 + 0.5f); array[num7 + 2] = (byte)((float)(int)array2[num9 + 2] * num8 + (float)(int)fog.B * num10 + 0.5f); } } } return array; } } internal interface IMapApi { string InfoJson(); string StateJson(); string HistoryJson(); bool TryGetTile(int z, int x, int y, out byte[] png, out string etag); } internal sealed class WebServer : IDisposable { private static readonly Dictionary<string, string> ContentTypes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { ".html", "text/html; charset=utf-8" }, { ".js", "application/javascript; charset=utf-8" }, { ".css", "text/css; charset=utf-8" }, { ".png", "image/png" }, { ".svg", "image/svg+xml" }, { ".ico", "image/x-icon" }, { ".txt", "text/plain; charset=utf-8" }, { ".json", "application/json; charset=utf-8" } }; private readonly ManualLogSource _log; private readonly IMapApi _api; private readonly string _overrideDir; private readonly Dictionary<string, string> _etags = new Dictionary<string, string>(); private HttpListener _listener; private volatile bool _stopping; public WebServer(ManualLogSource log, IMapApi api) { _log = log; _api = api; string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); _overrideDir = ((directoryName != null) ? Path.Combine(directoryName, "web") : null); } public void Start(string host, int port) { if (string.IsNullOrEmpty(host)) { host = "*"; } _listener = new HttpListener(); _listener.Prefixes.Add("http://" + host + ":" + port + "/"); _listener.Start(); _listener.BeginGetContext(OnContext, null); _log.LogInfo((object)("Web map listening on http://" + host + ":" + port + "/")); } public void Dispose() { _stopping = true; try { _listener?.Stop(); _listener?.Close(); } catch (Exception) { } } private void OnContext(IAsyncResult ar) { if (_stopping) { return; } HttpListenerContext httpListenerContext; try { httpListenerContext = _listener.EndGetContext(ar); } catch (Exception) { return; } finally { if (!_stopping) { try { _listener.BeginGetContext(OnContext, null); } catch (Exception) { } } } try { Handle(httpListenerContext); } catch (Exception ex3) { _log.LogWarning((object)("Web request failed: " + ex3.Message)); try { httpListenerContext.Response.Abort(); } catch (Exception) { } } } private void Handle(HttpListenerContext ctx) { HttpListenerRequest request = ctx.Request; HttpListenerResponse response = ctx.Response; response.Headers["X-Content-Type-Options"] = "nosniff"; if (request.HttpMethod != "GET" && request.HttpMethod != "HEAD") { Status(response, 405); return; } string text = request.Url.AbsolutePath; if (text.StartsWith("/tiles/", StringComparison.Ordinal)) { ServeTile(request, response, text); return; } switch (text) { case "/api/info": SendText(request, response, _api.InfoJson(), "application/json; charset=utf-8", "no-store", null); return; case "/api/state": SendText(request, response, _api.StateJson(), "application/json; charset=utf-8", "no-store", null); return; case "/api/history": SendText(request, response, _api.HistoryJson(), "application/json; charset=utf-8", "no-store", null); return; case "/": text = "/index.html"; break; } ServeStatic(request, response, text.Substring(1)); } private void ServeTile(HttpListenerRequest req, HttpListenerResponse res, string path) { string[] array = path.Substring("/tiles/".Length).Split(new char[1] { '/' }); byte[] png; string etag; if (array.Length != 3 || !array[2].EndsWith(".png", StringComparison.Ordinal) || !int.TryParse(array[0], out var result) || !int.TryParse(array[1], out var result2) || !int.TryParse(array[2].Substring(0, array[2].Length - 4), out var result3)) { Status(res, 404); } else if (!_api.TryGetTile(result, result2, result3, out png, out etag)) { Status(res, 404); } else { SendBytes(req, res, png, "image/png", "no-cache", etag); } } private void ServeStatic(HttpListenerRequest req, HttpListenerResponse res, string name) { if (name.Length == 0 || name.IndexOf('/') >= 0 || name.IndexOf('\\') >= 0 || name.Contains("..")) { Status(res, 404); return; } if (!ContentTypes.TryGetValue(Path.GetExtension(name), out var value)) { Status(res, 404); return; } byte[] array = null; if (_overrideDir != null) { string path = Path.Combine(_overrideDir, name); if (File.Exists(path)) { array = File.ReadAllBytes(path); } } if (array == null) { using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("web/" + name); if (stream == null) { Status(res, 404); return; } using MemoryStream memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); array = memoryStream.ToArray(); } string value2; lock (_etags) { if (!_etags.TryGetValue(name, out value2)) { value2 = "\"" + Fnv1a64(array).ToString("x16") + "\""; _etags[name] = value2; } } string cacheControl = ((name == "index.html") ? "no-cache" : "public, max-age=300"); SendBytes(req, res, array, value, cacheControl, value2); } private static ulong Fnv1a64(byte[] data) { ulong num = 14695981039346656037uL; foreach (byte b in data) { num = (num ^ b) * 1099511628211L; } return num; } private static void SendText(HttpListenerRequest req, HttpListenerResponse res, string text, string contentType, string cacheControl, string etag) { SendBytes(req, res, Encoding.UTF8.GetBytes(text), contentType, cacheControl, etag); } private static void SendBytes(HttpListenerRequest req, HttpListenerResponse res, byte[] data, string contentType, string cacheControl, string etag) { if (etag != null) { res.Headers["ETag"] = etag; if (req.Headers["If-None-Match"] == etag) { res.StatusCode = 304; res.Headers["Cache-Control"] = cacheControl; res.Close(); return; } } res.StatusCode = 200; res.ContentType = contentType; res.Headers["Cache-Control"] = cacheControl; res.ContentLength64 = data.Length; if (req.HttpMethod == "HEAD") { res.Close(); return; } using (Stream stream = res.OutputStream) { stream.Write(data, 0, data.Length); } res.Close(); } private static void Status(HttpListenerResponse res, int code) { res.StatusCode = code; res.ContentLength64 = 0L; res.Close(); } } public static class MyPluginInfo { public const string PLUGIN_GUID = "com.valheimwebmap"; public const string PLUGIN_NAME = "Valheim Web Map"; public const string PLUGIN_VERSION = "1.0.2"; } }