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 Items = new List(); } 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("ValheimProgress_MapData_v1", (Action)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(); 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 PendingExploration = new HashSet(); 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 _statusFile; private ConfigEntry _statusInterval; private ConfigEntry _samplingStep; private ConfigEntry _worldRadius; private ConfigEntry _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("General", "StatusFile", Path.Combine(DataDirectory, "status.json"), "JSON-bestand dat door de Discord-bot wordt gelezen."); _statusInterval = ((BaseUnityPlugin)this).Config.Bind("General", "StatusIntervalSeconds", 15, "Hoe vaak de server status.json vernieuwt."); _samplingStep = ((BaseUnityPlugin)this).Config.Bind("Exploration", "BiomeSamplingStep", 4, "Gebruik iedere N-de kaartpixel voor biomepercentages. Lager is nauwkeuriger maar zwaarder."); _worldRadius = ((BaseUnityPlugin)this).Config.Bind("Exploration", "WorldRadius", 10500f, "Straal in wereldmeters die voor het totale kaartpercentage meetelt."); _bosses = ((BaseUnityPlugin)this).Config.Bind("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(); int size = mapFields.Field("m_textureSize").GetValue(); float pixelSize = mapFields.Field("m_pixelSize").GetValue(); 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()); val.Write(val2.Field("m_pixelSize").GetValue()); 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 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()).GetValue() + 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> 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 stats = new Dictionary(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> ParseBosses() { List> list = new List>(); 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(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"); } }