Decompiled source of ValheimWebMap v1.0.2

plugins/ValheimWebMap/ValheimWebMap.dll

Decompiled a day ago
using 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";
	}
}