using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Collections; using UnityEngine; using UnityEngine.Rendering; using ValheimEventClips.Core; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ValheimEventClips")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.8.1.0")] [assembly: AssemblyInformationalVersion("0.8.1")] [assembly: AssemblyProduct("ValheimEventClips")] [assembly: AssemblyTitle("ValheimEventClips")] [assembly: AssemblyVersion("0.8.1.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 ValheimEventClips { internal enum BossNameMode { KillCredit, FinalBlow, Both } internal static class BossAttribution { private sealed class Context { internal string Enemy; internal string Name; } private const string RpcName = "ValheimMoments_BossFinalBlow_v1"; private static readonly FieldInfo LastHit = typeof(Character).GetField("m_lastHit", BindingFlags.Instance | BindingFlags.NonPublic); [ThreadStatic] private static Context current; private static ZRoutedRpc registered; private static readonly AttributionInbox inbox = new AttributionInbox(); private static readonly Stopwatch clock = Stopwatch.StartNew(); internal static Action OnDiagnostic; internal static string Resolve(HitData hit, out string reason) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0075: 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) if (hit == null) { reason = "no recorded damage"; return null; } Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if ((Object)(object)val != (Object)null) { reason = "resolved player object"; return val.GetPlayerName(); } if ((Object)(object)attacker == (Object)null && !((ZDOID)(ref hit.m_attacker)).IsNone() && (Object)(object)ZNet.instance != (Object)null) { foreach (PlayerInfo player in ZNet.instance.GetPlayerList()) { if (player.m_characterID == hit.m_attacker) { reason = "resolved exact player network ID"; return player.m_name; } } } reason = "hit=" + ((object)Unsafe.As(ref hit.m_hitType)/*cast due to .constrained prefix*/).ToString() + ", " + (((Object)(object)attacker != (Object)null) ? "non-player attacker" : (((ZDOID)(ref hit.m_attacker)).IsNone() ? "no attacker ID" : "attacker ID not in player list")); return null; } internal static void Install(Harmony harmony) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown //IL_004d: Expected O, but got Unknown //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Expected O, but got Unknown //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Expected O, but got Unknown harmony.Patch((MethodBase)AccessTools.Method(typeof(Character), "OnDeath", Type.EmptyTypes, (Type[])null), new HarmonyMethod(typeof(BossAttribution), "BeforeDeath", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(BossAttribution), "AfterDeath", (Type[])null), (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeof(Game), "RegisterKill", new Type[6] { typeof(long), typeof(string), typeof(int), typeof(KillModifiers), typeof(int), typeof(bool) }, (Type[])null), new HarmonyMethod(typeof(BossAttribution), "BeforeSendCredit", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); foreach (ConstructorInfo declaredConstructor in AccessTools.GetDeclaredConstructors(typeof(ZRoutedRpc), (bool?)null)) { harmony.Patch((MethodBase)declaredConstructor, (HarmonyMethod)null, new HarmonyMethod(typeof(BossAttribution), "AfterRouterCreated", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } EnsureRegistered(ZRoutedRpc.instance); } private static void AfterRouterCreated(ZRoutedRpc __instance) { EnsureRegistered(__instance); } private static void EnsureRegistered(ZRoutedRpc router) { try { if (router != null && registered != router) { router.Register("ValheimMoments_BossFinalBlow_v1", (Action)Receive); registered = router; inbox.Clear(); } } catch { } } private static void Receive(long sender, string enemy, string name) { inbox.Add(sender, enemy, name, clock.Elapsed.TotalSeconds); } private static void BeforeDeath(Character __instance, out Context __state) { __state = current; current = null; try { if (!__instance.IsBoss() || !__instance.IsOwner()) { return; } object? obj = LastHit?.GetValue(__instance); string reason; string name = Resolve((HitData)((obj is HitData) ? obj : null), out reason); current = new Context { Enemy = __instance.m_name, Name = name }; try { OnDiagnostic?.Invoke(reason); } catch { } } catch { } } private static void AfterDeath(Context __state) { current = __state; } private static void BeforeSendCredit(long playerPeerID, string enemyName, int bossNumber) { try { if (bossNumber > 0 && current != null && !(current.Enemy != enemyName)) { ZRoutedRpc.instance.InvokeRoutedRPC(playerPeerID, "ValheimMoments_BossFinalBlow_v1", new object[2] { enemyName, current.Name ?? "" }); } } catch { } } internal static string Take(long sender, string enemy) { if (sender == 0L && current != null && current.Enemy == enemy) { return current.Name; } string text = inbox.Take(sender, enemy, clock.Elapsed.TotalSeconds); try { OnDiagnostic?.Invoke((text == null) ? "owner metadata missing or expired" : ((text.Length == 0) ? "owner reported no player attacker" : "received owner attribution")); } catch { } return text; } internal static void Clear() { current = null; inbox.Clear(); OnDiagnostic = null; } } internal sealed class AttributionInbox { private sealed class Entry { internal long Sender; internal string Enemy; internal string Name; internal double Time; } private readonly List entries = new List(); internal void Add(long sender, string enemy, string name, double now) { if (!string.IsNullOrEmpty(enemy) && enemy.Length <= 256 && name != null && name.Length <= 256) { entries.RemoveAll((Entry e) => now - e.Time > 5.0 || (e.Sender == sender && e.Enemy == enemy)); if (entries.Count >= 64) { entries.RemoveAt(0); } entries.Add(new Entry { Sender = sender, Enemy = enemy, Name = name, Time = now }); } } internal string Take(long sender, string enemy, double now) { entries.RemoveAll((Entry e) => now - e.Time > 5.0); int num = entries.FindIndex((Entry e) => e.Sender == sender && e.Enemy == enemy); if (num < 0) { return null; } string name = entries[num].Name; entries.RemoveAt(num); return name; } internal void Clear() { entries.Clear(); } } internal sealed class BossKill { internal string EnemyKey; internal string PlayerName; internal string FinalBlowName; internal int BossNumber; internal bool FirstKill; internal BossLoot Loot; } internal static class BossKillDetector { private sealed class State { internal PlayerProfile Profile; internal string EnemyKey; internal int BossNumber; internal float Count; internal string FinalBlowName; internal BossLoot Loot; } internal static Action OnKill; internal static Action OnLootKill; internal static Func ObserveOrdinary; internal static Action OnError; private const BindingFlags Fields = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo Stats = typeof(PlayerProfile).GetField("m_playerStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static bool TryCount(PlayerProfile profile, string key, out float count) { count = 0f; try { if (!(Stats?.GetValue(profile) is Array { Length: not 0 } array)) { return false; } object value = array.GetValue(0); if (!((value?.GetType().GetField("m_enemyStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))?.GetValue(value) is Dictionary[] array2) || array2.Length == 0 || array2[0] == null || string.IsNullOrEmpty(key)) { return false; } array2[0].TryGetValue(key, out count); return !float.IsNaN(count) && !float.IsInfinity(count) && count >= 0f; } catch { return false; } } internal static void Install(Harmony harmony) { //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Expected O, but got Unknown //IL_00cc: Expected O, but got Unknown MethodInfo method = typeof(Game).GetMethod("RPC_RegisterKill", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[6] { typeof(long), typeof(string), typeof(int), typeof(int), typeof(int), typeof(bool) }, null); if (method == null || method.ReturnType != typeof(void)) { throw new MissingMethodException("Expected Game.RPC_RegisterKill signature not found."); } harmony.Patch((MethodBase)method, new HarmonyMethod(typeof(BossKillDetector), "Prefix", (Type[])null), new HarmonyMethod(typeof(BossKillDetector), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void Prefix(Game __instance, long sender, string enemyName, int bossNumber, out State __state) { __state = null; try { if (bossNumber <= 0) { Func observeOrdinary = ObserveOrdinary; if (observeOrdinary == null || !observeOrdinary()) { return; } } string finalBlowName = ((bossNumber > 0) ? BossAttribution.Take(sender, enemyName) : null); BossLoot loot = BossLootDetector.Take(sender, enemyName); PlayerProfile playerProfile = __instance.GetPlayerProfile(); if (!TryCount(playerProfile, enemyName, out var count)) { ReportError(); return; } __state = new State { Profile = playerProfile, EnemyKey = enemyName, BossNumber = bossNumber, Count = count, FinalBlowName = finalBlowName, Loot = loot }; } catch { ReportError(); } } private static void Postfix(State __state) { if (__state == null) { return; } try { if (!TryCount(__state.Profile, __state.EnemyKey, out var count)) { ReportError(); } else if (!(count <= __state.Count)) { ((__state.BossNumber > 0) ? OnKill : OnLootKill)?.Invoke(new BossKill { EnemyKey = __state.EnemyKey, BossNumber = __state.BossNumber, PlayerName = __state.Profile.GetName(), FirstKill = (__state.Count == 0f), FinalBlowName = __state.FinalBlowName, Loot = __state.Loot }); } } catch { ReportError(); } } private static void ReportError() { try { OnError?.Invoke(); } catch { } } } internal sealed class LootItem { internal string Id; internal string Name; internal int Quantity; internal int Rank = -1; internal int Sockets; internal string Rarity = ""; internal string Color = ""; internal string Modifiers = ""; internal bool Unidentified; } internal sealed class BossLoot { internal readonly string Id = Guid.NewGuid().ToString("N"); internal readonly List Items = new List(); internal bool Observed; internal bool Incomplete; internal bool Pending; internal int Revision; internal void AddEpic(LootItem item) { if (item == null || string.IsNullOrEmpty(item.Name) || string.IsNullOrEmpty(item.Id) || item.Quantity <= 0 || Items.Count >= 64) { Incomplete = true; return; } Items.Add(item); Observed = true; } internal void Add(string id, string name, long quantity) { if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(name) || quantity <= 0) { Incomplete = true; return; } if (id.Length > 128 || name.Length > 256 || quantity > int.MaxValue) { Incomplete = true; return; } LootItem lootItem = Items.Find((LootItem i) => i.Id == id && i.Name == name); if (lootItem != null) { if (quantity > int.MaxValue - lootItem.Quantity) { Incomplete = true; } else { lootItem.Quantity += (int)quantity; } } else if (Items.Count < 64) { Items.Add(new LootItem { Id = id, Name = name, Quantity = (int)quantity }); } else { Incomplete = true; } } internal string Display(int maximum, bool quantity, Func localize, bool showRarity = true, bool showModifiers = true, bool showSockets = true, bool showUnidentified = true) { if (!Observed) { return "unavailable"; } if (Items.Count == 0) { if (!Incomplete) { return "No items generated."; } return "unavailable"; } List list = new List(); foreach (LootItem item in Items) { list.Add(new LootItem { Id = item.Id, Name = localize(item.Name), Quantity = item.Quantity, Rank = item.Rank, Rarity = localize(item.Rarity), Color = item.Color, Modifiers = localize(item.Modifiers), Sockets = item.Sockets, Unidentified = item.Unidentified }); } list.Sort(delegate(LootItem a, LootItem b) { int num2 = b.Rank.CompareTo(a.Rank); if (num2 != 0) { return num2; } num2 = StringComparer.OrdinalIgnoreCase.Compare(a.Name, b.Name); return (num2 == 0) ? StringComparer.Ordinal.Compare(a.Id, b.Id) : num2; }); maximum = Math.Max(1, Math.Min(20, maximum)); List list2 = new List(); for (int num = 0; num < Math.Min(maximum, list.Count); num++) { LootItem lootItem = list[num]; list2.Add("* " + ((showRarity && lootItem.Rank >= 0) ? (Marker(lootItem.Color) + " " + lootItem.Rarity + " ") : "") + lootItem.Name + (quantity ? (" ×" + lootItem.Quantity) : "") + ((showUnidentified && lootItem.Unidentified) ? " (unidentified)" : "")); if (!lootItem.Unidentified && showModifiers && !string.IsNullOrEmpty(lootItem.Modifiers)) { list2.Add(" " + lootItem.Modifiers.Replace("\n", "\n ")); } if (!lootItem.Unidentified && showSockets && lootItem.Sockets > 0) { list2.Add(" Sockets: " + lootItem.Sockets); } } if (list.Count > maximum) { list2.Add("+" + (list.Count - maximum) + " more item types"); } if (Incomplete) { list2.Add("Some loot details unavailable."); } if (Pending) { list2.Add("Additional Epic Loot details unavailable before upload."); } return string.Join("\n", list2); } private static string Marker(string color) { if (color == null || color.Length != 7 || color[0] != '#' || !int.TryParse(color.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { return "◆"; } int[] array = new int[7] { 4359668, 10181046, 15965202, 15022389, 4431943, 16635957, 15658734 }; string[] array2 = new string[7] { "\ud83d\udd35", "\ud83d\udfe3", "\ud83d\udfe0", "\ud83d\udd34", "\ud83d\udfe2", "\ud83d\udfe1", "⚪" }; int num = 0; int num2 = int.MaxValue; for (int i = 0; i < array.Length; i++) { int num3 = (result >> 16) - (array[i] >> 16); int num4 = ((result >> 8) & 0xFF) - ((array[i] >> 8) & 0xFF); int num5 = (result & 0xFF) - (array[i] & 0xFF); int num6 = num3 * num3 + num4 * num4 + num5 * num5; if (num6 < num2) { num2 = num6; num = i; } } return array2[num]; } internal string Encode() { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(2); binaryWriter.Write(Observed); binaryWriter.Write(Incomplete); binaryWriter.Write(Pending); binaryWriter.Write(Revision); binaryWriter.Write(Items.Count); foreach (LootItem item in Items) { binaryWriter.Write(item.Id); binaryWriter.Write(item.Name); binaryWriter.Write(item.Quantity); binaryWriter.Write(item.Rank); binaryWriter.Write(item.Rarity); binaryWriter.Write(item.Color); binaryWriter.Write(item.Modifiers); binaryWriter.Write(item.Sockets); binaryWriter.Write(item.Unidentified); } } return Convert.ToBase64String(memoryStream.ToArray()); } internal static BossLoot Decode(string encoded) { if (encoded == null || encoded.Length > 524288) { return null; } try { using MemoryStream memoryStream = new MemoryStream(Convert.FromBase64String(encoded)); using BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8); if (binaryReader.ReadInt32() != 2) { return null; } BossLoot bossLoot = new BossLoot { Observed = binaryReader.ReadBoolean(), Incomplete = binaryReader.ReadBoolean(), Pending = binaryReader.ReadBoolean(), Revision = binaryReader.ReadInt32() }; if (bossLoot.Revision < 0) { return null; } int num = binaryReader.ReadInt32(); if (num < 0 || num > 64) { return null; } for (int i = 0; i < num; i++) { string text = binaryReader.ReadString(); string text2 = binaryReader.ReadString(); int num2 = binaryReader.ReadInt32(); if (text.Length == 0 || text.Length > 128 || text2.Length == 0 || text2.Length > 256 || num2 <= 0) { return null; } LootItem lootItem = new LootItem { Id = text, Name = text2, Quantity = num2, Rank = binaryReader.ReadInt32(), Rarity = binaryReader.ReadString(), Color = binaryReader.ReadString(), Modifiers = binaryReader.ReadString(), Sockets = binaryReader.ReadInt32(), Unidentified = binaryReader.ReadBoolean() }; if (lootItem.Rank < -1 || lootItem.Rank > 100 || lootItem.Rarity.Length > 64 || lootItem.Color.Length > 16 || lootItem.Modifiers.Length > 1024 || lootItem.Sockets < 0 || lootItem.Sockets > 64) { return null; } bossLoot.Items.Add(lootItem); } return (memoryStream.Position == memoryStream.Length) ? bossLoot : null; } catch { return null; } } } internal sealed class LootInbox { private sealed class Entry { internal long Sender; internal string Enemy; internal string Token; internal double Time; internal bool Taken; internal bool Completed; internal BossLoot Loot = new BossLoot(); } private readonly List entries = new List(); private void Prune(double now) { entries.RemoveAll((Entry e) => now - e.Time > 30.0); } internal void Announce(long sender, string enemy, string token, double now) { if (string.IsNullOrEmpty(enemy) || enemy.Length > 256 || token == null || token.Length != 32 || !Guid.TryParseExact(token, "N", out var _)) { return; } Prune(now); if (!entries.Exists((Entry e) => e.Sender == sender && e.Token == token)) { if (entries.Count >= 64) { entries.RemoveAt(0); } entries.Add(new Entry { Sender = sender, Enemy = enemy, Token = token, Time = now }); } } internal BossLoot Take(long sender, string enemy, double now) { Prune(now); Entry entry = entries.FindLast((Entry e) => !e.Taken && e.Sender == sender && e.Enemy == enemy && now - e.Time <= 5.0); if (entry == null) { return null; } entry.Taken = true; return entry.Loot; } internal void Complete(long sender, string token, string payload, double now) { Prune(now); Entry entry = entries.Find((Entry e) => e.Sender == sender && e.Token == token); if (entry != null) { BossLoot bossLoot = BossLoot.Decode(payload); if (bossLoot != null && (!entry.Completed || bossLoot.Revision > entry.Loot.Revision)) { entry.Loot.Items.Clear(); entry.Loot.Items.AddRange(bossLoot.Items); entry.Loot.Observed = bossLoot.Observed; entry.Loot.Incomplete = bossLoot.Incomplete; entry.Loot.Pending = bossLoot.Pending; entry.Loot.Revision = bossLoot.Revision; entry.Completed = true; } } } internal void Clear() { entries.Clear(); } } internal static class BossLootDetector { private sealed class Context { internal Character Character; internal BossLoot Loot = new BossLoot(); internal HashSet Recipients = new HashSet(); internal HashSet EpicItems = new HashSet(); internal int PendingRagdolls; } private const string CreditRpc = "ValheimMoments_BossLootCredit_v2"; private const string ResultRpc = "ValheimMoments_BossLootResult_v2"; private static readonly FieldInfo DropCharacter = typeof(CharacterDrop).GetField("m_character", BindingFlags.Instance | BindingFlags.NonPublic); [ThreadStatic] private static Context current; private static readonly LootInbox inbox = new LootInbox(); private static readonly Stopwatch clock = Stopwatch.StartNew(); private static ZRoutedRpc registered; private static bool enabled; private static ConditionalWeakTable ragdolls = new ConditionalWeakTable(); internal static Action OnObserved; internal static Action OnError; internal static void EnableEpic(Harmony harmony) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_007f: Expected O, but got Unknown harmony.Patch((MethodBase)AccessTools.Method(typeof(Ragdoll), "Setup", (Type[])null, (Type[])null), new HarmonyMethod(typeof(BossLootDetector), "BeforeRagdollSetup", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeof(Ragdoll), "SpawnLoot", (Type[])null, (Type[])null), new HarmonyMethod(typeof(BossLootDetector), "BeforeRagdollLoot", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(BossLootDetector), "AfterRagdollLoot", (Type[])null), (HarmonyMethod)null); } private static void BeforeRagdollSetup(object __instance, CharacterDrop characterDrop) { try { if (current != null && DropCharacter.GetValue(characterDrop) == current.Character) { ragdolls.Remove(__instance); ragdolls.Add(__instance, current); current.PendingRagdolls++; current.Loot.Pending = true; } } catch { Error(); } } private static void BeforeRagdollLoot(object __instance, out Context __state) { __state = current; current = ((enabled && ragdolls.TryGetValue(__instance, out var value)) ? value : null); } private static void AfterRagdollLoot(object __instance, Context __state, Exception __exception) { Context context = current; current = __state; try { if (context != null) { ragdolls.Remove(__instance); context.PendingRagdolls = Math.Max(0, context.PendingRagdolls - 1); context.Loot.Pending = context.PendingRagdolls > 0; if (__exception != null) { context.Loot.Incomplete = true; } Publish(context); if (!context.Loot.Pending) { context.EpicItems.Clear(); } } } catch { Error(); } } internal static void RecordEpic(List objects) { if (!enabled || current == null || objects == null) { return; } foreach (GameObject @object in objects) { try { ItemData val2 = (((Object)(object)@object == (Object)null) ? null : @object.GetComponent()?.m_itemData); if (current.EpicItems.Count >= 256) { current.Loot.Incomplete = true; break; } if (val2 != null && current.EpicItems.Add(val2)) { current.Loot.AddEpic(EpicLootAdapter.Read(val2, ((Object)@object).name)); OnObserved?.Invoke(current.Loot.Items.Count); } } catch { current.Loot.Incomplete = true; Error(); } } } internal static void Install(Harmony harmony) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected O, but got Unknown //IL_006b: Expected O, but got Unknown //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Expected O, but got Unknown //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Expected O, but got Unknown //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_017e: Expected O, but got Unknown if (DropCharacter == null) { throw new MissingFieldException("CharacterDrop.m_character"); } enabled = true; harmony.Patch((MethodBase)AccessTools.Method(typeof(Character), "OnDeath", Type.EmptyTypes, (Type[])null), new HarmonyMethod(typeof(BossLootDetector), "BeforeDeath", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(BossLootDetector), "AfterDeath", (Type[])null), (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeof(CharacterDrop), "GenerateDropList", Type.EmptyTypes, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(BossLootDetector), "AfterRoll", (Type[])null) { priority = 0 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeof(Game), "RegisterKill", new Type[6] { typeof(long), typeof(string), typeof(int), typeof(KillModifiers), typeof(int), typeof(bool) }, (Type[])null), new HarmonyMethod(typeof(BossLootDetector), "BeforeCredit", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); foreach (ConstructorInfo declaredConstructor in AccessTools.GetDeclaredConstructors(typeof(ZRoutedRpc), (bool?)null)) { harmony.Patch((MethodBase)declaredConstructor, (HarmonyMethod)null, new HarmonyMethod(typeof(BossLootDetector), "Register", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } Register(ZRoutedRpc.instance); } private static void Register(ZRoutedRpc __instance) { try { if (__instance == null || registered == __instance) { return; } __instance.Register("ValheimMoments_BossLootCredit_v2", (Action)delegate(long sender, string enemy, string token) { if (enabled) { inbox.Announce(sender, enemy, token, clock.Elapsed.TotalSeconds); } }); __instance.Register("ValheimMoments_BossLootResult_v2", (Action)delegate(long sender, string token, string payload) { if (enabled) { inbox.Complete(sender, token, payload, clock.Elapsed.TotalSeconds); } }); registered = __instance; inbox.Clear(); } catch { Error(); } } private static void BeforeDeath(Character __instance, out Context __state) { __state = current; current = null; try { if (!enabled || !__instance.IsOwner()) { return; } if (!__instance.IsBoss()) { Func observeOrdinary = BossKillDetector.ObserveOrdinary; if (observeOrdinary == null || !observeOrdinary()) { return; } } current = new Context { Character = __instance }; } catch { Error(); } } private static void AfterDeath(Context __state, Exception __exception) { Context context = current; current = __state; try { if (context != null) { if (__exception != null) { context.Loot.Incomplete = true; } Publish(context); } } catch { Error(); } } private static void Publish(Context context) { context.Loot.Revision++; string text = context.Loot.Encode(); foreach (long recipient in context.Recipients) { try { ZRoutedRpc.instance.InvokeRoutedRPC(recipient, "ValheimMoments_BossLootResult_v2", new object[2] { context.Loot.Id, text }); } catch { Error(); } } } private static void BeforeCredit(long playerPeerID, string enemyName, int bossNumber) { try { if (current != null && !(current.Character.m_name != enemyName) && current.Recipients.Add(playerPeerID)) { ZRoutedRpc.instance.InvokeRoutedRPC(playerPeerID, "ValheimMoments_BossLootCredit_v2", new object[2] { enemyName, current.Loot.Id }); } } catch { Error(); } } internal static BossLoot Take(long sender, string enemy) { if (sender == 0L && current != null && current.Character.m_name == enemy) { return current.Loot; } return inbox.Take(sender, enemy, clock.Elapsed.TotalSeconds); } private static void AfterRoll(CharacterDrop __instance, List> __result) { try { if (current == null || DropCharacter.GetValue(__instance) != current.Character) { return; } if (__result == null) { current.Loot.Incomplete = true; return; } current.Loot.Observed = true; foreach (KeyValuePair item in __result) { if ((Object)(object)item.Key == (Object)null || item.Value <= 0) { current.Loot.Incomplete = true; continue; } ItemDrop component = item.Key.GetComponent(); if ((Object)(object)component == (Object)null || component.m_itemData?.m_shared == null) { current.Loot.Incomplete = true; } else { current.Loot.Add(((Object)item.Key).name, component.m_itemData.m_shared.m_name, (long)item.Value * (long)component.m_itemData.m_stack); } } OnObserved?.Invoke(current.Loot.Items.Count); } catch { if (current != null) { current.Loot.Incomplete = true; } Error(); } } private static void Error() { try { OnError?.Invoke(); } catch { } } internal static void Clear() { enabled = false; current = null; ragdolls = new ConditionalWeakTable(); inbox.Clear(); OnObserved = null; OnError = null; } } internal enum LootDecision { Accept, Wait, Reject } internal static class BossLootFilter { private static readonly Dictionary ranks = new Dictionary(StringComparer.OrdinalIgnoreCase); internal static void SetRarities(Type enumType) { ranks.Clear(); foreach (object value in Enum.GetValues(enumType)) { ranks[Enum.GetName(enumType, value)] = Convert.ToInt32(value); } } internal static bool TryRank(string name, out int rank) { rank = -1; if (string.Equals(name?.Trim(), "None", StringComparison.OrdinalIgnoreCase)) { return true; } return ranks.TryGetValue(name?.Trim() ?? "", out rank); } internal static LootDecision Decide(BossLoot loot, int minimum, bool deadlineReached) { if (minimum < 0) { return LootDecision.Accept; } if (loot != null) { foreach (LootItem item in loot.Items) { if (item.Rank >= minimum) { return LootDecision.Accept; } } } if (!deadlineReached && (loot == null || loot.Pending)) { return LootDecision.Wait; } return LootDecision.Reject; } internal static LootDecision Evaluate(BossLoot loot, bool enabled, bool firstKill, bool firstBypasses, string minimumName, bool deadlineReached, out string reason) { if (!enabled) { reason = "rarity filter disabled"; return LootDecision.Accept; } if (firstKill && firstBypasses) { reason = "first recorded boss kill bypasses rarity"; return LootDecision.Accept; } if (!TryRank(minimumName, out var rank)) { reason = "rarity name unsupported or Epic Loot unavailable"; return LootDecision.Reject; } LootDecision lootDecision = Decide(loot, rank, deadlineReached); reason = lootDecision switch { LootDecision.Wait => "awaiting boss loot", LootDecision.Accept => "observed loot meets threshold (or threshold is None)", _ => "no observed item met rarity threshold", }; return lootDecision; } } internal sealed class ClipRelay : IDisposable { private const string Rpc = "ValheimMoments_ClipRelay_v1"; private readonly Func allow; private readonly Func limit; private readonly Action> deliver; private readonly Action log; private readonly HashSet registered = new HashSet(); private readonly Dictionary nextOffer = new Dictionary(); private ZNet session; private ZRpc source; private ZRpc target; private ZRpc deliveryPeer; private RelayBuffer incoming; private byte[] outgoing; private Task preparation; private string offeredKind; private string offeredMessage; private string outgoingId; private int sent; private int acknowledged; private double now; private double incomingDeadline; private double outgoingDeadline; private double nextSend; private double nextTick; private bool delivering; private bool awaitingOffer; private bool waitingResult; private bool disposed; internal bool DeliveryPeerConnected { get { if (deliveryPeer != null && registered.Contains(deliveryPeer)) { return deliveryPeer.IsConnected(); } return false; } } internal ClipRelay(Func allow, Func limit, Action> deliver, Action log) { this.allow = allow; this.limit = limit; this.deliver = deliver; this.log = log; } internal void Tick(double time) { if (disposed) { return; } now = time; if (now < nextTick) { return; } nextTick = now + 0.05; ZNet instance = ZNet.instance; if (session != instance) { Reset(); session = instance; } if ((Object)(object)session == (Object)null) { return; } HashSet live = new HashSet(); foreach (ZNetPeer peer in session.GetPeers()) { if (peer.IsReady() && peer.m_rpc.IsConnected()) { live.Add(peer.m_rpc); if (registered.Add(peer.m_rpc)) { peer.m_rpc.Register("ValheimMoments_ClipRelay_v1", (Action)Receive); } } } registered.RemoveWhere((ZRpc rpc) => !live.Contains(rpc)); List list = new List(); foreach (KeyValuePair item in nextOffer) { if (!live.Contains(item.Key)) { list.Add(item.Key); } } foreach (ZRpc item2 in list) { nextOffer.Remove(item2); } if (source != null && (!live.Contains(source) || now > incomingDeadline)) { incoming = null; source = null; } if (target != null && (!live.Contains(target) || now > outgoingDeadline)) { EndOutgoing("Transfer ended or host unavailable; local clip retained."); } if (preparation != null && preparation.IsCompleted) { Task task = preparation; preparation = null; try { byte[] result = task.GetAwaiter().GetResult(); if (target != null) { outgoing = result; outgoingDeadline = now + 10.0; Send(target, "B|" + outgoingId + "|" + offeredKind + "|" + result.Length + "|" + RelayProtocol.Text(offeredMessage)); } } catch { EndOutgoing("Clip could not be read or exceeds 10 MiB; local copy retained."); } } if (outgoing == null || awaitingOffer || waitingResult || sent != acknowledged || !(now >= nextSend)) { return; } try { int val = outgoing.Length - sent; byte[] array = new byte[Math.Min(16384, val)]; int num = array.Length; if (num == 0) { EndOutgoing("Clip could not be read; local copy retained."); return; } Buffer.BlockCopy(outgoing, sent, array, 0, num); int num2 = sent; sent += num; nextSend = now + 0.05; Send(target, "C|" + outgoingId + "|" + num2 + "|" + Convert.ToBase64String(array)); } catch { EndOutgoing("Clip transfer failed; local copy retained."); } } internal bool Offer(ZNet capturedSession, string file, string kind, string message) { if (disposed || (Object)(object)session == (Object)null || session != capturedSession || session.IsServer() || outgoing != null || preparation != null || target != null) { return false; } ZNetPeer serverPeer = session.GetServerPeer(); if (serverPeer == null || !registered.Contains(serverPeer.m_rpc)) { return false; } try { target = serverPeer.m_rpc; outgoingId = Guid.NewGuid().ToString("N"); offeredKind = kind; offeredMessage = message; sent = (acknowledged = 0); awaitingOffer = true; waitingResult = false; outgoingDeadline = now + 10.0; preparation = Task.Run(delegate { using FileStream fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); if (fileStream.Length < 20 || fileStream.Length > 10485760) { throw new IOException("Clip size outside relay limit"); } byte[] array = new byte[(int)fileStream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = fileStream.Read(array, i, array.Length - i); if (num == 0) { throw new EndOfStreamException(); } } return array; }); log("Offered clip to host; client webhook and bot-name settings are ignored."); return true; } catch { EndOutgoing("Could not offer clip to host; local copy retained."); return false; } } private void Receive(ZRpc rpc, string packet) { if (disposed || (Object)(object)session == (Object)null || session != ZNet.instance || !registered.Contains(rpc) || !rpc.IsConnected() || packet == null || packet.Length > 24000) { return; } try { string[] array = packet.Split('|'); if (array.Length >= 2 && RelayProtocol.ValidId(array[1])) { if (session.IsServer()) { ReceiveHost(rpc, array); } else if (target == rpc && array[1] == outgoingId) { ReceiveClient(array); } } } catch { log("Invalid relay data ignored."); } } private void ReceiveHost(ZRpc rpc, string[] p) { if (p[0] == "B" && p.Length == 5) { if (nextOffer.TryGetValue(rpc, out var value) && now < value) { Send(rpc, "R|" + p[1] + "|0"); return; } nextOffer[rpc] = now + 15.0; if (source != null || delivering || !RelayProtocol.ValidKind(p[2]) || !allow(p[2]) || !int.TryParse(p[3], out var result)) { Send(rpc, "R|" + p[1] + "|0"); return; } string message = RelayProtocol.ReadText(p[4]); try { incoming = new RelayBuffer(p[1], p[2], message, result, limit()); } catch { Send(rpc, "R|" + p[1] + "|0"); return; } source = rpc; incomingDeadline = now + 120.0; Send(rpc, "A|" + p[1] + "|0"); } else { if (!(p[0] == "C") || p.Length != 4 || source != rpc || incoming == null || !(incoming.Id == p[1])) { return; } if (!allow(incoming.Kind) || !int.TryParse(p[2], out var result2) || !incoming.Add(result2, Convert.FromBase64String(p[3]))) { Send(rpc, "R|" + p[1] + "|0"); incoming = null; source = null; return; } if (!incoming.Complete) { Send(rpc, "A|" + p[1] + "|" + incoming.Received); return; } RelayBuffer complete = incoming; incoming = null; source = null; if (complete.ValidWebP()) { delivering = true; deliveryPeer = rpc; ZNet origin = session; string arg = "Connected player"; foreach (ZNetPeer peer in session.GetPeers()) { if (peer.m_rpc == rpc) { arg = peer.m_playerName; break; } } Send(rpc, "A|" + p[1] + "|" + complete.Received); try { deliver(complete, arg, delegate(bool success) { delivering = false; deliveryPeer = null; if (!disposed && origin == session && registered.Contains(rpc)) { Send(rpc, "R|" + complete.Id + "|" + (success ? "1" : "0")); } }); return; } catch { delivering = false; deliveryPeer = null; Send(rpc, "R|" + complete.Id + "|0"); return; } } Send(rpc, "R|" + p[1] + "|0"); } } private void ReceiveClient(string[] p) { if (outgoing == null || p.Length != 3) { return; } int result; if (p[0] == "R") { EndOutgoing((p[2] == "1") ? "Host uploaded clip to Discord; local copy retained." : "Host declined or could not deliver clip; local copy retained."); } else if (!(p[0] != "A") && int.TryParse(p[2], out result) && result == sent) { acknowledged = result; awaitingOffer = false; if (sent == outgoing.Length) { waitingResult = true; outgoingDeadline = now + 90.0; } else { outgoingDeadline = now + 15.0; } } } private static void Send(ZRpc rpc, string packet) { rpc.Invoke("ValheimMoments_ClipRelay_v1", new object[1] { packet }); } internal void StopSending() { if (target != null) { EndOutgoing("Client relay disabled; local clip retained."); } } private void EndOutgoing(string reason) { outgoing = null; target = null; outgoingId = null; log(reason); } private void Reset() { if (target != null) { EndOutgoing("Session changed; local clip retained."); } incoming = null; source = null; registered.Clear(); nextOffer.Clear(); } public void Dispose() { disposed = true; Reset(); session = null; } } internal static class DeathCause { private static readonly FieldInfo LastHit = typeof(Character).GetField("m_lastHit", BindingFlags.Instance | BindingFlags.NonPublic); internal static string Read(Player player) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Invalid comparison between Unknown and I4 //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) try { object? obj = LastHit?.GetValue(player); HitData val = (HitData)((obj is HitData) ? obj : null); if (val == null) { return "unknown cause"; } if ((int)val.m_hitType == 1 || (int)val.m_hitType == 2 || (int)val.m_hitType == 0) { Character attacker = val.GetAttacker(); if ((Object)(object)attacker != (Object)null) { string hoverName = attacker.GetHoverName(); if (!string.IsNullOrWhiteSpace(hoverName)) { return hoverName; } } } return Label(val.m_hitType); } catch { return "unknown cause"; } } internal static string Label(HitType type) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected I4, but got Unknown return (type - 1) switch { 0 => "an enemy (attacker unavailable)", 1 => "another player (attacker unavailable)", 2 => "fall damage", 3 => "drowning", 4 => "burning", 5 => "freezing", 6 => "poison", 7 => "water damage", 8 => "smoke inhalation", 9 => "the edge of the world", 10 => "an impact", 11 => "a cart", 12 => "a falling tree", 13 => "self-inflicted damage", 14 => "structural damage", 15 => "a turret", 16 => "a boat", 17 => "a falling stalactite", 18 => "a catapult", 19 => "cinder fire", 20 => "the Ashlands ocean", 21 => "Ashlands lava", 22 => "an incinerator", 23 => "a drawbridge", _ => "unknown cause", }; } } internal static class DiscordRouting { internal static bool CanSubmit(object capturedSession, bool capturedAsHost, object currentSession, bool currentlyHost) { if (capturedSession != null && capturedAsHost && currentlyHost) { return capturedSession == currentSession; } return false; } internal static string Destination(string kind, string fallback, bool bossOverride, string boss, bool lootOverride, string loot, bool deathOverride, string death) { if (kind == "boss" && bossOverride) { return boss; } if (kind == "loot" && lootOverride) { return loot; } if (kind == "death" && deathOverride) { return death; } return fallback; } } internal sealed class DiscordOptions { internal string WebhookUrl; internal string Username; internal string Message = "Valheim moment"; internal bool SaveLocalCopy; internal long MaxUploadBytes = 10485760L; } internal sealed class UploadResult { internal bool Success; internal string Message; internal static UploadResult Fail(string message) { return new UploadResult { Message = message + " Local clip retained." }; } } internal static class DiscordWebhook { [DataContract] private sealed class Payload { [DataMember] public string username; [DataMember] public string content; [DataMember] public Mentions allowed_mentions = new Mentions(); } [DataContract] private sealed class Mentions { [DataMember] public string[] parse = new string[0]; } [DataContract] private sealed class RateLimit { [DataMember(IsRequired = true)] public double retry_after { get; set; } } internal static bool TryEndpoint(string value, out Uri endpoint) { endpoint = null; if (string.IsNullOrWhiteSpace(value) || !Uri.TryCreate(value.Trim(), UriKind.Absolute, out Uri result)) { return false; } if (result.Scheme != "https" || result.Port != 443 || result.UserInfo.Length != 0 || result.Fragment.Length != 0) { return false; } if (result.Host != "discord.com" && result.Host != "discordapp.com" && result.Host != "canary.discord.com" && result.Host != "ptb.discord.com") { return false; } if (!Regex.IsMatch(result.AbsolutePath, "^/api/(?:v[0-9]+/)?webhooks/[0-9]+/[A-Za-z0-9_-]+$")) { return false; } if (result.Query.Length != 0 && !Regex.IsMatch(result.Query, "^\\?thread_id=[0-9]+$")) { return false; } endpoint = new Uri(result.GetLeftPart(UriPartial.Path) + ((result.Query.Length == 0) ? "?" : (result.Query + "&")) + "wait=true"); return true; } internal static async Task UploadAsync(string file, DiscordOptions options, CancellationToken stop, HttpMessageHandler testHandler = null) { _ = 2; try { if (!TryEndpoint(options.WebhookUrl, out var endpoint)) { return UploadResult.Fail("Missing or invalid Discord webhook configuration."); } if (string.IsNullOrWhiteSpace(options.Username) || options.Username.Length > 80) { return UploadResult.Fail("Discord username must contain 1–80 characters."); } long length = new FileInfo(file).Length; if (length == 0L) { return UploadResult.Fail("Clip is empty."); } if (options.MaxUploadBytes < 1 || length > options.MaxUploadBytes) { return UploadResult.Fail("Clip exceeds the configured Discord upload limit (" + length + " bytes)."); } using (CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { stop })) { using HttpClient client = new HttpClient(testHandler ?? new HttpClientHandler { AllowAutoRedirect = false }); timeout.CancelAfter(TimeSpan.FromSeconds(75.0)); client.Timeout = Timeout.InfiniteTimeSpan; client.DefaultRequestHeaders.UserAgent.ParseAdd("ValheimMoments/0.8.1"); for (int attempt = 0; attempt < 3; attempt++) { using MultipartFormDataContent multipart = new MultipartFormDataContent(); using FileStream stream = File.OpenRead(file); using (MemoryStream memoryStream = new MemoryStream()) { new DataContractJsonSerializer(typeof(Payload)).WriteObject(memoryStream, new Payload { username = options.Username, content = options.Message }); ByteArrayContent byteArrayContent = new ByteArrayContent(memoryStream.ToArray()); byteArrayContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); multipart.Add(byteArrayContent, "payload_json"); } StreamContent streamContent = new StreamContent(stream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/webp"); multipart.Add(streamContent, "files[0]", "valheim-moment.webp"); using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = multipart }; using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeout.Token).ConfigureAwait(continueOnCapturedContext: false); int statusCode = (int)response.StatusCode; switch (statusCode) { case 200: stream.Dispose(); if (!options.SaveLocalCopy) { try { File.Delete(file); } catch { return new UploadResult { Success = true, Message = "Uploaded to Discord; local copy could not be removed." }; } } return new UploadResult { Success = true, Message = (options.SaveLocalCopy ? "Uploaded to Discord; local copy retained." : "Uploaded to Discord; local copy removed.") }; case 429: { double num = await RetrySeconds(response, timeout.Token).ConfigureAwait(continueOnCapturedContext: false); if (attempt == 2 || double.IsNaN(num) || double.IsInfinity(num) || num < 0.0 || num > 30.0) { return UploadResult.Fail("Discord rate limited the upload; retry budget exhausted or wait exceeds 30 seconds."); } await Task.Delay(TimeSpan.FromSeconds(Math.Max(0.05, num)), timeout.Token).ConfigureAwait(continueOnCapturedContext: false); break; } case 413: return UploadResult.Fail("Discord rejected the attachment as too large."); case 401: case 403: case 404: return UploadResult.Fail("Discord webhook is invalid, deleted, or not permitted."); default: return UploadResult.Fail("Discord rejected the upload (HTTP " + statusCode + ")."); } } } return UploadResult.Fail("Discord upload did not complete."); } catch (OperationCanceledException) { return UploadResult.Fail(stop.IsCancellationRequested ? "Discord upload cancelled." : "Discord upload timed out."); } catch { return UploadResult.Fail("Discord upload failed (network, file access, or service error)."); } } private static async Task RetrySeconds(HttpResponseMessage response, CancellationToken token) { if (response.Headers.RetryAfter != null) { if (response.Headers.RetryAfter.Delta.HasValue) { return response.Headers.RetryAfter.Delta.Value.TotalSeconds; } if (response.Headers.RetryAfter.Date.HasValue) { return Math.Max(0.0, (response.Headers.RetryAfter.Date.Value - DateTimeOffset.UtcNow).TotalSeconds); } } using Stream source = await response.Content.ReadAsStreamAsync().ConfigureAwait(continueOnCapturedContext: false); using MemoryStream json = new MemoryStream(); byte[] bytes = new byte[1024]; int num; while ((num = await source.ReadAsync(bytes, 0, bytes.Length, token).ConfigureAwait(continueOnCapturedContext: false)) > 0) { if (json.Length + num > 8192) { return double.NaN; } json.Write(bytes, 0, num); } json.Position = 0L; return ((RateLimit)new DataContractJsonSerializer(typeof(RateLimit)).ReadObject(json)).retry_after; } } internal static class EncoderClient { internal static string Encode(CaptureBuffer.Clip clip, string exe, string output, int width, int height, int quality, bool flip, CancellationToken cancellation) { ProcessStartInfo startInfo = new ProcessStartInfo(exe, "\"" + output + "\"") { UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = Path.GetDirectoryName(exe) }; Process process = new Process { StartInfo = startInfo }; try { process.Start(); try { process.PriorityClass = ProcessPriorityClass.BelowNormal; } catch { } Task task = process.StandardOutput.ReadToEndAsync(); Task task2 = process.StandardError.ReadToEndAsync(); using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(120.0)); using CancellationTokenSource cancellationTokenSource2 = CancellationTokenSource.CreateLinkedTokenSource(cancellation, cancellationTokenSource.Token); using (cancellationTokenSource2.Token.Register(delegate { try { if (!process.HasExited) { process.Kill(); } } catch { } })) { try { using (BinaryWriter binaryWriter = new BinaryWriter(process.StandardInput.BaseStream)) { binaryWriter.Write(826492246); binaryWriter.Write(width); binaryWriter.Write(height); binaryWriter.Write(clip.Count); binaryWriter.Write(quality); binaryWriter.Write(flip); double timestamp = clip.GetTimestamp(0); int num = 0; for (int num2 = 0; num2 < clip.Count; num2++) { cancellationTokenSource2.Token.ThrowIfCancellationRequested(); double num3 = ((num2 + 1 < clip.Count) ? clip.GetTimestamp(num2 + 1) : clip.EndTime); int num4 = Math.Max(num + 1, (int)Math.Round((num3 - timestamp) * 1000.0)); binaryWriter.Write(num4 - num); binaryWriter.Write(clip.GetPixels(num2)); num = num4; } } process.WaitForExit(); cancellationTokenSource2.Token.ThrowIfCancellationRequested(); string result = task2.GetAwaiter().GetResult(); if (process.ExitCode != 0) { throw new IOException("Encoder exit " + process.ExitCode + ": " + result.Trim()); } if (!File.Exists(output)) { throw new IOException("Encoder produced no output"); } return task.GetAwaiter().GetResult().Trim(); } finally { try { if (!process.HasExited) { process.Kill(); } } catch { } try { process.WaitForExit(5000); } catch { } try { if (File.Exists(output + ".partial")) { File.Delete(output + ".partial"); } } catch { } } } } finally { if (process != null) { ((IDisposable)process).Dispose(); } } } } internal static class EpicLootAdapter { private static MethodInfo display; private static MethodInfo rarity; private static MethodInfo rarityName; private static MethodInfo rarityColor; private static MethodInfo unidentified; private static MethodInfo getMagic; private static MethodInfo effectText; private static FieldInfo effects; private static FieldInfo sockets; private static FieldInfo magicRarity; private static readonly HashSet ranks = new HashSet(); private static readonly Regex tags = new Regex("<[^>]*>", RegexOptions.Compiled); internal static bool Install(Assembly assembly, Harmony harmony) { //IL_0285: Unknown result type (might be due to invalid IL or missing references) //IL_0292: Expected O, but got Unknown if (assembly == null) { return false; } Type type = assembly.GetType("EpicLoot.API", throwOnError: true); Type type2 = assembly.GetType("EpicLoot.MagicItem", throwOnError: true); Type type3 = assembly.GetType("EpicLoot.ItemRarity", throwOnError: true); Type type4 = assembly.GetType("EpicLoot.MagicItemEffect", throwOnError: true); display = Required(type, "GetItemDisplayName", typeof(ItemData)); rarity = Required(type, "TryGetRarity", typeof(ItemData), typeof(int).MakeByRefType()); rarityName = Required(type, "GetRarityDisplayNameByIndex", typeof(int)); rarityColor = Required(type, "GetRarityColorByIndex", typeof(int)); unidentified = Required(type, "IsUnidentified", typeof(ItemData)); getMagic = Required(assembly.GetType("EpicLoot.ItemDataExtensions", throwOnError: true), "GetMagicItem", typeof(ItemData)); effectText = Required(type2, "GetEffectText", type4, type3, typeof(bool), typeof(string)); effects = type2.GetField("Effects"); sockets = type2.GetField("SocketCount"); magicRarity = type2.GetField("Rarity"); if (effects == null || sockets == null || magicRarity == null) { throw new MissingFieldException("Epic Loot magic item layout changed"); } ranks.Clear(); foreach (object value in Enum.GetValues(type3)) { ranks.Add(Convert.ToInt32(value)); } BossLootFilter.SetRarities(type3); int num = 0; MethodInfo[] methods = assembly.GetType("EpicLoot.LootRoller", throwOnError: true).GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name == "RollLootTableAndSpawnObjects" && methodInfo.ReturnType == typeof(List)) { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(EpicLootAdapter), "AfterSpawn", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; } } if (num != 2) { throw new MissingMethodException("Expected two Epic Loot spawn-list overloads"); } BossLootDetector.EnableEpic(harmony); return true; } private static MethodInfo Required(Type type, string name, params Type[] args) { return type.GetMethod(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, args, null) ?? throw new MissingMethodException(type.FullName, name); } private static void AfterSpawn(List __result) { BossLootDetector.RecordEpic(__result); } internal static string Plain(string value, int max = 256) { string text = tags.Replace(value ?? "", "").Trim(); if (text.Length <= max) { return text; } return text.Substring(0, char.IsHighSurrogate(text[max - 1]) ? (max - 1) : max); } internal static LootItem Read(ItemData item, string id) { LootItem lootItem = new LootItem(); lootItem.Id = Plain(id, 128); lootItem.Name = Plain((string)display.Invoke(null, new object[1] { item })); lootItem.Quantity = item.m_stack; LootItem lootItem2 = lootItem; object[] array = new object[2] { item, 0 }; if ((bool)rarity.Invoke(null, array) && ranks.Contains((int)array[1])) { lootItem2.Rank = (int)array[1]; lootItem2.Rarity = Plain((string)rarityName.Invoke(null, new object[1] { lootItem2.Rank }), 64); lootItem2.Color = Plain((string)rarityColor.Invoke(null, new object[1] { lootItem2.Rank }), 16); } lootItem2.Unidentified = (bool)unidentified.Invoke(null, new object[1] { item }); if (!lootItem2.Unidentified) { object obj = getMagic.Invoke(null, new object[1] { item }); if (obj != null) { lootItem2.Sockets = Math.Max(0, Math.Min(64, (int)sockets.GetValue(obj))); List list = new List(); foreach (object item2 in (IEnumerable)effects.GetValue(obj)) { if (list.Count >= 12) { break; } list.Add(Plain((string)effectText.Invoke(null, new object[4] { item2, magicRarity.GetValue(obj), false, "" }), 160)); } lootItem2.Modifiers = Plain(string.Join("\n", list), 1024); } } return lootItem2; } } internal static class EventMessages { internal static string Heading(string text, int level) { if (string.IsNullOrWhiteSpace(text)) { return ""; } return new string('#', level) + " " + Regex.Replace(text.TrimStart(), "^#{1,6}[ \\t]+", ""); } internal static string FormatPost(string message) { string input = (message ?? "Valheim moment").TrimStart(); input = Regex.Replace(input, "^(Kill credit:|Final blow:)", "**$1**", RegexOptions.IgnoreCase | RegexOptions.Multiline); input = Heading(input, 1); if (input.Length <= 2000) { return input; } return input.Substring(0, char.IsHighSurrogate(input[1999]) ? 1999 : 2000); } internal static string Loot(string template, string enemy, string player, string loot, string itemCount = "") { return Boss((string.IsNullOrWhiteSpace(template) ? "Great loot from {enemy}!" : template).Replace("{enemy}", "{boss}"), string.IsNullOrWhiteSpace(enemy) ? "a creature" : enemy, player, BossNameMode.KillCredit, null, loot, itemCount); } internal static string Boss(string template, string boss, string player, BossNameMode? mode = null, string finalBlow = null, string loot = null, string itemCount = "") { string text = (string.IsNullOrWhiteSpace(template) ? "\ud83c\udfc6 {boss} defeated!" : template); bool flag = mode == BossNameMode.KillCredit || mode == BossNameMode.Both; bool flag2 = mode == BossNameMode.FinalBlow || mode == BossNameMode.Both; string text2 = (string.IsNullOrWhiteSpace(player) ? "A player" : player); string text3 = (string.IsNullOrWhiteSpace(finalBlow) ? "unavailable" : finalBlow); string text4 = text.Replace("{boss}", string.IsNullOrWhiteSpace(boss) ? "Boss" : boss).Replace("{player}", (mode == BossNameMode.FinalBlow) ? text3 : text2).Replace("{credit}", flag ? text2 : "") .Replace("{killer}", flag2 ? text3 : "") .Replace("{loot}", loot ?? "") .Replace("{item_count}", (loot == null) ? "" : itemCount); if (flag && !text.Contains("{credit}") && !text.Contains("{player}")) { text4 = text4 + "\nKill credit: " + text2; } if (flag2 && !text.Contains("{killer}") && (mode != BossNameMode.FinalBlow || !text.Contains("{player}"))) { text4 = text4 + "\nFinal blow: " + text3; } if (loot != null && !text.Contains("{loot}")) { text4 = text4 + "\n\n" + loot; } if (text4.Length <= 2000) { return text4; } return text4.Substring(0, char.IsHighSurrogate(text4[1999]) ? 1999 : 2000); } internal static string Death(string template, bool includeName, string nameOverride, string characterName, bool includeCause = false, string cause = "unknown cause") { string text = ((!includeName) ? "A player" : (string.IsNullOrWhiteSpace(nameOverride) ? characterName : nameOverride)); if (string.IsNullOrWhiteSpace(text)) { text = "A player"; } string text2 = (string.IsNullOrWhiteSpace(template) ? "\ud83d\udc80 {player} died!" : template); string text3 = (string.IsNullOrWhiteSpace(cause) ? "unknown cause" : cause); string text4 = text2.Replace("{cause}", includeCause ? text3 : "").Replace("{player}", text); if (includeCause && !text2.Contains("{cause}")) { text4 = text4 + "\nCause: " + text3; } if (text4.Length <= 2000) { return text4; } return text4[..(char.IsHighSurrogate(text4[1999]) ? 1999 : 2000)]; } } internal sealed class LootHighlights { private sealed class Candidate { internal BossKill Kill; internal double Deadline; } private readonly List pending = new List(); internal int Count => pending.Count; internal void Add(BossKill kill, double now, double wait) { if (kill == null || kill.BossNumber > 0) { return; } foreach (Candidate item in pending) { if (item.Kill == kill || (kill.Loot != null && item.Kill.Loot == kill.Loot)) { return; } } if (pending.Count == 64) { pending.RemoveAt(0); } pending.Add(new Candidate { Kill = kill, Deadline = now + (double.IsNaN(wait) ? 12.0 : Math.Max(0.0, Math.Min(25.0, wait))) }); } internal void Poll(double now, string minimumName, Action accept) { if (!BossLootFilter.TryRank(minimumName, out var rank)) { Clear(); return; } int num = 0; while (num < pending.Count) { Candidate candidate = pending[num]; bool flag = false; if (candidate.Kill.Loot != null) { foreach (LootItem item in candidate.Kill.Loot.Items) { if (item.Quantity > 0 && item.Rank >= rank) { flag = true; break; } } } if (flag && now <= candidate.Deadline) { pending.RemoveAt(num); accept(candidate.Kill); } else if (now >= candidate.Deadline || (candidate.Kill.Loot != null && candidate.Kill.Loot.Observed && !candidate.Kill.Loot.Pending)) { pending.RemoveAt(num); } else { num++; } } } internal void Clear() { pending.Clear(); } } internal static class PlayerDeathDetector { private sealed class DeathState { internal bool WasAlive; internal string Cause; } internal static Action OnLocalDeath; internal static Action OnError; internal static void Install(Harmony harmony) { //IL_005b: 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_007d: Expected O, but got Unknown //IL_007d: Expected O, but got Unknown MethodInfo method = typeof(Player).GetMethod("OnDeath", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null); if (method == null || method.ReturnType != typeof(void)) { throw new MissingMethodException("Expected Player.OnDeath() was not found."); } harmony.Patch((MethodBase)method, new HarmonyMethod(typeof(PlayerDeathDetector), "Prefix", (Type[])null), new HarmonyMethod(typeof(PlayerDeathDetector), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void Prefix(Player __instance, out DeathState __state) { __state = null; try { if ((Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer && !((Character)__instance).IsDead()) { __state = new DeathState { WasAlive = true, Cause = DeathCause.Read(__instance) }; } } catch { ReportError(); } } private static void Postfix(Player __instance, DeathState __state) { try { if (__state != null && __state.WasAlive && (Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer && ((Character)__instance).IsDead()) { OnLocalDeath?.Invoke(__instance, __state.Cause); } } catch { ReportError(); } } private static void ReportError() { try { OnError?.Invoke(); } catch { } } } [BepInPlugin("local.valheimeventclips", "Valheim Moments", "0.8.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { private sealed class ReadbackSlot { internal RenderTexture Target; internal NativeArray Pixels; internal AsyncGPUReadbackRequest Request; internal double Submitted; } private readonly Stopwatch clock = Stopwatch.StartNew(); private readonly Queue free = new Queue(3); private readonly Queue pending = new Queue(3); private readonly List allSlots = new List(3); private readonly CancellationTokenSource shutdown = new CancellationTokenSource(); private CaptureBuffer history; private CaptureBuffer.Clip encodingClip; private Task encoding; private Task upload; private ConfigEntry discordEnabled; private ConfigEntry uploadClips; private ConfigEntry saveLocalCopy; private ConfigEntry webhookUrl; private ConfigEntry discordUsername; private ConfigEntry useBossWebhook; private ConfigEntry useLootWebhook; private ConfigEntry useDeathWebhook; private ConfigEntry bossWebhook; private ConfigEntry lootWebhook; private ConfigEntry deathWebhook; private ZNet pendingSession; private ZNet activeSession; private ZNet uploadSession; private bool pendingAsHost; private bool activeAsHost; private string activeKind; private CancellationTokenSource uploadCancellation; private ClipRelay relay; private ConfigEntry relayEnabled; private Action relayCompletion; private string relayDirectory; private ConfigEntry uploadLimitMiB; private ConfigEntry manualTrigger; private ConfigEntry deathTrigger; private ConfigEntry deathEnabled; private ConfigEntry includePlayerName; private ConfigEntry includeCause; private ConfigEntry deathMessage; private ConfigEntry playerNameOverride; private Harmony deathHarmony; private Harmony bossHarmony; private ConfigEntry bossTrigger; private ConfigEntry bossEnabled; private ConfigEntry firstBossOnly; private ConfigEntry bossMessage; private double bossPostSeconds; private ConfigEntry bossNameMode; private Harmony attributionHarmony; private Harmony lootHarmony; private ConfigEntry showBossLoot; private ConfigEntry showLootQuantity; private ConfigEntry maxLootItems; private ConfigEntry lootHeader; private BossKill pendingBoss; private BossKill activeBoss; private ConfigEntry showRarity; private ConfigEntry showModifiers; private ConfigEntry showSockets; private ConfigEntry showUnidentified; private ConfigEntry lootWaitSeconds; private double lootDeadline; private Harmony epicHarmony; private ConfigEntry filterBossLoot; private ConfigEntry firstKillBypassesRarity; private ConfigEntry minimumBossRarity; private CaptureBuffer.Clip waitingForLoot; private readonly LootHighlights lootHighlights = new LootHighlights(); private ConfigEntry lootTrigger; private ConfigEntry lootEnabled; private ConfigEntry minimumLootRarity; private ConfigEntry highlightMessage; private ConfigEntry highlightWaitSeconds; private double lootPostSeconds; private bool epicReady; private ConfigEntry highlightQuantity; private ConfigEntry highlightRarity; private ConfigEntry highlightModifiers; private ConfigEntry highlightSockets; private ConfigEntry highlightUnidentified; private ConfigEntry highlightMaxItems; private ConfigEntry highlightHeader; private string pendingKind = "manual"; private string pendingMessage = "Valheim moment"; private string activeMessage; private RenderTexture screen; private byte[] scratch; private ConfigEntry captureEnabled; private ConfigEntry timing; private ConfigEntry flip; private ConfigEntry captureKey; private ConfigEntry toggleKey; private int width; private int height; private int fps; private int quality; private string encoderPath; private string outputDirectory; private string activeOutput; private bool initialized; private bool stopped; private bool paused; private bool historyCleared; private double nextCapture; private double lastReport; private double lastUpdate; private double submitMs; private double copyMs; private double latencyMs; private double maxSubmitMs; private double maxCopyMs; private double maxFrameMs; private double frameMs; private int submitted; private int received; private int skipped; private int errors; private int updateCount; private int consecutiveErrors; private void Start() { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown if (!initialized || stopped) { return; } try { Assembly assembly = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly2 in assemblies) { if (assembly2.GetName().Name == "EpicLoot") { assembly = assembly2; break; } } epicHarmony = new Harmony("local.valheimeventclips.epicloot"); epicReady = EpicLootAdapter.Install(assembly, epicHarmony); ((BaseUnityPlugin)this).Logger.LogInfo((object)(epicReady ? "[Loot] Optional Epic Loot adapter installed." : "[Loot] Epic Loot absent; vanilla summaries enabled.")); } catch (Exception ex) { try { Harmony obj = epicHarmony; if (obj != null) { obj.UnpatchSelf(); } } catch { } ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Loot] Epic Loot adapter unavailable: " + ex.GetType().Name + ". Vanilla summaries remain enabled.")); } } private T Setting(string key, T value, string help) { return ((BaseUnityPlugin)this).Config.Bind("Capture", key, value, help + " Restart Valheim after changing.").Value; } private void Awake() { //IL_091c: Unknown result type (might be due to invalid IL or missing references) //IL_0922: Invalid comparison between Unknown and I4 //IL_0d5b: Unknown result type (might be due to invalid IL or missing references) //IL_0bd2: Unknown result type (might be due to invalid IL or missing references) //IL_0bdc: Expected O, but got Unknown //IL_0c6e: Unknown result type (might be due to invalid IL or missing references) //IL_0c78: Expected O, but got Unknown //IL_0cd2: Unknown result type (might be due to invalid IL or missing references) //IL_0cdc: Expected O, but got Unknown //IL_0b29: Unknown result type (might be due to invalid IL or missing references) //IL_0b2e: Unknown result type (might be due to invalid IL or missing references) //IL_0b58: Unknown result type (might be due to invalid IL or missing references) //IL_0b62: Expected O, but got Unknown try { captureEnabled = ((BaseUnityPlugin)this).Config.Bind("Capture", "Enabled", true, "Enable recording. F9 toggles recording for baseline comparison."); captureKey = ((BaseUnityPlugin)this).Config.Bind("Capture", "ManualCaptureKey", (KeyCode)291, "Save recent gameplay plus post-event footage locally."); toggleKey = ((BaseUnityPlugin)this).Config.Bind("Capture", "ToggleCaptureKey", (KeyCode)290, "Pause/resume recording to compare game performance."); timing = ((BaseUnityPlugin)this).Config.Bind("Debug", "LogCaptureTiming", true, "Log aggregate CPU timing, readback latency and frame counts every 10 seconds."); flip = ((BaseUnityPlugin)this).Config.Bind("Capture", "FlipVertically", false, "Enable if the test WebP is upside down on your graphics backend."); discordEnabled = ((BaseUnityPlugin)this).Config.Bind("Discord", "Enabled", false, "Host/single-player only: enable Discord delivery. Remote clients send clips to the host and never use local webhook settings."); relayEnabled = ((BaseUnityPlugin)this).Config.Bind("Discord", "EnableClientRelay", true, "Host: accept clips from connected clients. Client: allow sending clips to the host. Both sides need this version. Host Discord.Enabled and event trigger switches also apply. Client copies are always retained."); uploadClips = ((BaseUnityPlugin)this).Config.Bind("Discord", "UploadClips", true, "Upload newly completed clips when Discord is enabled."); saveLocalCopy = ((BaseUnityPlugin)this).Config.Bind("Discord", "SaveLocalCopy", true, "Keep uploaded clips locally. Failed/skipped uploads always retain the clip."); webhookUrl = ((BaseUnityPlugin)this).Config.Bind("Discord", "WebhookURL", "", "Secret: enter locally, never share this config. HTTPS Discord webhook; optional thread_id query."); discordUsername = ((BaseUnityPlugin)this).Config.Bind("Discord", "Username", "Valheim Moments", "Host/single-player only: bot display name, 1–80 characters. Remote client values are ignored."); useBossWebhook = ((BaseUnityPlugin)this).Config.Bind("Discord", "UseBossKillWebhook", false, "Host only: route boss clips to BossKillWebhookURL; when off, use WebhookURL."); bossWebhook = ((BaseUnityPlugin)this).Config.Bind("Discord", "BossKillWebhookURL", "", "Host-only secret: optional boss destination. An enabled but invalid override keeps the clip locally; it does not silently change channels."); useLootWebhook = ((BaseUnityPlugin)this).Config.Bind("Discord", "UseGoodLootWebhook", false, "Host only: route ordinary-loot clips to GoodLootWebhookURL; when off, use WebhookURL."); lootWebhook = ((BaseUnityPlugin)this).Config.Bind("Discord", "GoodLootWebhookURL", "", "Host-only secret: optional ordinary-loot destination."); useDeathWebhook = ((BaseUnityPlugin)this).Config.Bind("Discord", "UsePlayerDeathWebhook", false, "Host only: route death clips to PlayerDeathWebhookURL; when off, use WebhookURL."); deathWebhook = ((BaseUnityPlugin)this).Config.Bind("Discord", "PlayerDeathWebhookURL", "", "Host-only secret: optional player-death destination."); uploadLimitMiB = ((BaseUnityPlugin)this).Config.Bind("Discord", "MaxUploadMiB", 10, "Per-file upload guard. Discord can impose its own limit. Allowed range 1–100."); manualTrigger = ((BaseUnityPlugin)this).Config.Bind("Triggers", "ManualCapture", true, "Enable the manual hotkey independently of automatic events."); deathTrigger = ((BaseUnityPlugin)this).Config.Bind("Triggers", "PlayerDeath", true, "Enable local player death captures."); deathEnabled = ((BaseUnityPlugin)this).Config.Bind("Player Death", "Enabled", true, "Enable this event. Triggers.PlayerDeath must also be enabled."); deathMessage = ((BaseUnityPlugin)this).Config.Bind("Player Death", "Message", "\ud83d\udc80 {player} died!", "Discord death message. Placeholders: {player}, {cause}. Cause appends automatically when enabled and no placeholder is present."); includeCause = ((BaseUnityPlugin)this).Config.Bind("Player Death", "IncludeCause", true, "Include the recorded attacker or environmental cause; unknown when unavailable."); includePlayerName = ((BaseUnityPlugin)this).Config.Bind("Player Death", "IncludePlayerName", true, "Replace {player} with the character name/override; otherwise use A player."); playerNameOverride = ((BaseUnityPlugin)this).Config.Bind("Player Death", "PlayerNameOverride", "", "Optional display name instead of the character name."); bossTrigger = ((BaseUnityPlugin)this).Config.Bind("Triggers", "BossKill", true, "Capture boss kills credited by Valheim to this character."); bossEnabled = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "Enabled", true, "Enable boss capture; Triggers.BossKill must also be enabled."); firstBossOnly = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "FirstKillOnly", false, "Only capture when this character has no previous kill of this boss in saved game statistics."); bossMessage = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "Message", "\ud83c\udfc6 {boss} defeated!", "Discord boss message. Supported placeholders: {boss}, {player}. Loot placeholders: {loot}, {item_count}."); bossPostSeconds = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "PostEventSeconds", 4.0, "Seconds to record after a credited boss kill, to show loot dropping. Restart after changing. Longer clips must fit Capture.MemoryBudgetMiB and the encoder's 256 MiB raw-frame limit.").Value; bossNameMode = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "PlayerNameMode", BossNameMode.Both, "KillCredit, FinalBlow or Both. Kill credit names this character; final blow names the last-hit player when available. Co-op final-blow sharing requires this version on the client owning the boss. Templates support {credit} and {killer}; selected names append when placeholders are absent."); showBossLoot = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "ShowLoot", true, "Show observed vanilla rolls and completed Epic Loot drops when its optional adapter is available."); showLootQuantity = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "ShowQuantity", true, "Show item quantities in the boss loot summary."); maxLootItems = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "MaxLootItemsShown", 5, "Maximum entries shown, 1-20; highest verified rarity first, then name. Distinct magic items stay separate."); lootHeader = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "LootHeader", "Generated loot:", "Message placeholders {loot} and {item_count}; count is displayed-data entries before the display limit (grouped vanilla types and individual Epic Loot items)."); showRarity = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "ShowRarity", true, "Show verified rarity names and approximate color emojis."); showModifiers = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "ShowItemModifiers", true, "Show Epic Loot's formatted modifiers for identified items."); showSockets = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "ShowItemSockets", true, "Show verified socket counts for identified items."); showUnidentified = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "ShowUnidentifiedStatus", true, "Label unidentified items. Hidden modifiers are never exposed."); lootWaitSeconds = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "LootWaitSeconds", 12.0, "Maximum seconds from boss kill to wait for delayed Epic Loot before upload, clamped 0-25. Recording duration remains PostEventSeconds."); filterBossLoot = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "OnlyCaptureIfLootMeetsRarity", false, "Only encode/save/upload a boss clip when at least one observed item meets MinimumLootRarity. Preserve kill footage while awaiting drops. Missing/unknown qualifying data skips the clip at the wait deadline."); minimumBossRarity = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "MinimumLootRarity", "Legendary", "None accepts all; otherwise an actual Epic Loot rarity name (0.14.2: Magic, Rare, Epic, Legendary, Mythic, Ancient). Used only when OnlyCaptureIfLootMeetsRarity=true. Unknown names or absent Epic Loot fail closed."); firstKillBypassesRarity = ((BaseUnityPlugin)this).Config.Bind("Boss Kill", "FirstKillBypassesRarity", true, "Always keep this character's first recorded kill of each boss regardless of MinimumLootRarity. Repeat kills still use the rarity filter. FirstKillOnly separately excludes all repeat kills."); lootTrigger = ((BaseUnityPlugin)this).Config.Bind("Triggers", "LootDrop", true, "Enable ordinary-creature loot highlights. Loot Capture.Enabled must also be enabled; bosses use Boss Kill rules exclusively."); lootEnabled = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "Enabled", true, "Capture qualifying Epic Loot drops from ordinary kills credited to this character. Requires the optional Epic Loot adapter. No pickup/crafting triggers."); minimumLootRarity = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "MinimumRarity", "Legendary", "Minimum observed rarity: Magic, Rare, Epic, Legendary, Mythic, Ancient. None accepts any observed item. Unknown names skip captures."); highlightMessage = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "Message", "Great loot from {enemy}!", "Placeholders: {enemy}, {player}, {loot}, {item_count}. Loot and kill credit append if omitted. Item display is configured independently in this section."); highlightQuantity = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "ShowQuantity", showLootQuantity.Value, "Show item quantities in ordinary-loot posts."); highlightRarity = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "ShowRarity", showRarity.Value, "Show rarity labels and colored markers; does not change MinimumRarity filtering."); highlightModifiers = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "ShowItemModifiers", showModifiers.Value, "Show identified items' Epic Loot modifier text in ordinary-loot posts."); highlightSockets = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "ShowItemSockets", showSockets.Value, "Show identified items' socket counts in ordinary-loot posts."); highlightUnidentified = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "ShowUnidentifiedStatus", showUnidentified.Value, "Label unidentified items. Hidden modifiers and sockets are never revealed."); highlightMaxItems = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "MaxLootItemsShown", maxLootItems.Value, "Maximum displayed entries, 1-20; highest rarity first. Display limits do not affect capture eligibility."); highlightHeader = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "LootHeader", lootHeader.Value, "Header above generated loot in ordinary-loot posts."); highlightWaitSeconds = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "LootWaitSeconds", 12.0, "Wait 0-25 seconds after credited kill for drops. Holds metadata only; up to 64 pending kills."); lootPostSeconds = ((BaseUnityPlugin)this).Config.Bind("Loot Capture", "PostEventSeconds", 4.0, "Seconds after observing qualifying loot. Uses rolling pre-event footage before the drop; long ragdoll delays may leave the kill outside the clip. Restart after changing.").Value; width = Setting("Width", 640, "Output pixel width, 16–1920."); height = Setting("Height", 360, "Output pixel height, 16–1080. Screen is stretched to this aspect ratio."); fps = Setting("FPS", 15, "Capture sampling rate, 1–30. Missed captures are skipped."); quality = Setting("WebPQuality", 80, "Lossy animated WebP quality, 1–100."); double num = Setting("PreEventSeconds", 5.0, "History duration."); double num2 = Setting("PostEventSeconds", 2.0, "Post-trigger duration."); int num3 = Setting("MemoryBudgetMiB", 192, "Maximum preallocated managed frame pool; excludes GPU and helper memory."); string directoryName = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location); relayDirectory = Path.Combine(directoryName, "RelayTemp"); relay = new ClipRelay(CanRelay, () => Math.Max(1, Math.Min(10, uploadLimitMiB.Value)) * 1048576, ReceiveRelayedClip, delegate(string message) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Relay] " + message)); }); encoderPath = Path.Combine(directoryName, "Encoder", "ValheimEventClips.Encoder.exe"); outputDirectory = Path.Combine(directoryName, "Clips"); if (Application.isBatchMode || (int)SystemInfo.graphicsDeviceType == 4) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Relay] Host delivery ready; graphics capture disabled on this headless server."); return; } if (!SystemInfo.supportsAsyncGPUReadback) { throw new NotSupportedException("This graphics backend does not support asynchronous GPU readback."); } if (!File.Exists(encoderPath)) { throw new FileNotFoundException("Bundled Encoder/ValheimEventClips.Encoder.exe is missing."); } if (width < 16 || width > 1920 || height < 16 || height > 1080 || fps < 1 || fps > 30 || quality < 1 || quality > 100 || num3 < 16 || num3 > 512) { throw new ArgumentOutOfRangeException("Capture configuration is outside prototype limits."); } double num4 = Math.Max(num2, Math.Max(bossPostSeconds, lootPostSeconds)); if (double.IsNaN(lootPostSeconds) || double.IsInfinity(lootPostSeconds) || lootPostSeconds < 0.0) { throw new ArgumentOutOfRangeException("Loot Capture.PostEventSeconds"); } if (double.IsNaN(bossPostSeconds) || double.IsInfinity(bossPostSeconds) || bossPostSeconds < 0.0) { throw new ArgumentOutOfRangeException("Boss Kill.PostEventSeconds"); } if ((double)((long)width * (long)height * 4) * (Math.Ceiling(num * (double)fps) + Math.Ceiling(num4 * (double)fps)) > 268435456.0) { throw new ArgumentOutOfRangeException("Clip exceeds the encoder's 256 MiB raw-frame limit."); } history = new CaptureBuffer(width, height, fps, num, num2, (long)num3 * 1024L * 1024, num4); scratch = new byte[checked(width * height * 4)]; for (int num5 = 0; num5 < 3; num5++) { ReadbackSlot readbackSlot = new ReadbackSlot(); allSlots.Add(readbackSlot); readbackSlot.Target = MakeTarget(width, height); readbackSlot.Pixels = new NativeArray(scratch.Length, (Allocator)4, (NativeArrayOptions)0); free.Enqueue(readbackSlot); } initialized = true; try { deathHarmony = new Harmony("local.valheimeventclips.death"); PlayerDeathDetector.OnLocalDeath = OnLocalDeath; PlayerDeathDetector.OnError = delegate { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Death] Could not inspect local death; gameplay was left unchanged."); }; PlayerDeathDetector.Install(deathHarmony); ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Death] Local player death detector installed."); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Death] Detector unavailable: " + ex.GetType().Name + ". Manual capture remains available.")); } try { bossHarmony = new Harmony("local.valheimeventclips.boss"); BossKillDetector.OnKill = OnBossKill; BossKillDetector.ObserveOrdinary = () => epicReady && lootTrigger.Value && lootEnabled.Value; BossKillDetector.OnLootKill = delegate(BossKill kill) { if (captureEnabled.Value && !paused) { lootHighlights.Add(kill, clock.Elapsed.TotalSeconds, highlightWaitSeconds.Value); } }; BossKillDetector.OnError = delegate { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Boss] Unable to verify character kill statistics; boss event skipped."); }; BossKillDetector.Install(bossHarmony); ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Boss] Local kill-credit detector installed."); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Boss] Detector unavailable: " + ex2.GetType().Name + ". Other captures remain available.")); } try { attributionHarmony = new Harmony("local.valheimeventclips.boss.attribution"); BossAttribution.OnDiagnostic = delegate(string reason) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Boss] Final-blow source: " + reason)); }; BossAttribution.Install(attributionHarmony); ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Boss] Final-blow attribution installed."); } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Boss] Final-blow attribution unavailable: " + ex3.GetType().Name)); } try { lootHarmony = new Harmony("local.valheimeventclips.boss.loot"); BossLootDetector.OnObserved = delegate(int count) { ((BaseUnityPlugin)this).Logger.LogDebug((object)("[Loot] Observed generated loot; item types=" + count)); }; BossLootDetector.OnError = delegate { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Loot] Some loot data unavailable; boss capture remains enabled."); }; BossLootDetector.Install(lootHarmony); ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Loot] Generated boss loot observer installed."); } catch (Exception ex4) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Loot] Observer unavailable: " + ex4.GetType().Name)); } ((BaseUnityPlugin)this).Logger.LogInfo((object)$"[Capture] Ready: Unity {Application.unityVersion}, {SystemInfo.graphicsDeviceType}, {width}x{height} at {fps} FPS; pool {(double)history.AllocatedPixelBytes / 1048576.0:F1} MiB. F10 saves; F9 pauses. Output: {outputDirectory}"); ((MonoBehaviour)this).StartCoroutine(CaptureLoop()); } catch (Exception ex5) { ((BaseUnityPlugin)this).Logger.LogError((object)("[Capture] Initialization failed: " + ex5.Message)); StopCapture(); } } private static RenderTexture MakeTarget(int w, int h) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0015: 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) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown RenderTexture val = new RenderTexture(w, h, 0, (RenderTextureFormat)0, (RenderTextureReadWrite)2) { name = "ValheimEventClips", antiAliasing = 1, useMipMap = false, filterMode = (FilterMode)1 }; if (!val.Create()) { Object.Destroy((Object)(object)val); throw new InvalidOperationException("RenderTexture creation failed"); } return val; } private void Update() { //IL_0240: Unknown result type (might be due to invalid IL or missing references) //IL_0340: Unknown result type (might be due to invalid IL or missing references) if (stopped) { return; } try { if (relay != null && !relayEnabled.Value) { relay.StopSending(); } relay?.Tick(clock.Elapsed.TotalSeconds); if (uploadCancellation != null && upload != null && !upload.IsCompleted && (!DiscordRouting.CanSubmit(uploadSession, capturedAsHost: true, ZNet.instance, (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) || (relayCompletion != null && (!relayEnabled.Value || !discordEnabled.Value || !uploadClips.Value || !relay.DeliveryPeerConnected)))) { uploadCancellation.Cancel(); } if (upload != null && upload.IsCompleted) { UploadResult result = upload.GetAwaiter().GetResult(); if (result.Success) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Discord] " + result.Message)); } else { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Discord] " + result.Message)); } upload = null; uploadCancellation?.Dispose(); uploadCancellation = null; uploadSession = null; Action action = relayCompletion; relayCompletion = null; action?.Invoke(result.Success); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Relay] Delivery update failed: " + ex.GetType().Name)); } if (!initialized) { return; } try { double totalSeconds = clock.Elapsed.TotalSeconds; if (lastUpdate > 0.0) { double num = (totalSeconds - lastUpdate) * 1000.0; frameMs += num; maxFrameMs = Math.Max(maxFrameMs, num); updateCount++; } lastUpdate = totalSeconds; if (Input.GetKeyDown(toggleKey.Value)) { paused = !paused; ((BaseUnityPlugin)this).Logger.LogInfo((object)(paused ? "[Capture] Paused for baseline comparison." : "[Capture] Recording resumed; allow 5 seconds to warm up.")); } DrainReadbacks(); bool flag = captureEnabled.Value && !paused; if (flag && epicReady && lootTrigger.Value && lootEnabled.Value) { lootHighlights.Poll(totalSeconds, minimumLootRarity.Value, OnLootHighlight); } else { lootHighlights.Clear(); } if (!flag) { if (!historyCleared && pending.Count == 0) { history.ClearHistory(); historyCleared = true; } } else { historyCleared = false; if (manualTrigger.Value && Application.isFocused && Input.GetKeyDown(captureKey.Value)) { Trigger("manual", "Valheim moment"); } } if (encoding != null && encoding.IsCompleted && (!encoding.IsCompletedSuccessfully || activeBoss?.Loot == null || !activeBoss.Loot.Pending || (activeBoss.BossNumber > 0 && !showBossLoot.Value) || totalSeconds >= lootDeadline)) { try { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[WebP] " + encoding.GetAwaiter().GetResult() + "; saved " + activeOutput)); StartUpload(activeOutput); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[WebP] Clip failed: " + ex2.Message)); } encodingClip.Release(); encodingClip = null; encoding = null; } if (flag && encoding == null) { CaptureBuffer.Clip clip = history.TryComplete((pending.Count == 0) ? totalSeconds : pending.Peek().Submitted); if (clip != null) { if (clip.Count == 0) { clip.Release(); ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Capture] No frames available; clip discarded."); } else if (pendingBoss != null && pendingBoss.BossNumber > 0 && filterBossLoot.Value) { waitingForLoot = clip; } else { StartEncoding(clip); } } } if (waitingForLoot != null) { string reason; LootDecision lootDecision = BossLootFilter.Evaluate(pendingBoss?.Loot, filterBossLoot.Value, pendingBoss != null && pendingBoss.FirstKill, firstKillBypassesRarity.Value, minimumBossRarity.Value, totalSeconds >= lootDeadline, out reason); if (lootDecision != LootDecision.Wait) { CaptureBuffer.Clip clip2 = waitingForLoot; waitingForLoot = null; if (lootDecision == LootDecision.Accept) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Loot] Boss clip accepted: " + reason)); StartEncoding(clip2); } else { clip2.Release(); pendingBoss = null; ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Loot] Boss clip skipped: " + reason)); } } } if (timing.Value && totalSeconds - lastReport >= 10.0) { ((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("[Capture] {0}: received={1}, submitted={2}, skipped={3}, errors={4}, buffer={5}; submit CPU avg/max={6:F2}/{7:F2}ms, copy CPU avg/max={8:F2}/{9:F2}ms, readback latency avg={10:F2}ms; game Update avg/max={11:F2}/{12:F2}ms; managed={13:F1}MiB; effective capture FPS={14:F2}", flag ? "recording" : "paused", received, submitted, skipped, errors, history.BufferedFrames, submitMs / (double)Math.Max(1, submitted), maxSubmitMs, copyMs / (double)Math.Max(1, received), maxCopyMs, latencyMs / (double)Math.Max(1, received + errors), frameMs / (double)Math.Max(1, updateCount), maxFrameMs, (double)GC.GetTotalMemory(forceFullCollection: false) / 1048576.0, (double)received / (totalSeconds - lastReport))); submitted = (received = (skipped = (errors = (updateCount = 0)))); submitMs = (copyMs = (latencyMs = (maxSubmitMs = (maxCopyMs = (frameMs = (maxFrameMs = 0.0)))))); lastReport = totalSeconds; } } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogError((object)("[Capture] Stopping after error: " + ex3.Message)); StopCapture(); } } private IEnumerator CaptureLoop() { WaitForEndOfFrame endOfFrame = new WaitForEndOfFrame(); while (!stopped) { yield return endOfFrame; if (captureEnabled.Value && !paused && Application.isFocused) { try { SubmitCapture(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("[Capture] Submission failed: " + ex.Message)); StopCapture(); } } } } private void SubmitCapture() { //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) double totalSeconds = clock.Elapsed.TotalSeconds; if (totalSeconds < nextCapture) { return; } nextCapture = (Math.Floor(totalSeconds * (double)fps) + 1.0) / (double)fps; if (free.Count == 0) { skipped++; } else { if (Screen.width < 16 || Screen.height < 16) { return; } if ((Object)(object)screen == (Object)null || ((Texture)screen).width != Screen.width || ((Texture)screen).height != Screen.height) { if (pending.Count != 0) { skipped++; return; } if ((long)Screen.width * (long)Screen.height > 33554432) { throw new InvalidOperationException("Screen exceeds prototype capture limit"); } ReleaseTarget(screen); screen = MakeTarget(Screen.width, Screen.height); } ReadbackSlot readbackSlot = free.Peek(); double totalMilliseconds = clock.Elapsed.TotalMilliseconds; ScreenCapture.CaptureScreenshotIntoRenderTexture(screen); Graphics.Blit((Texture)(object)screen, readbackSlot.Target); readbackSlot.Submitted = totalSeconds; readbackSlot.Request = AsyncGPUReadback.RequestIntoNativeArray(ref readbackSlot.Pixels, (Texture)(object)readbackSlot.Target, 0, (TextureFormat)4, (Action)null); free.Dequeue(); pending.Enqueue(readbackSlot); double num = clock.Elapsed.TotalMilliseconds - totalMilliseconds; submitMs += num; maxSubmitMs = Math.Max(maxSubmitMs, num); submitted++; } } private void DrainReadbacks() { while (pending.Count > 0 && ((AsyncGPUReadbackRequest)(ref pending.Peek().Request)).done) { ReadbackSlot readbackSlot = pending.Dequeue(); latencyMs += (clock.Elapsed.TotalSeconds - readbackSlot.Submitted) * 1000.0; if (((AsyncGPUReadbackRequest)(ref readbackSlot.Request)).hasError) { errors++; consecutiveErrors++; if (consecutiveErrors >= 8) { throw new InvalidOperationException("Repeated GPU readback failures; try another graphics backend."); } } else { consecutiveErrors = 0; double totalMilliseconds = clock.Elapsed.TotalMilliseconds; readbackSlot.Pixels.CopyTo(scratch); if (!history.AddFrame(scratch, readbackSlot.Submitted)) { skipped++; } double num = clock.Elapsed.TotalMilliseconds - totalMilliseconds; copyMs += num; maxCopyMs = Math.Max(maxCopyMs, num); received++; } free.Enqueue(readbackSlot); } } private void StartEncoding(CaptureBuffer.Clip clip) { encodingClip = clip; activeMessage = pendingMessage; activeKind = pendingKind; activeSession = pendingSession; activeAsHost = pendingAsHost; activeBoss = pendingBoss; pendingBoss = null; activeOutput = Path.Combine(outputDirectory, pendingKind + "-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff") + "-" + Guid.NewGuid().ToString("N").Substring(0, 6) + ".webp"); string destination = activeOutput; bool flipImage = flip.Value; int num = 0; for (int i = 0; i < clip.Count; i++) { if (clip.GetTimestamp(i) < clip.TriggerTime) { num++; } } ((BaseUnityPlugin)this).Logger.LogInfo((object)$"[WebP] Encoding {clip.Count} frames in background helper; pre-event={num}, post-event={clip.Count - num}."); encoding = Task.Run(() => EncoderClient.Encode(clip, encoderPath, destination, width, height, quality, flipImage, shutdown.Token)); } private static void ReleaseTarget(RenderTexture target) { if (!((Object)(object)target == (Object)null)) { target.Release(); Object.Destroy((Object)(object)target); } } private void StartUpload(string file) { ZNet instance = ZNet.instance; if ((Object)(object)instance != (Object)null && !instance.IsServer() && !activeAsHost && activeSession == instance) { string message = EventMessages.FormatPost((activeBoss == null) ? activeMessage : BossMessage(activeBoss)); if (!relayEnabled.Value || !relay.Offer(activeSession, file, activeKind, message)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Relay] Clip retained locally: relay disabled, unavailable, busy or clip exceeds 10 MiB."); } } else if (!DiscordRouting.CanSubmit(activeSession, activeAsHost, instance, (Object)(object)instance != (Object)null && instance.IsServer())) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Discord] Local clip retained: changed or ended sessions cannot submit old clips."); } else { if (!discordEnabled.Value || !uploadClips.Value) { return; } if (upload != null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Discord] Upload busy; new clip retained locally."); return; } DiscordOptions options = new DiscordOptions { WebhookUrl = DiscordRouting.Destination(activeKind, webhookUrl.Value, useBossWebhook.Value, bossWebhook.Value, useLootWebhook.Value, lootWebhook.Value, useDeathWebhook.Value, deathWebhook.Value), Username = discordUsername.Value, Message = EventMessages.FormatPost((activeBoss == null) ? activeMessage : BossMessage(activeBoss)), SaveLocalCopy = saveLocalCopy.Value, MaxUploadBytes = (long)Math.Max(1, Math.Min(100, uploadLimitMiB.Value)) * 1048576L }; ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Discord] Starting background upload."); uploadSession = instance; uploadCancellation = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { shutdown.Token }); CancellationToken token = uploadCancellation.Token; upload = Task.Run(() => DiscordWebhook.UploadAsync(file, options, token)); } } private void OnDestroy() { StopCapture(); } private string Destination(string kind) { return DiscordRouting.Destination(kind, webhookUrl.Value, useBossWebhook.Value, bossWebhook.Value, useLootWebhook.Value, lootWebhook.Value, useDeathWebhook.Value, deathWebhook.Value); } private bool CanRelay(string kind) { if (!relayEnabled.Value || !discordEnabled.Value || !uploadClips.Value || upload != null) { return false; } if (kind == "manual" && !manualTrigger.Value) { return false; } if (kind == "boss" && (!bossTrigger.Value || !bossEnabled.Value)) { return false; } if (kind == "loot" && (!lootTrigger.Value || !lootEnabled.Value)) { return false; } if (kind == "death" && (!deathTrigger.Value || !deathEnabled.Value)) { return false; } Uri endpoint; if (RelayProtocol.ValidKind(kind)) { return DiscordWebhook.TryEndpoint(Destination(kind), out endpoint); } return false; } private void ReceiveRelayedClip(RelayBuffer clip, string recorder, Action completion) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || !CanRelay(clip.Kind)) { completion(obj: false); return; } string text = (recorder ?? "Connected player").Replace("\r", " ").Replace("\n", " ").Replace("*", "") .Replace("`", ""); if (text.Length > 80) { text = text.Substring(0, 80); } string message = clip.Message; int num = message.IndexOf('\n'); if (num < 0) { num = message.Length; } message = message.Insert(num, "\n**Recorded by:** " + text); DiscordOptions options = new DiscordOptions { WebhookUrl = Destination(clip.Kind), Username = discordUsername.Value, Message = EventMessages.FormatPost(message), SaveLocalCopy = true, MaxUploadBytes = (long)Math.Max(1, Math.Min(10, uploadLimitMiB.Value)) * 1048576L }; uploadSession = instance; relayCompletion = completion; uploadCancellation = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { shutdown.Token }); CancellationToken token = uploadCancellation.Token; string file = Path.Combine(relayDirectory, Guid.NewGuid().ToString("N") + ".webp"); upload = Task.Run(async delegate { _ = 1; try { token.ThrowIfCancellationRequested(); Directory.CreateDirectory(relayDirectory); using (FileStream stream = new FileStream(file, FileMode.CreateNew, FileAccess.Write, FileShare.None, 16384, useAsync: true)) { await stream.WriteAsync(clip.Bytes, 0, clip.Bytes.Length, token).ConfigureAwait(continueOnCapturedContext: false); } UploadResult uploadResult = await DiscordWebhook.UploadAsync(file, options, token).ConfigureAwait(continueOnCapturedContext: false); return new UploadResult { Success = uploadResult.Success, Message = (uploadResult.Success ? "Client clip uploaded; client retains original." : "Client clip delivery failed; client retains original.") }; } catch { return new UploadResult { Success = false, Message = "Client clip delivery cancelled or failed; client retains original." }; } finally { try { if (File.Exists(file)) { File.Delete(file); } } catch { } } }); } private void OnLootHighlight(BossKill kill) { if (Trigger("loot", "", lootPostSeconds)) { pendingBoss = kill; lootDeadline = clock.Elapsed.TotalSeconds + Math.Max(0.0, Math.Min(25.0, double.IsNaN(highlightWaitSeconds.Value) ? 12.0 : highlightWaitSeconds.Value)); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Loot] Qualifying ordinary-creature drop captured; minimum=" + minimumLootRarity.Value)); } } private void OnBossKill(BossKill kill) { if (bossTrigger.Value && bossEnabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Boss] Kill credited; boss number=" + kill.BossNumber + ", first kill=" + kill.FirstKill)); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Boss] Capture rules: FirstKillOnly=" + firstBossOnly.Value + ", rarity filter=" + filterBossLoot.Value + ", minimum=" + minimumBossRarity.Value + ", first-kill bypass=" + firstKillBypassesRarity.Value)); if (firstBossOnly.Value && !kill.FirstKill) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Boss] Repeat kill skipped by FirstKillOnly."); } else if (Trigger("boss", "", bossPostSeconds)) { pendingBoss = kill; double value = lootWaitSeconds.Value; lootDeadline = clock.Elapsed.TotalSeconds + (double.IsNaN(value) ? 12.0 : Math.Max(0.0, Math.Min(25.0, value))); } } } private string BossMessage(BossKill kill) { try { bool num = kill.BossNumber <= 0; string loot = null; if (num) { loot = EventMessages.Heading(highlightHeader.Value, 2) + "\n" + ((kill.Loot == null) ? "unavailable" : kill.Loot.Display(highlightMaxItems.Value, highlightQuantity.Value, (string text) => Localization.instance.Localize(text), highlightRarity.Value, highlightModifiers.Value, highlightSockets.Value, highlightUnidentified.Value)); } else if (showBossLoot.Value) { loot = EventMessages.Heading(lootHeader.Value, 2) + "\n" + ((kill.Loot == null) ? "unavailable" : kill.Loot.Display(maxLootItems.Value, showLootQuantity.Value, (string text) => Localization.instance.Localize(text), showRarity.Value, showModifiers.Value, showSockets.Value, showUnidentified.Value)); } string itemCount = ((kill.Loot != null && kill.Loot.Observed) ? kill.Loot.Items.Count.ToString() : "unknown"); if (num) { return EventMessages.Loot(highlightMessage.Value, Localization.instance.Localize(kill.EnemyKey), kill.PlayerName, loot, itemCount); } return EventMessages.Boss(bossMessage.Value, Localization.instance.Localize(kill.EnemyKey), kill.PlayerName, bossNameMode.Value, kill.FinalBlowName, loot, itemCount); } catch { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Loot] Message enrichment failed; sending boss names only."); return (kill.BossNumber <= 0) ? EventMessages.Loot(highlightMessage.Value, kill.EnemyKey, kill.PlayerName, "unavailable") : EventMessages.Boss(bossMessage.Value, kill.EnemyKey, kill.PlayerName, bossNameMode.Value, kill.FinalBlowName); } } private void OnLocalDeath(Player player, string cause) { if (deathTrigger.Value && deathEnabled.Value) { string message = EventMessages.Death(deathMessage.Value, includePlayerName.Value, playerNameOverride.Value, includePlayerName.Value ? player.GetPlayerName() : "", includeCause.Value, cause); Trigger("death", message); } } private bool Trigger(string kind, string message, double? postOverride = null) { if (!initialized || stopped || !captureEnabled.Value || paused) { return false; } if (!history.TryTrigger(clock.Elapsed.TotalSeconds, postOverride)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Capture] " + kind + " trigger ignored: a clip is collecting or encoding.")); return false; } pendingBoss = null; pendingSession = ZNet.instance; pendingAsHost = (Object)(object)pendingSession != (Object)null && pendingSession.IsServer(); pendingKind = kind; pendingMessage = message; ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Capture] " + kind + " event triggered; buffered frames: " + history.BufferedFrames)); return true; } private void OnDisable() { if (initialized || relay != null) { StopCapture(); } } private void StopCapture() { if (stopped) { return; } stopped = true; relay?.Dispose(); PlayerDeathDetector.OnLocalDeath = null; PlayerDeathDetector.OnError = null; BossKillDetector.OnKill = null; BossKillDetector.OnLootKill = null; BossKillDetector.ObserveOrdinary = null; lootHighlights.Clear(); BossKillDetector.OnError = null; BossAttribution.Clear(); BossLootDetector.Clear(); pendingBoss = (activeBoss = null); waitingForLoot?.Release(); waitingForLoot = null; try { Harmony obj = epicHarmony; if (obj != null) { obj.UnpatchSelf(); } } catch { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Loot] Could not remove Epic Loot patches."); } try { Harmony obj3 = lootHarmony; if (obj3 != null) { obj3.UnpatchSelf(); } } catch { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Loot] Could not remove loot patches."); } try { Harmony obj5 = attributionHarmony; if (obj5 != null) { obj5.UnpatchSelf(); } } catch { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Boss] Could not remove attribution patches."); } try { Harmony obj7 = bossHarmony; if (obj7 != null) { obj7.UnpatchSelf(); } } catch { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Boss] Could not remove patch during shutdown."); } try { Harmony obj9 = deathHarmony; if (obj9 != null) { obj9.UnpatchSelf(); } } catch { ((BaseUnityPlugin)this).Logger.LogWarning((object)"[Death] Could not remove patch during shutdown."); } shutdown.Cancel(); try { if (pending.Count > 0) { AsyncGPUReadback.WaitAllRequests(); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Capture] Readback teardown: " + ex.Message)); return; } foreach (ReadbackSlot allSlot in allSlots) { if (allSlot.Pixels.IsCreated) { allSlot.Pixels.Dispose(); } ReleaseTarget(allSlot.Target); } allSlots.Clear(); pending.Clear(); free.Clear(); ReleaseTarget(screen); screen = null; if (history != null) { history.ClearHistory(); } scratch = null; history = null; } } internal static class RelayProtocol { internal const int ChunkBytes = 16384; internal const int MaxBytes = 10485760; internal const int MaxPacketChars = 24000; internal static bool ValidId(string id) { Guid result; if (id != null && id.Length == 32) { return Guid.TryParseExact(id, "N", out result); } return false; } internal static bool ValidKind(string kind) { switch (kind) { default: return kind == "death"; case "manual": case "boss": case "loot": return true; } } internal static string Text(string value) { return Convert.ToBase64String(Encoding.UTF8.GetBytes(value)); } internal static string ReadText(string value) { if (value == null || value.Length > 12000) { return null; } try { string text = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(Convert.FromBase64String(value)); return (text.Length <= 2000) ? text : null; } catch { return null; } } } internal sealed class RelayBuffer { internal readonly string Id; internal readonly string Kind; internal readonly string Message; internal readonly byte[] Bytes; internal int Received { get; private set; } internal bool Complete => Received == Bytes.Length; internal RelayBuffer(string id, string kind, string message, int size, int limit) { if (!RelayProtocol.ValidId(id) || !RelayProtocol.ValidKind(kind) || message == null || message.Length > 2000 || size < 20 || size > Math.Min(limit, 10485760)) { throw new ArgumentException("Invalid relay offer"); } Id = id; Kind = kind; Message = message; Bytes = new byte[size]; } internal bool Add(int offset, byte[] chunk) { if (chunk == null || chunk.Length == 0 || chunk.Length > 16384 || offset != Received || chunk.Length > Bytes.Length - Received) { return false; } Buffer.BlockCopy(chunk, 0, Bytes, Received, chunk.Length); Received += chunk.Length; return true; } internal bool ValidWebP() { if (!Complete || Encoding.ASCII.GetString(Bytes, 0, 4) != "RIFF" || Encoding.ASCII.GetString(Bytes, 8, 4) != "WEBP") { return false; } if ((long)(uint)(Bytes[4] | (Bytes[5] << 8) | (Bytes[6] << 16) | (Bytes[7] << 24)) + 8L != Bytes.Length) { return false; } long num; uint num3; for (num = 12L; num < Bytes.Length; num += 8L + (long)num3 + (num3 & 1)) { if (num + 8 > Bytes.Length) { return false; } int num2 = (int)num + 4; num3 = (uint)(Bytes[num2] | (Bytes[num2 + 1] << 8) | (Bytes[num2 + 2] << 16) | (Bytes[num2 + 3] << 24)); } return num == Bytes.Length; } } } namespace ValheimEventClips.Core { public sealed class CaptureBuffer { internal sealed class Frame { internal readonly byte[] Pixels; internal double Time; internal int References; internal Frame(int bytes) { Pixels = new byte[bytes]; } } public sealed class Clip { private readonly Frame[] frames; private readonly CaptureBuffer owner; private bool released; public int Count { get { Check(); return frames.Length; } } public double TriggerTime { get; private set; } public double EndTime { get; private set; } internal Clip(CaptureBuffer owner, Frame[] frames, double trigger, double end) { this.owner = owner; this.frames = frames; TriggerTime = trigger; EndTime = end; } public byte[] GetPixels(int index) { Check(); return frames[index].Pixels; } public double GetTimestamp(int index) { Check(); return frames[index].Time; } private void Check() { if (released) { throw new ObjectDisposedException("Clip"); } } public void Release() { owner.CheckThread(); if (!released) { Frame[] array = frames; foreach (Frame frame in array) { owner.Release(frame); } released = true; owner.busy = false; } } } private readonly Queue free; private readonly Queue ring; private readonly List pending; private readonly int ownerThread; private readonly int preFrames; private readonly int maxClipFrames; private readonly int bytesPerFrame; private readonly double preSeconds; private readonly double postSeconds; private readonly double maxPostSeconds; private double activePostSeconds; private double lastTime = double.NegativeInfinity; private double triggerTime; private bool collecting; private bool busy; public int BufferedFrames => ring.Count; public int FreeFrames => free.Count; public long AllocatedPixelBytes { get; private set; } public bool IsBusy => busy; public CaptureBuffer(int width, int height, int fps, double pre, double post, long memoryBudgetBytes, double? maximumPostSeconds = null) { double num = maximumPostSeconds ?? post; if (width < 1 || height < 1 || fps < 1 || fps > 120 || !Finite(pre) || !Finite(post) || !Finite(num) || pre <= 0.0 || post < 0.0 || num < post || pre + num > 60.0) { throw new ArgumentOutOfRangeException("Invalid capture settings"); } int num2; checked { bytesPerFrame = width * height * 4; preFrames = (int)Math.Ceiling(pre * (double)fps); maxClipFrames = preFrames + (int)Math.Ceiling(num * (double)fps); num2 = preFrames + maxClipFrames + 1; AllocatedPixelBytes = unchecked((long)num2) * unchecked((long)bytesPerFrame); if (AllocatedPixelBytes > memoryBudgetBytes) { throw new ArgumentOutOfRangeException("memoryBudgetBytes", "Capture pool exceeds memory budget"); } preSeconds = pre; postSeconds = post; maxPostSeconds = num; ownerThread = Thread.CurrentThread.ManagedThreadId; free = new Queue(num2); ring = new Queue(preFrames); pending = new List(maxClipFrames); } for (int i = 0; i < num2; i++) { free.Enqueue(new Frame(bytesPerFrame)); } } private static bool Finite(double value) { if (!double.IsNaN(value)) { return !double.IsInfinity(value); } return false; } private void CheckThread() { if (Thread.CurrentThread.ManagedThreadId != ownerThread) { throw new InvalidOperationException("Capture state must be accessed on its owner thread"); } } private void Release(Frame frame) { if (--frame.References == 0) { free.Enqueue(frame); } } public bool AddFrame(byte[] rgba, double timestamp) { CheckThread(); if (rgba == null || rgba.Length != bytesPerFrame) { throw new ArgumentException("Expected one tightly packed RGBA frame", "rgba"); } if (!Finite(timestamp) || timestamp <= lastTime) { throw new ArgumentOutOfRangeException("timestamp", "Frames must be strictly ordered"); } lastTime = timestamp; while (ring.Count > 0 && (ring.Count >= preFrames || ring.Peek().Time < timestamp - preSeconds)) { Release(ring.Dequeue()); } if (free.Count == 0) { return false; } Frame frame = free.Dequeue(); Buffer.BlockCopy(rgba, 0, frame.Pixels, 0, bytesPerFrame); frame.Time = timestamp; frame.References = 1; ring.Enqueue(frame); if (collecting && timestamp >= triggerTime - preSeconds && timestamp < triggerTime + activePostSeconds && pending.Count < maxClipFrames) { frame.References++; pending.Add(frame); } return true; } public bool TryTrigger(double timestamp, double? postOverride = null) { CheckThread(); double num = postOverride ?? postSeconds; if (!Finite(num) || num < 0.0 || num > maxPostSeconds) { throw new ArgumentOutOfRangeException("postOverride"); } if (!Finite(timestamp) || timestamp < lastTime) { throw new ArgumentOutOfRangeException("timestamp"); } if (busy) { return false; } triggerTime = timestamp; activePostSeconds = num; pending.Clear(); foreach (Frame item in ring) { if (item.Time >= timestamp - preSeconds && item.Time < timestamp + activePostSeconds) { item.References++; pending.Add(item); } } busy = (collecting = true); return true; } public Clip TryComplete(double completedThrough) { CheckThread(); if (!Finite(completedThrough)) { throw new ArgumentOutOfRangeException("completedThrough"); } if (!collecting || completedThrough < triggerTime + activePostSeconds) { return null; } Clip result = new Clip(this, pending.ToArray(), triggerTime, triggerTime + activePostSeconds); pending.Clear(); collecting = false; return result; } public void CancelPending() { CheckThread(); if (!collecting) { return; } foreach (Frame item in pending) { Release(item); } pending.Clear(); busy = (collecting = false); } public void ClearHistory() { CheckThread(); CancelPending(); while (ring.Count > 0) { Release(ring.Dequeue()); } lastTime = double.NegativeInfinity; } } }