Decompiled source of ValheimProgress v1.0.0

plugins/ValheimProgress.dll

Decompiled 2 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using UnityEngine;

[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
namespace ValheimProgress;

[BepInPlugin("nl.ricoscripts.valheimprogress", "Valheim Progress", "1.0.0")]
public sealed class ValheimProgressPlugin : BaseUnityPlugin
{
	private sealed class BiomeResult
	{
		public long Total;

		public long Explored;

		public List<BiomeItem> Items = new List<BiomeItem>();
	}

	private sealed class BiomeItem
	{
		public string Name;

		public long Total;

		public long Explored;
	}

	[HarmonyPatch(typeof(ZNet), "LoadWorld")]
	private static class LoadWorldPatch
	{
		private static void Postfix(ZNet __instance)
		{
			if (__instance.IsServer())
			{
				_worldStartedUtc = DateTime.UtcNow;
				Instance.LoadExploration();
				Instance.WriteStatus();
			}
		}
	}

	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	private static class NewConnectionPatch
	{
		private static void Postfix(ZNetPeer peer, ZNet __instance)
		{
			if (__instance.IsServer())
			{
				peer.m_rpc.Register<ZPackage>("ValheimProgress_MapData_v1", (Action<ZRpc, ZPackage>)ReceiveMapData);
			}
		}
	}

	[HarmonyPatch(typeof(Minimap), "SetMapData", new Type[] { typeof(byte[]) })]
	private static class MapLoadedPatch
	{
		private static void Postfix(Minimap __instance)
		{
			if ((Object)(object)Instance != (Object)null && (Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer())
			{
				((MonoBehaviour)Instance).StartCoroutine(SendFullMap(__instance));
			}
		}
	}

	[HarmonyPatch(typeof(Minimap), "Explore", new Type[]
	{
		typeof(int),
		typeof(int)
	})]
	private static class ExplorePatch
	{
		private static void Postfix(Minimap __instance, int x, int y, bool __result)
		{
			if (!__result || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer())
			{
				return;
			}
			int value = Traverse.Create((object)__instance).Field("m_textureSize").GetValue<int>();
			if (x < 0 || y < 0 || x >= value || y >= value)
			{
				return;
			}
			lock (PendingExploration)
			{
				PendingExploration.Add(y * value + x);
			}
		}
	}

	public const string PluginGuid = "nl.ricoscripts.valheimprogress";

	public const string PluginName = "Valheim Progress";

	public const string PluginVersion = "1.0.0";

	private const string MapRpc = "ValheimProgress_MapData_v1";

	private const int ProtocolVersion = 1;

	internal static ValheimProgressPlugin Instance;

	private static readonly object MapLock = new object();

	private static readonly HashSet<int> PendingExploration = new HashSet<int>();

	private static bool[] _explored = new bool[4194304];

	private static int _mapSize = 2048;

	private static float _pixelSize = 12f;

	private static bool _mapDirty;

	private static DateTime _worldStartedUtc = DateTime.UtcNow;

	private static DateTime _lastStatusUtc = DateTime.MinValue;

	private static DateTime _lastMapSaveUtc = DateTime.MinValue;

	private static DateTime _lastDeltaUtc = DateTime.MinValue;

	private ConfigEntry<string> _statusFile;

	private ConfigEntry<int> _statusInterval;

	private ConfigEntry<int> _samplingStep;

	private ConfigEntry<float> _worldRadius;

	private ConfigEntry<string> _bosses;

	private string DataDirectory => Path.Combine(Paths.ConfigPath, "rs.valheimprogress");

	private string ExplorationFile => Path.Combine(DataDirectory, "exploration.bin");

	private void Awake()
	{
		Instance = this;
		Directory.CreateDirectory(DataDirectory);
		_statusFile = ((BaseUnityPlugin)this).Config.Bind<string>("General", "StatusFile", Path.Combine(DataDirectory, "status.json"), "JSON-bestand dat door de Discord-bot wordt gelezen.");
		_statusInterval = ((BaseUnityPlugin)this).Config.Bind<int>("General", "StatusIntervalSeconds", 15, "Hoe vaak de server status.json vernieuwt.");
		_samplingStep = ((BaseUnityPlugin)this).Config.Bind<int>("Exploration", "BiomeSamplingStep", 4, "Gebruik iedere N-de kaartpixel voor biomepercentages. Lager is nauwkeuriger maar zwaarder.");
		_worldRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Exploration", "WorldRadius", 10500f, "Straal in wereldmeters die voor het totale kaartpercentage meetelt.");
		_bosses = ((BaseUnityPlugin)this).Config.Bind<string>("Progress", "Bosses", "Eikthyr=defeated_eikthyr;The Elder=defeated_gdking;Bonemass=defeated_bonemass;Moder=defeated_dragon;Yagluth=defeated_goblinking;The Queen=defeated_queen;Fader=defeated_fader", "Naam=globalkey, gescheiden door puntkomma's. Hierdoor kunnen nieuwe eindbazen later worden toegevoegd.");
		LoadExploration();
		Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "nl.ricoscripts.valheimprogress");
		((BaseUnityPlugin)this).Logger.LogInfo((object)"Valheim Progress 1.0.0 geladen.");
	}

	private void Update()
	{
		ZNet instance = ZNet.instance;
		if ((Object)(object)instance == (Object)null)
		{
			return;
		}
		if (instance.IsServer())
		{
			if ((DateTime.UtcNow - _lastStatusUtc).TotalSeconds >= (double)Math.Max(5, _statusInterval.Value))
			{
				_lastStatusUtc = DateTime.UtcNow;
				WriteStatus();
			}
			if (_mapDirty && (DateTime.UtcNow - _lastMapSaveUtc).TotalSeconds >= 60.0)
			{
				SaveExploration();
			}
		}
		else if ((DateTime.UtcNow - _lastDeltaUtc).TotalSeconds >= 10.0)
		{
			_lastDeltaUtc = DateTime.UtcNow;
			SendPendingExploration();
		}
	}

	private void OnDestroy()
	{
		if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
		{
			WriteStatus();
			if (_mapDirty)
			{
				SaveExploration();
			}
		}
	}

	private static void EnsureMap(int size, float pixelSize)
	{
		if (size < 256 || size > 8192)
		{
			return;
		}
		lock (MapLock)
		{
			if (_mapSize != size || _explored.Length != size * size)
			{
				_mapSize = size;
				_explored = new bool[size * size];
			}
			if (pixelSize > 0.1f && pixelSize < 100f)
			{
				_pixelSize = pixelSize;
			}
		}
	}

	private void LoadExploration()
	{
		try
		{
			if (!File.Exists(ExplorationFile))
			{
				return;
			}
			using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(ExplorationFile)))
			{
				if (binaryReader.ReadInt32() != 1)
				{
					return;
				}
				int num = binaryReader.ReadInt32();
				float pixelSize = binaryReader.ReadSingle();
				int num2 = binaryReader.ReadInt32();
				if (num < 256 || num > 8192 || num2 != (num * num + 7) / 8)
				{
					return;
				}
				EnsureMap(num, pixelSize);
				MergePacked(binaryReader.ReadBytes(num2), 0, num * num);
				_mapDirty = false;
			}
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Opgeslagen kaartverkenning geladen.");
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Kaartverkenning kon niet worden geladen: " + ex.Message));
		}
	}

	private void SaveExploration()
	{
		try
		{
			Directory.CreateDirectory(DataDirectory);
			byte[] array;
			lock (MapLock)
			{
				array = PackBits(_explored, 0, _explored.Length);
			}
			using (BinaryWriter binaryWriter = new BinaryWriter(File.Create(ExplorationFile)))
			{
				binaryWriter.Write(1);
				binaryWriter.Write(_mapSize);
				binaryWriter.Write(_pixelSize);
				binaryWriter.Write(array.Length);
				binaryWriter.Write(array);
			}
			_mapDirty = false;
			_lastMapSaveUtc = DateTime.UtcNow;
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Kaartverkenning kon niet worden opgeslagen: " + ex.Message));
		}
	}

	private static byte[] PackBits(bool[] values, int start, int length)
	{
		byte[] array = new byte[(length + 7) / 8];
		for (int i = 0; i < length; i++)
		{
			if (values[start + i])
			{
				array[i / 8] |= (byte)(1 << i % 8);
			}
		}
		return array;
	}

	private static void MergePacked(byte[] packed, int start, int length)
	{
		lock (MapLock)
		{
			int num = Math.Min(length, _explored.Length - start);
			for (int i = 0; i < num; i++)
			{
				if ((packed[i / 8] & (1 << i % 8)) != 0 && !_explored[start + i])
				{
					_explored[start + i] = true;
					_mapDirty = true;
				}
			}
		}
	}

	private static void ReceiveMapData(ZRpc rpc, ZPackage package)
	{
		try
		{
			package.SetPos(0);
			if (package.ReadInt() != 1)
			{
				return;
			}
			int num = package.ReadInt();
			int size = package.ReadInt();
			float pixelSize = package.ReadSingle();
			EnsureMap(size, pixelSize);
			switch (num)
			{
			case 1:
			{
				int num4 = package.ReadInt();
				int num5 = package.ReadInt();
				int num6 = package.ReadInt();
				if (num4 >= 0 && num5 >= 0 && num4 + num5 <= _explored.Length && num6 == (num5 + 7) / 8)
				{
					byte[] array = new byte[num6];
					for (int j = 0; j < num6; j++)
					{
						array[j] = package.ReadByte();
					}
					MergePacked(array, num4, num5);
				}
				break;
			}
			case 2:
			{
				int num2 = package.ReadInt();
				if (num2 < 0 || num2 > 100000)
				{
					break;
				}
				lock (MapLock)
				{
					for (int i = 0; i < num2; i++)
					{
						int num3 = package.ReadInt();
						if (num3 >= 0 && num3 < _explored.Length && !_explored[num3])
						{
							_explored[num3] = true;
							_mapDirty = true;
						}
					}
					break;
				}
			}
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)Instance).Logger.LogWarning((object)("Ongeldige kaartdata genegeerd: " + ex.Message));
		}
	}

	private static IEnumerator SendFullMap(Minimap map)
	{
		yield return (object)new WaitForSeconds(8f);
		while ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.GetServerRPC() == null)
		{
			yield return (object)new WaitForSeconds(1f);
		}
		if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || (Object)(object)map == (Object)null)
		{
			yield break;
		}
		Traverse mapFields = Traverse.Create((object)map);
		bool[] explored = mapFields.Field("m_explored").GetValue<bool[]>();
		int size = mapFields.Field("m_textureSize").GetValue<int>();
		float pixelSize = mapFields.Field("m_pixelSize").GetValue<float>();
		if (explored == null || explored.Length != size * size)
		{
			yield break;
		}
		int chunkSize = (explored.Length + 64 - 1) / 64;
		for (int chunk = 0; chunk < 64; chunk++)
		{
			int start = chunk * chunkSize;
			if (start >= explored.Length)
			{
				break;
			}
			int length = Math.Min(chunkSize, explored.Length - start);
			byte[] packed = PackBits(explored, start, length);
			ZPackage package = new ZPackage();
			package.Write(1);
			package.Write(1);
			package.Write(size);
			package.Write(pixelSize);
			package.Write(start);
			package.Write(length);
			package.Write(packed.Length);
			for (int i = 0; i < packed.Length; i++)
			{
				package.Write(packed[i]);
			}
			ZNet.instance.GetServerRPC().Invoke("ValheimProgress_MapData_v1", new object[1] { package });
			yield return null;
		}
	}

	private static void SendPendingExploration()
	{
		//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bc: Expected O, but got Unknown
		ZNet instance = ZNet.instance;
		Minimap instance2 = Minimap.instance;
		if ((Object)(object)instance == (Object)null || instance.IsServer() || instance.GetServerRPC() == null || (Object)(object)instance2 == (Object)null)
		{
			return;
		}
		int[] array;
		lock (PendingExploration)
		{
			if (PendingExploration.Count == 0)
			{
				return;
			}
			array = PendingExploration.Take(50000).ToArray();
			int[] array2 = array;
			foreach (int item in array2)
			{
				PendingExploration.Remove(item);
			}
		}
		ZPackage val = new ZPackage();
		val.Write(1);
		val.Write(2);
		Traverse val2 = Traverse.Create((object)instance2);
		val.Write(val2.Field("m_textureSize").GetValue<int>());
		val.Write(val2.Field("m_pixelSize").GetValue<float>());
		val.Write(array.Length);
		int[] array3 = array;
		foreach (int num in array3)
		{
			val.Write(num);
		}
		instance.GetServerRPC().Invoke("ValheimProgress_MapData_v1", new object[1] { val });
	}

	private void WriteStatus()
	{
		try
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null || !instance.IsServer())
			{
				return;
			}
			Directory.CreateDirectory(Path.GetDirectoryName(_statusFile.Value));
			List<string> source = (from name in (from player in instance.GetPlayerList()
					select player.m_name into name
					where !string.IsNullOrWhiteSpace(name)
					select name).Distinct()
				orderby name
				select name).ToList();
			int value = 0;
			if ((Object)(object)EnvMan.instance != (Object)null)
			{
				value = Traverse.Create((object)EnvMan.instance).Method("GetCurrentDay", Array.Empty<object>()).GetValue<int>() + 1;
			}
			BiomeResult biomeResult = CalculateBiomeStats();
			StringBuilder stringBuilder = new StringBuilder(4096);
			stringBuilder.Append("{\n  \"schemaVersion\": 1,");
			stringBuilder.Append("\n  \"generatedAt\": \"").Append(DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)).Append("\",");
			stringBuilder.Append("\n  \"online\": true,");
			stringBuilder.Append("\n  \"world\": \"").Append(Json(instance.GetWorldName())).Append("\",");
			stringBuilder.Append("\n  \"day\": ").Append(value).Append(',');
			stringBuilder.Append("\n  \"uptimeSeconds\": ").Append((long)(DateTime.UtcNow - _worldStartedUtc).TotalSeconds).Append(',');
			stringBuilder.Append("\n  \"players\": [").Append(string.Join(", ", source.Select((string name) => "\"" + Json(name) + "\""))).Append("],");
			stringBuilder.Append("\n  \"bosses\": [");
			List<KeyValuePair<string, string>> list = ParseBosses();
			for (int num = 0; num < list.Count; num++)
			{
				if (num > 0)
				{
					stringBuilder.Append(',');
				}
				stringBuilder.Append("\n    { \"name\": \"").Append(Json(list[num].Key)).Append("\", \"key\": \"")
					.Append(Json(list[num].Value))
					.Append("\", \"defeated\": ")
					.Append((!((Object)(object)ZoneSystem.instance != (Object)null) || !ZoneSystem.instance.GetGlobalKey(list[num].Value)) ? "false" : "true")
					.Append(" }");
			}
			stringBuilder.Append("\n  ],");
			stringBuilder.Append("\n  \"biomes\": [");
			for (int num2 = 0; num2 < biomeResult.Items.Count; num2++)
			{
				BiomeItem biomeItem = biomeResult.Items[num2];
				if (num2 > 0)
				{
					stringBuilder.Append(',');
				}
				stringBuilder.Append("\n    { \"name\": \"").Append(Json(biomeItem.Name)).Append("\", \"discovered\": ")
					.Append((biomeItem.Explored <= 0) ? "false" : "true")
					.Append(", \"exploredPercent\": ")
					.Append(Percent(biomeItem.Explored, biomeItem.Total))
					.Append(" }");
			}
			stringBuilder.Append("\n  ],");
			stringBuilder.Append("\n  \"worldExploredPercent\": ").Append(Percent(biomeResult.Explored, biomeResult.Total)).Append(',');
			stringBuilder.Append("\n  \"explorationSamples\": ").Append(biomeResult.Total).Append("\n}\n");
			File.WriteAllText(_statusFile.Value, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("status.json kon niet worden geschreven: " + ex.Message));
		}
	}

	private BiomeResult CalculateBiomeStats()
	{
		//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
		BiomeResult biomeResult = new BiomeResult();
		WorldGenerator instance = WorldGenerator.instance;
		if (instance == null)
		{
			return biomeResult;
		}
		int num = Math.Max(1, Math.Min(32, _samplingStep.Value));
		int num2 = _mapSize / 2;
		float num3 = _worldRadius.Value * _worldRadius.Value;
		Dictionary<string, BiomeItem> stats = new Dictionary<string, BiomeItem>(StringComparer.OrdinalIgnoreCase);
		lock (MapLock)
		{
			for (int i = 0; i < _mapSize; i += num)
			{
				float num4 = (float)(i - num2) * _pixelSize;
				for (int j = 0; j < _mapSize; j += num)
				{
					float num5 = (float)(j - num2) * _pixelSize;
					if (!(num5 * num5 + num4 * num4 > num3))
					{
						string text = FriendlyBiome(((object)instance.GetBiome(num5, num4, 0.02f, false)/*cast due to .constrained prefix*/).ToString());
						if (!stats.TryGetValue(text, out var value))
						{
							BiomeItem biomeItem = new BiomeItem();
							biomeItem.Name = text;
							value = biomeItem;
							stats[text] = value;
						}
						value.Total++;
						biomeResult.Total++;
						if (_explored[i * _mapSize + j])
						{
							value.Explored++;
							biomeResult.Explored++;
						}
					}
				}
			}
		}
		string[] order = new string[9] { "Meadows", "Black Forest", "Swamp", "Mountain", "Plains", "Mistlands", "Ashlands", "Deep North", "Ocean" };
		biomeResult.Items = order.Select((string name) => (!stats.ContainsKey(name)) ? new BiomeItem
		{
			Name = name
		} : stats[name]).ToList();
		foreach (BiomeItem item in from item in stats.Values
			where !order.Contains(item.Name)
			orderby item.Name
			select item)
		{
			biomeResult.Items.Add(item);
		}
		return biomeResult;
	}

	private List<KeyValuePair<string, string>> ParseBosses()
	{
		List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();
		string[] array = (_bosses.Value ?? string.Empty).Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries);
		foreach (string text in array)
		{
			string[] array2 = text.Split(new char[1] { '=' }, 2);
			if (array2.Length == 2 && array2[0].Trim().Length > 0 && array2[1].Trim().Length > 0)
			{
				list.Add(new KeyValuePair<string, string>(array2[0].Trim(), array2[1].Trim()));
			}
		}
		return list;
	}

	private static string FriendlyBiome(string name)
	{
		return name switch
		{
			"BlackForest" => "Black Forest", 
			"AshLands" => "Ashlands", 
			"DeepNorth" => "Deep North", 
			_ => name, 
		};
	}

	private static string Percent(long explored, long total)
	{
		return ((total > 0) ? ((double)explored * 100.0 / (double)total) : 0.0).ToString("0.000", CultureInfo.InvariantCulture);
	}

	private static string Json(string value)
	{
		if (value == null)
		{
			return string.Empty;
		}
		return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r")
			.Replace("\n", "\\n")
			.Replace("\t", "\\t");
	}
}