using System; using System.Diagnostics; using System.Globalization; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; 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("EilifCompanionClient")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.0.0")] [assembly: AssemblyInformationalVersion("0.2.0+5109d837318fa85e77890bf184d65a8f6bb60fec")] [assembly: AssemblyProduct("EilifCompanionClient")] [assembly: AssemblyTitle("EilifCompanionClient")] [assembly: AssemblyVersion("0.2.0.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 EilifCompanionClient { internal static class EilifDeathReporter { private static readonly FieldInfo LastHitField = AccessTools.Field(typeof(Character), "m_lastHit"); internal static void Report(Player who) { //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)who == (Object)null || (Object)(object)who != (Object)(object)Player.m_localPlayer || !EilifMapTrackerPlugin.IsOnServer()) { return; } if (LastHitField == null) { ManualLogSource log = EilifMapTrackerPlugin.Log; if (log != null) { log.LogWarning((object)"[EilifDeath] Character.m_lastHit not found — the game's field layout changed; no death reported."); } return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return; } object? value = LastHitField.GetValue(who); HitData val = (HitData)((value is HitData) ? value : null); if (val == null) { ManualLogSource log2 = EilifMapTrackerPlugin.Log; if (log2 != null) { log2.LogWarning((object)"[EilifDeath] no m_lastHit at death — nothing to report."); } return; } string text = ((object)Unsafe.As(ref val.m_hitType)/*cast due to .constrained prefix*/).ToString(); string text2 = ResolveAttacker(val); string text3 = ResolveBiome(who); string playerName = who.GetPlayerName(); string text4 = instance.GetWorldName() ?? ""; if (!string.IsNullOrEmpty(playerName)) { Vector3 position = ((Component)who).transform.position; string s = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture); string json = "{\"schemaVersion\":1,\"game\":\"valheim\",\"source\":\"eilif-death\",\"world\":" + EilifMapTrackerPlugin.JsonStr(text4) + ",\"player\":" + EilifMapTrackerPlugin.JsonStr(playerName) + ",\"tsUtc\":" + EilifMapTrackerPlugin.JsonStr(s) + ",\"hitType\":" + EilifMapTrackerPlugin.JsonStr(text) + ",\"attacker\":" + ((text2 == null) ? "null" : EilifMapTrackerPlugin.JsonStr(text2)) + ",\"biome\":" + EilifMapTrackerPlugin.JsonStr(text3) + ",\"pos\":{\"x\":" + Fmt(position.x) + ",\"z\":" + Fmt(position.z) + "}}"; ManualLogSource log3 = EilifMapTrackerPlugin.Log; if (log3 != null) { log3.LogInfo((object)("[EilifDeath] " + playerName + " died in " + text4 + ": hitType=" + text + ", attacker=" + (text2 ?? "(none)") + ", biome=" + text3)); } EilifMapTrackerPlugin.PostJson(json, "[EilifDeath]", "[EilifDeath] reported " + text + ((text2 == null) ? "" : (" by " + text2)) + " for " + playerName); } } private static string ResolveAttacker(HitData hit) { try { Character attacker = hit.GetAttacker(); if ((Object)(object)attacker == (Object)null) { return null; } Player val = (Player)(object)((attacker is Player) ? attacker : null); if ((Object)(object)val != (Object)null) { string playerName = val.GetPlayerName(); return string.IsNullOrEmpty(playerName) ? null : playerName; } string name = attacker.m_name; if (!string.IsNullOrEmpty(name)) { return name; } return PrefabName(((Component)attacker).gameObject); } catch (Exception ex) { ManualLogSource log = EilifMapTrackerPlugin.Log; if (log != null) { log.LogWarning((object)("[EilifDeath] attacker lookup failed: " + ex.Message)); } return null; } } private unsafe static string ResolveBiome(Player who) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) try { Vector3 position = ((Component)who).transform.position; Biome val = Heightmap.FindBiome(position); if ((int)val == 0 && WorldGenerator.instance != null) { val = WorldGenerator.instance.GetBiome(position.x, position.z, 0.02f, false); } return ((object)(*(Biome*)(&val))/*cast due to .constrained prefix*/).ToString(); } catch { return "None"; } } private static string PrefabName(GameObject go) { if ((Object)(object)go == (Object)null) { return null; } string name = ((Object)go).name; if (string.IsNullOrEmpty(name)) { return null; } int num = name.IndexOf("(Clone)", StringComparison.OrdinalIgnoreCase); if (num <= 0) { return name; } return name.Substring(0, num); } private static string Fmt(float v) { return ((float)Math.Round(v, 1)).ToString("0.#", CultureInfo.InvariantCulture); } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class Patch_PlayerOnDeath { private static void Postfix(Player __instance) { try { EilifDeathReporter.Report(__instance); } catch (Exception ex) { ManualLogSource log = EilifMapTrackerPlugin.Log; if (log != null) { log.LogWarning((object)("[EilifDeath] death hook failed: " + ex.Message)); } } } } [BepInPlugin("net.eilif.companionclient", "Eilif Companion Client", "0.2.0")] public class EilifMapTrackerPlugin : BaseUnityPlugin { public const string PluginGuid = "net.eilif.companionclient"; public const string PluginName = "Eilif Companion Client"; public const string PluginVersion = "0.2.0"; internal static ManualLogSource Log; internal static EilifMapTrackerPlugin Instance; internal static string IngestUrl = ""; internal static string IngestToken = ""; private ConfigEntry _url; private ConfigEntry _token; private ConfigEntry _intervalSeconds; private static readonly HttpClient Http = new HttpClient { Timeout = TimeSpan.FromSeconds(20.0) }; private int _postInFlight; private float _postTimer; private bool _wasConnected; private float _lastPct = -1f; private static readonly FieldInfo ExploredField = AccessTools.Field(typeof(Minimap), "m_explored"); private static readonly FieldInfo TextureSizeField = AccessTools.Field(typeof(Minimap), "m_textureSize"); private string _lastCachedPlayer = ""; private string _lastCachedWorld = ""; private void Awake() { //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) Log = ((BaseUnityPlugin)this).Logger; Instance = this; _url = ((BaseUnityPlugin)this).Config.Bind("Map", "Url", "https://valheim-dashboard.vercel.app/api/gs-ingest", "Dashboard ingest endpoint. The explored-map % is POSTed here as source:'client-map'."); _token = ((BaseUnityPlugin)this).Config.Bind("Map", "Token", "", "Optional Bearer token. If set it is sent as 'Authorization: Bearer '. Blank is fine (pilot)."); _intervalSeconds = ((BaseUnityPlugin)this).Config.Bind("Map", "IntervalSeconds", 300, new ConfigDescription("Seconds between map-% posts while connected to a server.", (AcceptableValueBase)(object)new AcceptableValueRange(60, 3600), Array.Empty())); IngestUrl = _url.Value; IngestToken = _token.Value; try { ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; } catch { } Harmony val = new Harmony("net.eilif.companionclient"); val.PatchAll(typeof(Patch_GameLogout)); val.PatchAll(typeof(Patch_PlayerOnDeath)); Log.LogInfo((object)string.Format("[EilifMap] {0} v{1} loaded. Posting explored-map % to {2} every {3}s while on a server.", "Eilif Companion Client", "0.2.0", _url.Value, _intervalSeconds.Value)); Log.LogInfo((object)("[EilifDeath] death-cause reporter armed (posts source:'eilif-death' to " + _url.Value + " when the local player dies on a server).")); } private void Update() { bool flag = IsOnServer(); if (flag) { _postTimer += Time.unscaledDeltaTime; if (_postTimer >= (float)_intervalSeconds.Value) { _postTimer = 0f; ComputeAndPost("interval"); } } else if (_wasConnected) { _postTimer = 0f; if (_lastPct >= 0f) { Post(_lastCachedPlayer, _lastCachedWorld, _lastPct, "disconnect(cached)"); } _lastPct = -1f; } _wasConnected = flag; } internal static bool IsOnServer() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || instance.IsServer()) { return false; } if (instance.GetServerPeer() == null) { return false; } if ((Object)(object)Minimap.instance != (Object)null) { return (Object)(object)Player.m_localPlayer != (Object)null; } return false; } internal void ComputeAndPost(string reason) { try { Minimap instance = Minimap.instance; ZNet instance2 = ZNet.instance; Player localPlayer = Player.m_localPlayer; if (!((Object)(object)instance == (Object)null) && !((Object)(object)instance2 == (Object)null) && !((Object)(object)localPlayer == (Object)null)) { float? num = ComputeExploredPct(instance); if (num.HasValue) { string playerName = localPlayer.GetPlayerName(); string text = instance2.GetWorldName() ?? ""; _lastCachedPlayer = playerName; _lastCachedWorld = text; _lastPct = num.Value; Post(playerName, text, num.Value, reason); } } } catch (Exception ex) { Log.LogWarning((object)("[EilifMap] compute/post failed: " + ex.Message)); } } private static float? ComputeExploredPct(Minimap mm) { if (ExploredField == null || TextureSizeField == null) { return null; } if (!(ExploredField.GetValue(mm) is bool[] array) || array.Length == 0) { return null; } int num = (int)TextureSizeField.GetValue(mm); if (num <= 0 || array.Length < num * num) { return null; } float num2 = (float)num / 2f; float num3 = num2 * num2; long num4 = 0L; long num5 = 0L; for (int i = 0; i < num; i++) { float num6 = (float)i - num2; float num7 = num3 - num6 * num6; if (num7 < 0f) { continue; } float num8 = (float)Math.Sqrt(num7); int num9 = Math.Max(0, (int)Math.Ceiling(num2 - num8)); int num10 = Math.Min(num - 1, (int)Math.Floor(num2 + num8)); int num11 = i * num; for (int j = num9; j <= num10; j++) { num5++; if (array[num11 + j]) { num4++; } } } if (num5 == 0L) { return null; } float val = (float)((double)num4 / (double)num5 * 100.0); return Math.Min(100f, val); } private void Post(string playerName, string world, float pct, string reason) { if (!string.IsNullOrEmpty(playerName) && Interlocked.CompareExchange(ref _postInFlight, 1, 0) == 0) { float num = (float)Math.Round(pct, 2); string json = "{\"schemaVersion\":1,\"game\":\"valheim\",\"source\":\"client-map\",\"playerName\":" + JsonStr(playerName) + ",\"world\":" + JsonStr(world) + ",\"exploredPct\":" + num.ToString("0.##", CultureInfo.InvariantCulture) + "}"; string url = _url.Value; string token = _token.Value; string pctText = num.ToString("0.##", CultureInfo.InvariantCulture); Task.Run(() => PostAsync(url, token, json, playerName, world, pctText, reason)); } } private async Task PostAsync(string url, string token, string json, string playerName, string world, string pctText, string reason) { try { HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, url); try { req.Content = (HttpContent)new StringContent(json, Encoding.UTF8, "application/json"); if (!string.IsNullOrEmpty(token)) { ((HttpHeaders)req.Headers).TryAddWithoutValidation("Authorization", "Bearer " + token); } HttpResponseMessage val = await Http.SendAsync(req).ConfigureAwait(continueOnCapturedContext: false); try { if (val.IsSuccessStatusCode) { Log.LogInfo((object)("[EilifMap] posted " + pctText + "% for " + playerName + " (" + world + ") [" + reason + "]")); } else { Log.LogWarning((object)$"[EilifMap] post HTTP {(int)val.StatusCode} {val.ReasonPhrase} for {playerName}"); } } finally { ((IDisposable)val)?.Dispose(); } } finally { ((IDisposable)req)?.Dispose(); } } catch (Exception ex) { Log.LogWarning((object)("[EilifMap] post failed: " + ex.Message)); } finally { Interlocked.Exchange(ref _postInFlight, 0); } } internal static void PostJson(string json, string tag, string successLine) { string url = IngestUrl; if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(json)) { return; } string token = IngestToken; try { Task.Run(() => SendAsync(url, token, json, tag, successLine)); } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)(tag + " could not queue post: " + ex.Message)); } } } private static async Task SendAsync(string url, string token, string json, string tag, string successLine) { try { HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, url); try { req.Content = (HttpContent)new StringContent(json, Encoding.UTF8, "application/json"); if (!string.IsNullOrEmpty(token)) { ((HttpHeaders)req.Headers).TryAddWithoutValidation("Authorization", "Bearer " + token); } HttpResponseMessage val = await Http.SendAsync(req).ConfigureAwait(continueOnCapturedContext: false); try { if (val.IsSuccessStatusCode) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)successLine); } } else { ManualLogSource log2 = Log; if (log2 != null) { log2.LogWarning((object)$"{tag} post HTTP {(int)val.StatusCode} {val.ReasonPhrase}"); } } } finally { ((IDisposable)val)?.Dispose(); } } finally { ((IDisposable)req)?.Dispose(); } } catch (Exception ex) { ManualLogSource log3 = Log; if (log3 != null) { log3.LogWarning((object)(tag + " post failed: " + ex.Message)); } } } internal static string JsonStr(string s) { StringBuilder stringBuilder = new StringBuilder(s.Length + 2); stringBuilder.Append('"'); foreach (char c in s) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { stringBuilder.Append(c); } } stringBuilder.Append('"'); return stringBuilder.ToString(); } } [HarmonyPatch(typeof(Game), "Logout")] internal static class Patch_GameLogout { private static void Prefix() { try { EilifMapTrackerPlugin.Instance?.ComputeAndPost("logout"); } catch (Exception ex) { ManualLogSource log = EilifMapTrackerPlugin.Log; if (log != null) { log.LogWarning((object)("[EilifMap] logout hook failed: " + ex.Message)); } } } } }