using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using FiresDiscordIntegration.ClientLogRelay; using FiresDiscordIntegration.ClientLogRelay.Consumers; using FiresDiscordIntegration.ClientLogRelay.Interactions; using FiresDiscordIntegration.ClientLogRelay.Transport; using FiresDiscordIntegration.ClientLogRelay.Webhook; using FiresDiscordIntegration.Discord; using FiresDiscordIntegration.Utilities; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Splatform; using TMPro; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("FiresDiscordIntegration")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("FiresDiscordIntegration")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("c1f16343-d521-42e6-a7a7-1b3aa4d63f4c")] [assembly: AssemblyFileVersion("1.0.3.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.3.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 FiresDiscordIntegration { [BepInPlugin("com.Fire.FiresDiscordIntegration", "FiresDiscordIntegration", "1.0.1")] public class FiresDiscordIntegrationPlugin : BaseUnityPlugin { public const string PluginGUID = "com.Fire.FiresDiscordIntegration"; public const string PluginName = "FiresDiscordIntegration"; public const string PluginVersion = "1.0.1"; public static FiresDiscordIntegrationPlugin Instance; public static readonly Harmony Harmony = new Harmony("com.Fire.FiresDiscordIntegration"); public ConfigSync configSync; private void Awake() { Instance = this; configSync = new ConfigSync("com.Fire.FiresDiscordIntegration") { DisplayName = "FiresDiscordIntegration", CurrentVersion = "1.0.1", MinimumRequiredVersion = "1.0.1" }; ConfigManager.Instance.Initialize(((BaseUnityPlugin)this).Config); DiscordIntegrationConfig.Initialize(((BaseUnityPlugin)this).Config); DiscordIntegrationConfig.BindToSync(configSync); Harmony.PatchAll(); try { DiscordBotIdentity.ResolveUsername("FiresDiscordIntegration"); } catch (Exception ex) { Debug.LogWarning((object)("[FiresDiscordIntegration] BotIdentity pre-warm failed: " + ex.Message)); } WrapperBatExtractor.ExtractIfMissing(); Debug.Log((object)"[FiresDiscordIntegration] v1.0.1 loaded."); } private void OnDestroy() { if (configSync != null) { configSync = null; } Instance = null; } } public sealed class ConfigManager { private static ConfigManager _instance; public ConfigEntry configVerboseLogging; public static ConfigManager Instance => _instance ?? (_instance = new ConfigManager()); public ConfigFile Config { get; private set; } public void Initialize(ConfigFile config) { Config = config; configVerboseLogging = config.Bind("General", "VerboseLogging", false, "Emit additional debug logs for the Discord/log-relay subsystems. Off by default — most operators only want this on for troubleshooting."); } } public class ConfigSync { [CompilerGenerated] private sealed class <>c__DisplayClass59_0 { public long target; internal bool b__0(ZNetPeer p) { return target == ZRoutedRpc.Everybody || p.m_uid == target; } } [CompilerGenerated] private sealed class <>c__DisplayClass60_0 { public ConfigSync <>4__this; public ZPackage package; internal IEnumerator b__1(ZNetPeer p) { return <>4__this.distributeConfigToPeers(p, package); } } [CompilerGenerated] private sealed class <>c__DisplayClass61_0 { public ZNetPeer peer; public ConfigSync <>4__this; } [CompilerGenerated] private sealed class d__59 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public long target; public ZPackage package; public ConfigSync <>4__this; private <>c__DisplayClass59_0 <>8__1; private List 5__2; private IEnumerator 5__3; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__59(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; 5__2 = null; 5__3 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass59_0(); <>8__1.target = target; if (!Object.op_Implicit((Object)(object)ZNet.instance)) { return false; } 5__2 = ((List)AccessTools.Field(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance)).Where((ZNetPeer p) => <>8__1.target == ZRoutedRpc.Everybody || p.m_uid == <>8__1.target).ToList(); 5__3 = <>4__this.SendZPackage(5__2, package); break; case 1: <>1__state = -1; break; } if (5__3.MoveNext()) { <>2__current = 5__3.Current; <>1__state = 1; return true; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__60 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public List peers; public ZPackage package; public ConfigSync <>4__this; private <>c__DisplayClass60_0 <>8__1; private byte[] 5__2; private List 5__3; private ZPackage 5__4; private MemoryStream 5__5; private DeflateStream 5__6; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__60(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; 5__2 = null; 5__3 = null; 5__4 = null; 5__5 = null; 5__6 = null; <>1__state = -2; } private bool MoveNext() { //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass60_0(); <>8__1.<>4__this = <>4__this; <>8__1.package = package; if (!Object.op_Implicit((Object)(object)ZNet.instance)) { return false; } 5__2 = <>8__1.package.GetArray(); if (5__2.Length > 10000) { 5__4 = new ZPackage(); 5__4.Write((byte)4); 5__5 = new MemoryStream(); try { 5__6 = new DeflateStream(5__5, CompressionLevel.Optimal); try { 5__6.Write(5__2, 0, 5__2.Length); } finally { if (5__6 != null) { ((IDisposable)5__6).Dispose(); } } 5__6 = null; 5__4.Write(5__5.ToArray()); <>8__1.package = 5__4; } finally { if (5__5 != null) { ((IDisposable)5__5).Dispose(); } } 5__4 = null; 5__5 = null; } 5__3 = (from p in peers where p.IsReady() select <>8__1.<>4__this.distributeConfigToPeers(p, <>8__1.package)).ToList(); 5__3.RemoveAll((IEnumerator w) => !w.MoveNext()); break; case 1: <>1__state = -1; 5__3.RemoveAll((IEnumerator w) => !w.MoveNext()); break; } if (5__3.Any()) { <>2__current = null; <>1__state = 1; return true; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__61 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public ZNetPeer peer; public ZPackage package; public ConfigSync <>4__this; private <>c__DisplayClass61_0 <>8__1; private byte[] 5__2; private int 5__3; private long 5__4; private int 5__5; private ZPackage 5__6; private IEnumerator <>s__7; private bool 5__8; private IEnumerator <>s__9; private bool 5__10; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__61(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { switch (<>1__state) { case -3: case 1: try { } finally { <>m__Finally1(); } break; case -4: case 3: try { } finally { <>m__Finally2(); } break; } <>8__1 = null; 5__2 = null; 5__6 = null; <>s__7 = null; <>s__9 = null; <>1__state = -2; } private bool MoveNext() { //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass61_0(); <>8__1.peer = peer; <>8__1.<>4__this = <>4__this; 5__2 = package.GetArray(); if (5__2.Length > 250000) { 5__3 = (5__2.Length + 249999) / 250000; 5__4 = ++packageCounter; 5__5 = 0; goto IL_0247; } <>s__9 = waitForQueue().GetEnumerator(); <>1__state = -4; goto IL_02bb; case 1: <>1__state = -3; goto IL_0128; case 2: <>1__state = -1; goto IL_022d; case 3: { <>1__state = -4; goto IL_02bb; } IL_02bb: if (<>s__9.MoveNext()) { 5__10 = <>s__9.Current; <>2__current = 5__10; <>1__state = 3; return true; } <>m__Finally2(); <>s__9 = null; SendPackage(package); break; IL_0247: if (5__5 < 5__3) { <>s__7 = waitForQueue().GetEnumerator(); <>1__state = -3; goto IL_0128; } break; IL_022d: 5__6 = null; 5__5++; goto IL_0247; IL_0128: if (<>s__7.MoveNext()) { 5__8 = <>s__7.Current; <>2__current = 5__8; <>1__state = 1; return true; } <>m__Finally1(); <>s__7 = null; if (!<>8__1.peer.m_socket.IsConnected()) { break; } 5__6 = new ZPackage(); 5__6.Write((byte)2); 5__6.Write(5__4); 5__6.Write(5__5); 5__6.Write(5__3); 5__6.Write(5__2.Skip(250000 * 5__5).Take(250000).ToArray()); SendPackage(5__6); if (5__5 < 5__3 - 1) { <>2__current = true; <>1__state = 2; return true; } goto IL_022d; } return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } void SendPackage(ZPackage pkg) { if (isServer) { ((<>c__DisplayClass61_0)this).peer.m_rpc.Invoke(((<>c__DisplayClass61_0)this).<>4__this.Name + " ConfigSync", new object[1] { pkg }); } else { ZRoutedRpc.instance.InvokeRoutedRPC(((<>c__DisplayClass61_0)this).peer.m_server ? 0 : ((<>c__DisplayClass61_0)this).peer.m_uid, ((<>c__DisplayClass61_0)this).<>4__this.Name + " ConfigSync", new object[1] { pkg }); } } [IteratorStateMachine(typeof(<>c__DisplayClass61_0.<g__waitForQueue|0>d))] IEnumerable waitForQueue() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <>c__DisplayClass61_0.<g__waitForQueue|0>d(-2) { <>4__this = this }; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<>s__7 != null) { <>s__7.Dispose(); } } private void <>m__Finally2() { <>1__state = -1; if (<>s__9 != null) { <>s__9.Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } public static bool ProcessingServerUpdate = false; public readonly string Name; public string DisplayName; public string CurrentVersion; public string MinimumRequiredVersion; public bool ModRequired; private bool? forceConfigLocking; private bool isSourceOfTruth = true; public static HashSet configSyncs = new HashSet(); public HashSet allConfigs = new HashSet(); public HashSet allCustomValues = new HashSet(); public static bool isServer; public static bool lockExempt = false; private OwnConfigEntryBase lockedConfig; private const byte PARTIAL_CONFIGS = 1; private const byte FRAGMENTED_CONFIG = 2; private const byte COMPRESSED_CONFIG = 4; private readonly Dictionary> configValueCache = new Dictionary>(); private readonly List> cacheExpirations = new List>(); private static long packageCounter = 0L; private DateTime lastConfigLogTime = DateTime.MinValue; private bool initialSyncDone; public bool IsLocked { get { bool? flag = forceConfigLocking; bool num; if (!flag.HasValue) { OwnConfigEntryBase ownConfigEntryBase = lockedConfig; if (!(((ownConfigEntryBase != null) ? ownConfigEntryBase.BaseConfig.BoxedValue : null) is IConvertible convertible)) { goto IL_0056; } num = convertible.ToInt32(CultureInfo.InvariantCulture) != 0; } else { num = flag.GetValueOrDefault(); } if (!num) { goto IL_0056; } int result = ((!lockExempt) ? 1 : 0); goto IL_0057; IL_0056: result = 0; goto IL_0057; IL_0057: return (byte)result != 0; } set { forceConfigLocking = value; } } public bool IsAdmin => lockExempt || isSourceOfTruth; public bool IsSourceOfTruth { get { return isSourceOfTruth; } internal set { if (value != isSourceOfTruth) { isSourceOfTruth = value; this.SourceOfTruthChanged?.Invoke(value); } } } public bool InitialSyncDone { get { return initialSyncDone; } internal set { initialSyncDone = value; } } public event Action SourceOfTruthChanged; public event Action lockedConfigChanged; public ConfigSync(string name) { Name = name; DisplayName = name; configSyncs.Add(this); } public void RequestSync() { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer()) { List source = (List)AccessTools.Field(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance); ZNetPeer val = ((IEnumerable)source).FirstOrDefault((Func)((ZNetPeer p) => p.m_server)); if (val != null) { ZRoutedRpc.instance.InvokeRoutedRPC(val.m_uid, Name + " ConfigSync", new object[1] { (object)new ZPackage() }); } } } public SyncedConfigEntry AddConfigEntry(ConfigEntry configEntry) { SyncedConfigEntry syncedEntry = (configData((ConfigEntryBase)(object)configEntry) as SyncedConfigEntry) ?? new SyncedConfigEntry(configEntry); object[] first = ((ConfigEntryBase)configEntry).Description.Tags?.ToArray() ?? new object[1] { new ConfigurationManagerAttributes() }; first = first.Concat(new object[1] { syncedEntry }).ToArray(); AccessTools.Field(typeof(ConfigDescription), "k__BackingField").SetValue(((ConfigEntryBase)configEntry).Description, first); configEntry.SettingChanged += delegate { if (!ProcessingServerUpdate && syncedEntry.SynchronizedConfig) { Broadcast(ZRoutedRpc.Everybody, (ConfigEntryBase)(object)configEntry); } }; allConfigs.Add(syncedEntry); return syncedEntry; } public SyncedConfigEntry AddLockingConfigEntry(ConfigEntry lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry(lockingConfig); lockingConfig.SettingChanged += delegate { this.lockedConfigChanged?.Invoke(); }; return (SyncedConfigEntry)lockedConfig; } internal void AddCustomValue(CustomSyncedValueBase customValue) { if (allCustomValues.Any((CustomSyncedValueBase v) => v.Identifier == customValue.Identifier) || customValue.Identifier == "serverversion") { throw new Exception("Cannot have multiple settings with the same name or with a reserved name (serverversion)"); } allCustomValues.Add(customValue); allCustomValues = new HashSet(allCustomValues.OrderByDescending((CustomSyncedValueBase v) => v.Priority)); customValue.ValueChanged += delegate { if (!ProcessingServerUpdate) { Broadcast(ZRoutedRpc.Everybody, customValue); } }; } public void Broadcast(long target, ConfigEntryBase config) { if (!IsLocked || IsAdmin) { ZPackage package = ConfigsToPackage((IEnumerable)(object)new ConfigEntryBase[1] { config }); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(SendZPackage(target, package)); } } } public void Broadcast(long target, CustomSyncedValueBase customValue) { if (!IsLocked || IsAdmin) { ZPackage package = ConfigsToPackage(null, new CustomSyncedValueBase[1] { customValue }); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(SendZPackage(target, package)); } } } internal void RPC_FromServerConfigSync(ZRpc rpc, ZPackage package) { lockedConfigChanged += serverLockedSettingChanged; IsSourceOfTruth = false; if (HandleConfigSyncRPC(0L, package, clientUpdate: false)) { InitialSyncDone = true; } } internal void RPC_FromOtherClientConfigSync(long sender, ZPackage package) { HandleConfigSyncRPC(sender, package, clientUpdate: true); } private bool HandleConfigSyncRPC(long sender, ZPackage package, bool clientUpdate) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown //IL_019b: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Expected O, but got Unknown //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Expected O, but got Unknown try { if (isServer && IsLocked) { ZRpc currentRpc = SnatchCurrentlyHandlingRPC.currentRpc; object obj; if (currentRpc == null) { obj = null; } else { ISocket socket = currentRpc.GetSocket(); obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; SyncedList val = (SyncedList)AccessTools.Field(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); if (text != null && !val.Contains(text)) { return false; } } cacheExpirations.RemoveAll((KeyValuePair kv) => kv.Key < DateTimeOffset.Now.Ticks && configValueCache.Remove(kv.Value)); byte b = package.ReadByte(); if ((b & 2u) != 0) { long num = package.ReadLong(); string text2 = sender.ToString() + num; if (!configValueCache.TryGetValue(text2, out var value)) { value = new SortedDictionary(); configValueCache[text2] = value; cacheExpirations.Add(new KeyValuePair(DateTimeOffset.Now.Ticks + 600000000, text2)); } int key = package.ReadInt(); int num2 = package.ReadInt(); value[key] = package.ReadByteArray(); if (value.Count < num2) { return false; } configValueCache.Remove(text2); package = new ZPackage(value.Values.SelectMany((byte[] a) => a).ToArray()); b = package.ReadByte(); } ProcessingServerUpdate = true; if ((b & 4u) != 0) { using MemoryStream stream = new MemoryStream(package.ReadByteArray()); using MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) { deflateStream.CopyTo(memoryStream); } package = new ZPackage(memoryStream.ToArray()); b = package.ReadByte(); } if ((b & 1) == 0) { resetConfigsFromServer(); } ParsedConfigs parsedConfigs = ReadConfigsFromPackage(package); ConfigFile val2 = null; bool saveOnConfigSet = false; foreach (KeyValuePair configValue in parsedConfigs.configValues) { if (!isServer && configValue.Key.LocalBaseValue == null) { configValue.Key.LocalBaseValue = configValue.Key.BaseConfig.BoxedValue; } if (val2 == null) { val2 = configValue.Key.BaseConfig.ConfigFile; saveOnConfigSet = val2.SaveOnConfigSet; val2.SaveOnConfigSet = false; } configValue.Key.BaseConfig.BoxedValue = configValue.Value; } if (val2 != null) { val2.SaveOnConfigSet = saveOnConfigSet; val2.Save(); } foreach (KeyValuePair customValue in parsedConfigs.customValues) { if (!isServer && customValue.Key.LocalBaseValue == null) { customValue.Key.LocalBaseValue = customValue.Key.BoxedValue; } customValue.Key.BoxedValue = customValue.Value; } if ((DateTime.Now - lastConfigLogTime).TotalSeconds > 12.0) { Debug.Log((object)string.Format("[{0}] Received {1} configs and {2} custom values from {3} for mod {4}", "FiresDiscordIntegration", parsedConfigs.configValues.Count, parsedConfigs.customValues.Count, (isServer || clientUpdate) ? $"client {sender}" : "server", DisplayName ?? Name)); lastConfigLogTime = DateTime.Now; } if (!isServer) { serverLockedSettingChanged(); } return true; } finally { ProcessingServerUpdate = false; } } private void serverLockedSettingChanged() { foreach (OwnConfigEntryBase allConfig in allConfigs) { ConfigurationManagerAttributes configurationManagerAttributes = allConfig.BaseConfig.Description.Tags?.OfType().FirstOrDefault(); if (configurationManagerAttributes != null) { configurationManagerAttributes.ReadOnly = !isWritableConfig(allConfig); } } } internal static bool isWritableConfig(OwnConfigEntryBase config) { ConfigSync configSync = configSyncs.FirstOrDefault((ConfigSync cs) => cs.allConfigs.Contains(config)); if (configSync == null || configSync.IsSourceOfTruth || !config.SynchronizedConfig || config.LocalBaseValue == null) { return true; } return !configSync.IsLocked || config != configSync.lockedConfig || lockExempt; } internal void resetConfigsFromServer() { ConfigFile val = null; bool saveOnConfigSet = false; foreach (OwnConfigEntryBase item in allConfigs.Where((OwnConfigEntryBase c) => c.LocalBaseValue != null)) { if (val == null) { val = item.BaseConfig.ConfigFile; saveOnConfigSet = val.SaveOnConfigSet; val.SaveOnConfigSet = false; } item.BaseConfig.BoxedValue = item.LocalBaseValue; item.LocalBaseValue = null; } if (val != null) { val.SaveOnConfigSet = saveOnConfigSet; val.Save(); } foreach (CustomSyncedValueBase item2 in allCustomValues.Where((CustomSyncedValueBase c) => c.LocalBaseValue != null)) { item2.BoxedValue = item2.LocalBaseValue; item2.LocalBaseValue = null; } lockedConfigChanged -= serverLockedSettingChanged; serverLockedSettingChanged(); } private ParsedConfigs ReadConfigsFromPackage(ZPackage package) { ParsedConfigs parsedConfigs = new ParsedConfigs(); Dictionary dictionary = allConfigs.ToDictionary((OwnConfigEntryBase c) => c.BaseConfig.Definition.Section + "*" + c.BaseConfig.Definition.Key, (OwnConfigEntryBase c) => c); int num = package.ReadInt(); for (int i = 0; i < num; i++) { string text = package.ReadString(); string key = package.ReadString(); string text2 = package.ReadString(); Type type = Type.GetType(text2); if (text2 == "" || type != null) { object obj; try { obj = ((text2 == "") ? null : ReadValueWithTypeFromZPackage(package, type)); } catch (InvalidDeserializationTypeException ex) { Debug.LogWarning((object)("[FiresDiscordIntegration] Got unexpected struct internal type " + ex.received + " for field " + ex.field + " struct " + text2 + " for " + key + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + ex.expected)); continue; } OwnConfigEntryBase value; if (text == "Internal") { if (key == "lockexempt" && obj is bool flag) { lockExempt = flag; continue; } CustomSyncedValueBase customSyncedValueBase = allCustomValues.FirstOrDefault((CustomSyncedValueBase v) => v.Identifier == key); if (customSyncedValueBase != null) { if ((text2 == "" && (!customSyncedValueBase.Type.IsValueType || Nullable.GetUnderlyingType(customSyncedValueBase.Type) != null)) || GetZPackageTypeString(customSyncedValueBase.Type) == text2) { parsedConfigs.customValues[customSyncedValueBase] = obj; continue; } Debug.LogWarning((object)("[FiresDiscordIntegration] Got unexpected type " + text2 + " for internal value " + key + " for mod " + (DisplayName ?? Name) + ", expecting " + customSyncedValueBase.Type.AssemblyQualifiedName)); } } else if (dictionary.TryGetValue(text + "*" + key, out value)) { Type type2 = configType(value.BaseConfig); if ((text2 == "" && (!type2.IsValueType || Nullable.GetUnderlyingType(type2) != null)) || GetZPackageTypeString(type2) == text2) { parsedConfigs.configValues[value] = obj; continue; } Debug.LogWarning((object)("[FiresDiscordIntegration] Got unexpected type " + text2 + " for " + key + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + type2.AssemblyQualifiedName)); } else { Debug.LogWarning((object)("[FiresDiscordIntegration] Received unknown config entry " + key + " in section " + text + " for mod " + (DisplayName ?? Name) + ".")); } continue; } Debug.LogWarning((object)("[FiresDiscordIntegration] Got invalid type " + text2 + ", abort reading of received configs")); return new ParsedConfigs(); } return parsedConfigs; } private static string GetZPackageTypeString(Type type) { return type.AssemblyQualifiedName; } private static void AddValueToZPackage(ZPackage package, object value) { Type type = value?.GetType(); if (value is Enum) { value = ((IConvertible)value).ToType(Enum.GetUnderlyingType(type), CultureInfo.InvariantCulture); } else { if (value is ICollection collection) { package.Write(collection.Count); { IEnumerator enumerator = collection.GetEnumerator(); try { while (enumerator.MoveNext()) { AddValueToZPackage(package, enumerator.Current); } return; } finally { IDisposable disposable = enumerator as IDisposable; if (disposable != null) { disposable.Dispose(); } } } } if (type != null && type.IsValueType && !type.IsPrimitive) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); package.Write(fields.Length); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { package.Write(GetZPackageTypeString(fieldInfo.FieldType)); AddValueToZPackage(package, fieldInfo.GetValue(value)); } return; } } ZRpc.Serialize(new object[1] { value }, ref package); } private static object ReadValueWithTypeFromZPackage(ZPackage package, Type type) { if (type != null && type.IsValueType && !type.IsPrimitive && !type.IsEnum) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); int num = package.ReadInt(); if (num != fields.Length) { throw new InvalidDeserializationTypeException { received = $"(field count: {num})", expected = $"(field count: {fields.Length})" }; } object uninitializedObject = FormatterServices.GetUninitializedObject(type); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { string text = package.ReadString(); if (text != GetZPackageTypeString(fieldInfo.FieldType)) { throw new InvalidDeserializationTypeException { received = text, expected = GetZPackageTypeString(fieldInfo.FieldType), field = fieldInfo.Name }; } fieldInfo.SetValue(uninitializedObject, ReadValueWithTypeFromZPackage(package, fieldInfo.FieldType)); } return uninitializedObject; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<, >)) { int num2 = package.ReadInt(); Type type2 = typeof(KeyValuePair<, >).MakeGenericType(type.GenericTypeArguments); IDictionary dictionary = (IDictionary)Activator.CreateInstance(type); FieldInfo field = type2.GetField("key", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field2 = type2.GetField("value", BindingFlags.Instance | BindingFlags.NonPublic); for (int j = 0; j < num2; j++) { object obj = ReadValueWithTypeFromZPackage(package, type2); dictionary.Add(field.GetValue(obj), field2.GetValue(obj)); } return dictionary; } if (type.IsGenericType) { Type type3 = typeof(ICollection<>).MakeGenericType(type.GenericTypeArguments[0]); if (type3.IsAssignableFrom(type)) { int num3 = package.ReadInt(); object obj2 = Activator.CreateInstance(type); MethodInfo method = type3.GetMethod("Add"); for (int k = 0; k < num3; k++) { method.Invoke(obj2, new object[1] { ReadValueWithTypeFromZPackage(package, type.GenericTypeArguments[0]) }); } return obj2; } } ParameterInfo parameterInfo = (ParameterInfo)FormatterServices.GetUninitializedObject(typeof(ParameterInfo)); AccessTools.Field(typeof(ParameterInfo), "ClassImpl").SetValue(parameterInfo, type); List source = new List(); ZRpc.Deserialize(new ParameterInfo[2] { null, parameterInfo }, package, ref source); return source.First(); } internal ZPackage ConfigsToPackage(IEnumerable configs = null, IEnumerable customValues = null, IEnumerable packageEntries = null, bool partial = true) { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Expected O, but got Unknown List list = configs?.Where((ConfigEntryBase c) => configData(c).SynchronizedConfig).ToList() ?? new List(); List list2 = customValues?.ToList() ?? new List(); List list3 = packageEntries?.ToList() ?? new List(); ZPackage val = new ZPackage(); val.Write((byte)(partial ? 1u : 0u)); val.Write(list.Count + list2.Count + list3.Count); foreach (PackageEntry item in list3) { val.Write(item.section); val.Write(item.key); val.Write((item.value == null) ? "" : GetZPackageTypeString(item.type)); AddValueToZPackage(val, item.value); } foreach (CustomSyncedValueBase item2 in list2) { val.Write("Internal"); val.Write(item2.Identifier); val.Write(GetZPackageTypeString(item2.Type)); AddValueToZPackage(val, item2.BoxedValue); } foreach (ConfigEntryBase item3 in list) { val.Write(item3.Definition.Section); val.Write(item3.Definition.Key); val.Write(GetZPackageTypeString(configType(item3))); AddValueToZPackage(val, item3.BoxedValue); } return val; } private static Type configType(ConfigEntryBase config) { return configType(config.SettingType); } private static Type configType(Type type) { return type.IsEnum ? Enum.GetUnderlyingType(type) : type; } [IteratorStateMachine(typeof(d__59))] public IEnumerator SendZPackage(long target, ZPackage package) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__59(0) { <>4__this = this, target = target, package = package }; } [IteratorStateMachine(typeof(d__60))] public IEnumerator SendZPackage(List peers, ZPackage package) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__60(0) { <>4__this = this, peers = peers, package = package }; } [IteratorStateMachine(typeof(d__61))] private IEnumerator distributeConfigToPeers(ZNetPeer peer, ZPackage package) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__61(0) { <>4__this = this, peer = peer, package = package }; } internal static OwnConfigEntryBase configData(ConfigEntryBase config) { return config.Description.Tags?.OfType().SingleOrDefault(); } private static T configAttribute(ConfigEntryBase config) where T : class { object[] tags = config.Description.Tags; return (tags != null) ? tags.OfType().FirstOrDefault() : null; } } public abstract class OwnConfigEntryBase { public readonly ConfigEntryBase BaseConfig; public object LocalBaseValue; public bool SynchronizedConfig = true; protected OwnConfigEntryBase(ConfigEntryBase config) { BaseConfig = config; } } public class SyncedConfigEntry : OwnConfigEntryBase { public T Value { get { if (!(BaseConfig is ConfigEntry val)) { throw new InvalidOperationException("Cannot cast BaseConfig to ConfigEntry<" + typeof(T).Name + ">"); } return val.Value; } set { ((ConfigEntry)(object)BaseConfig).Value = value; } } public T DefaultValue => (T)BaseConfig.BoxedValue; public SyncedConfigEntry(ConfigEntry config) : base((ConfigEntryBase)(object)config) { } } public class CustomSyncedValueBase { public string Identifier { get; } public Type Type { get; } public object BoxedValue { get; set; } public object LocalBaseValue { get; set; } public int Priority { get; } public event Action ValueChanged; public CustomSyncedValueBase(string identifier, Type type, int priority = 0) { Identifier = identifier; Type = type; Priority = priority; } } public class ConfigurationManagerAttributes { public bool? ReadOnly; } public class ParsedConfigs { public readonly Dictionary configValues = new Dictionary(); public readonly Dictionary customValues = new Dictionary(); } public class PackageEntry { public string section; public string key; public Type type; public object value; } public class InvalidDeserializationTypeException : Exception { public string expected; public string received; public string field = ""; } [HarmonyPatch(typeof(ZRpc), "HandlePackage")] public class SnatchCurrentlyHandlingRPC { public static ZRpc currentRpc; [HarmonyPrefix] private static void Prefix(ZRpc __instance) { currentRpc = __instance; } } [HarmonyPatch(typeof(ZNet), "Awake")] public class RegisterRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { ConfigSync.isServer = __instance.IsServer(); foreach (ConfigSync configSync in ConfigSync.configSyncs) { ZRoutedRpc.instance.Register(configSync.Name + " ConfigSync", (Action)configSync.RPC_FromOtherClientConfigSync); if (ConfigSync.isServer) { Debug.Log((object)("[FiresDiscordIntegration] Registered '" + configSync.Name + " ConfigSync' RPC")); } } } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] public class RegisterClientRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { return; } foreach (ConfigSync configSync in ConfigSync.configSyncs) { peer.m_rpc.Register(configSync.Name + " ConfigSync", (Action)configSync.RPC_FromServerConfigSync); } } } [HarmonyPatch(typeof(ConfigEntryBase), "GetSerializedValue")] public class PreventSavingServerInfo { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, ref string __result) { OwnConfigEntryBase ownConfigEntryBase = ConfigSync.configData(__instance); if (ownConfigEntryBase == null || ConfigSync.isWritableConfig(ownConfigEntryBase)) { return true; } __result = TomlTypeConverter.ConvertToString(ownConfigEntryBase.LocalBaseValue, __instance.SettingType); return false; } } [HarmonyPatch(typeof(ConfigEntryBase), "SetSerializedValue")] public class PreventConfigRereadChangingValues { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, string value) { OwnConfigEntryBase ownConfigEntryBase = ConfigSync.configData(__instance); if (ownConfigEntryBase?.LocalBaseValue != null) { try { ownConfigEntryBase.LocalBaseValue = TomlTypeConverter.ConvertToValue(value, __instance.SettingType); } catch (Exception ex) { Debug.LogWarning((object)string.Format("[{0}] Config value of setting \"{1}\" could not be parsed and will be ignored. Reason: {2}; Value: {3}", "FiresDiscordIntegration", __instance.Definition, ex.Message, value)); } return false; } return true; } } } namespace FiresDiscordIntegration.ClientLogRelay { public sealed class ClientLogArtifacts { public string PlatformId { get; } public string PlayerName { get; } public byte[] LogBytes { get; } public IReadOnlyDictionary ModList { get; } public IReadOnlyDictionary ServerMods { get; } public string BrandLabel { get; } public DateTime CapturedUtc { get; } public string ErrorsWarningsReport { get; internal set; } public int ErrorCount { get; internal set; } public int WarningCount { get; internal set; } public int BenignSkipped { get; internal set; } public int DuplicatesCollapsed { get; internal set; } public IReadOnlyList SourceBreakdown { get; internal set; } public ModListDiff.Result ModDiff { get; internal set; } public string SafePlatformId => (PlatformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_"); public string SafePlayerName { get { if (string.IsNullOrEmpty(PlayerName)) { return "unknown"; } char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); return string.Concat(PlayerName.Split(invalidFileNameChars)); } } public string DefaultFolderName => SafePlayerName + "_" + SafePlatformId; public ClientLogArtifacts(string platformId, string playerName, byte[] logBytes, IReadOnlyDictionary modList, IReadOnlyDictionary serverMods = null, string brandLabel = null) { PlatformId = platformId ?? string.Empty; PlayerName = (string.IsNullOrEmpty(playerName) ? "unknown" : playerName); LogBytes = logBytes ?? Array.Empty(); ModList = modList ?? new Dictionary(0); ServerMods = serverMods; BrandLabel = brandLabel; CapturedUtc = DateTime.UtcNow; } } public static class ClientLogArtifactWriter { public static string Write(string targetDir, ClientLogArtifacts artifacts) { if (artifacts == null) { return null; } if (string.IsNullOrEmpty(targetDir)) { return null; } try { string text = Path.Combine(targetDir, artifacts.DefaultFolderName); if (!Directory.Exists(text)) { Directory.CreateDirectory(text); } if (artifacts.LogBytes != null && artifacts.LogBytes.Length != 0) { File.WriteAllBytes(Path.Combine(text, "LogOutput.log"), artifacts.LogBytes); } if (artifacts.ModList != null && artifacts.ModList.Count > 0) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Client BepInEx Mod List"); stringBuilder.AppendLine("# Player: " + artifacts.PlayerName); stringBuilder.AppendLine("# SteamID: " + artifacts.PlatformId); stringBuilder.AppendLine($"# Captured: {artifacts.CapturedUtc:yyyy-MM-dd HH:mm:ss} UTC"); stringBuilder.AppendLine($"# Mod Count: {artifacts.ModList.Count}"); stringBuilder.AppendLine("# Format: GUID=Version"); stringBuilder.AppendLine(); foreach (KeyValuePair item in artifacts.ModList.OrderBy, string>((KeyValuePair kv) => kv.Key, StringComparer.OrdinalIgnoreCase)) { stringBuilder.Append(item.Key).Append('=').AppendLine(item.Value); } File.WriteAllText(Path.Combine(text, "modlist.txt"), stringBuilder.ToString()); } if (!string.IsNullOrEmpty(artifacts.ErrorsWarningsReport)) { File.WriteAllText(Path.Combine(text, "errors_warnings.txt"), artifacts.ErrorsWarningsReport); } if (artifacts.ModDiff != null && !string.IsNullOrEmpty(artifacts.ModDiff.Report)) { File.WriteAllText(Path.Combine(text, "mod_diff.txt"), artifacts.ModDiff.Report); } return text; } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] Failed to write artifacts for " + artifacts.PlatformId + ": " + ex.Message)); return null; } } } public static class ClientLogRelay { private static readonly object _lock = new object(); private static readonly List _consumers = new List(); private const string LoginOwnerKey = "FiresMods.ClientLogRelay.LoginSnapshotOwner"; private const string LoginOwnerPriorityKey = "FiresMods.ClientLogRelay.LoginSnapshotOwnerPriority"; private static ILogRequestHandler _logRequestHandler; public static bool HasConsumers { get { lock (_lock) { return _consumers.Count > 0; } } } public static bool HasLogRequestHandler { get { lock (_lock) { return _logRequestHandler != null; } } } public static bool RegisterConsumer(IClientLogConsumer consumer) { if (consumer == null || string.IsNullOrEmpty(consumer.ConsumerId)) { return false; } lock (_lock) { for (int i = 0; i < _consumers.Count; i++) { if (string.Equals(_consumers[i].ConsumerId, consumer.ConsumerId, StringComparison.Ordinal)) { return false; } } _consumers.Add(consumer); Debug.Log((object)$"[ClientLogRelay] Registered consumer: {consumer.ConsumerId} (total: {_consumers.Count})"); return true; } } public static bool UnregisterConsumer(string consumerId) { if (string.IsNullOrEmpty(consumerId)) { return false; } lock (_lock) { for (int i = 0; i < _consumers.Count; i++) { if (string.Equals(_consumers[i].ConsumerId, consumerId, StringComparison.Ordinal)) { _consumers.RemoveAt(i); Debug.Log((object)$"[ClientLogRelay] Unregistered consumer: {consumerId} (total: {_consumers.Count})"); return true; } } return false; } } public static void ReportArtifacts(ClientLogArtifacts artifacts) { if (artifacts == null) { return; } try { LogErrorWarningExtractor.Result result = LogErrorWarningExtractor.Extract(artifacts.LogBytes, artifacts.PlayerName, artifacts.PlatformId); artifacts.ErrorsWarningsReport = result.Report; artifacts.ErrorCount = result.ErrorCount; artifacts.WarningCount = result.WarningCount; artifacts.BenignSkipped = result.BenignSkipped; artifacts.DuplicatesCollapsed = result.DuplicatesCollapsed; artifacts.SourceBreakdown = result.SourceBreakdown; } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] Extraction failed for " + artifacts.PlatformId + ": " + ex.Message)); artifacts.ErrorsWarningsReport = "# Extraction error: " + ex.Message; } if (artifacts.ServerMods != null && artifacts.ServerMods.Count > 0) { try { artifacts.ModDiff = ModListDiff.Compute(artifacts.ModList, artifacts.ServerMods, artifacts.PlayerName, artifacts.PlatformId, artifacts.BrandLabel, artifacts.CapturedUtc); } catch (Exception ex2) { Debug.LogWarning((object)("[ClientLogRelay] ModListDiff failed for " + artifacts.PlatformId + ": " + ex2.Message)); } } IClientLogConsumer[] array; lock (_lock) { if (_consumers.Count == 0) { Debug.Log((object)("[ClientLogRelay] No consumers; dropping artifacts for " + artifacts.PlatformId)); return; } array = _consumers.ToArray(); } foreach (IClientLogConsumer clientLogConsumer in array) { try { clientLogConsumer.OnClientArtifacts(artifacts); } catch (Exception ex3) { Debug.LogWarning((object)("[ClientLogRelay] Consumer '" + clientLogConsumer.ConsumerId + "' threw: " + ex3.Message)); } } } public static bool TryClaimLoginSnapshotOwnership(string ownerId, int priority = 0) { if (string.IsNullOrEmpty(ownerId)) { return false; } lock (_lock) { string text = AppDomain.CurrentDomain.GetData("FiresMods.ClientLogRelay.LoginSnapshotOwner") as string; int num2 = ((AppDomain.CurrentDomain.GetData("FiresMods.ClientLogRelay.LoginSnapshotOwnerPriority") is int num) ? num : int.MinValue); if (string.IsNullOrEmpty(text)) { AppDomain.CurrentDomain.SetData("FiresMods.ClientLogRelay.LoginSnapshotOwner", ownerId); AppDomain.CurrentDomain.SetData("FiresMods.ClientLogRelay.LoginSnapshotOwnerPriority", priority); Debug.Log((object)$"[ClientLogRelay] Login snapshot owner: '{ownerId}' (priority {priority})"); return true; } if (string.Equals(text, ownerId, StringComparison.Ordinal)) { if (priority > num2) { AppDomain.CurrentDomain.SetData("FiresMods.ClientLogRelay.LoginSnapshotOwnerPriority", priority); } return true; } if (priority > num2) { Debug.Log((object)("[ClientLogRelay] Login snapshot owner changed: " + $"'{text}' (priority {num2}) ? '{ownerId}' (priority {priority})")); AppDomain.CurrentDomain.SetData("FiresMods.ClientLogRelay.LoginSnapshotOwner", ownerId); AppDomain.CurrentDomain.SetData("FiresMods.ClientLogRelay.LoginSnapshotOwnerPriority", priority); return true; } Debug.Log((object)("[ClientLogRelay] Login snapshot claim declined for '" + ownerId + "' " + $"(priority {priority}); current owner '{text}' (priority {num2})")); return false; } } public static bool IsLoginSnapshotOwner(string ownerId) { if (string.IsNullOrEmpty(ownerId)) { return false; } string text = AppDomain.CurrentDomain.GetData("FiresMods.ClientLogRelay.LoginSnapshotOwner") as string; if (string.IsNullOrEmpty(text)) { return true; } return string.Equals(text, ownerId, StringComparison.Ordinal); } public static string GetLoginSnapshotOwner() { return AppDomain.CurrentDomain.GetData("FiresMods.ClientLogRelay.LoginSnapshotOwner") as string; } public static void RegisterLogRequestHandler(ILogRequestHandler handler) { lock (_lock) { _logRequestHandler = handler; Debug.Log((object)((handler != null) ? ("[ClientLogRelay] LogRequestHandler installed: " + handler.GetType().FullName) : "[ClientLogRelay] LogRequestHandler cleared")); } } public static bool TryDispatchLogRequest(string messageId, string discordUserId, string emoji) { ILogRequestHandler logRequestHandler; lock (_lock) { logRequestHandler = _logRequestHandler; } if (logRequestHandler == null) { return false; } if (!LogRequestRegistry.TryGet(messageId, out var ctx)) { return false; } bool flag; try { flag = logRequestHandler.IsAuthorized(discordUserId); } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] ILogRequestHandler.IsAuthorized threw: " + ex.Message)); return false; } if (!flag) { Debug.Log((object)("[ClientLogRelay] Log request from unauthorised Discord user '" + discordUserId + "' on message " + messageId + "; ignoring")); return false; } try { logRequestHandler.HandleRequest(ctx, discordUserId, emoji); return true; } catch (Exception ex2) { Debug.LogWarning((object)("[ClientLogRelay] ILogRequestHandler.HandleRequest threw: " + ex2.Message)); return false; } } } [HarmonyPatch] public static class ClientLogRelayIntegration { [CompilerGenerated] private sealed class <>c__DisplayClass41_0 { public bool playerReady; internal void b__0(Player _) { playerReady = true; } } [CompilerGenerated] private sealed class d__41 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private <>c__DisplayClass41_0 <>8__1; private float 5__2; private long 5__3; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__41(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <>1__state = -2; } private bool MoveNext() { //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass41_0(); Debug.Log((object)"[ClientLogRelay][DIAG] DelayedClientPush ENTER (queueing PlayerSpawnGate.RunWhenLocalReady)"); <>8__1.playerReady = false; PlayerSpawnGate.RunWhenLocalReady(60f, delegate { <>8__1.playerReady = true; }); 5__2 = Time.realtimeSinceStartup + 65f; goto IL_009f; case 1: <>1__state = -1; goto IL_009f; case 2: <>1__state = -1; Debug.Log((object)"[ClientLogRelay][DIAG] DelayedClientPush 2s grace elapsed, about to call PushLogToServerCoroutine"); if ((Object)(object)ZNet.instance == (Object)null) { Debug.Log((object)"[ClientLogRelay] ZNet.instance gone before push — cancelling."); return false; } if (ZRoutedRpc.instance == null) { Debug.LogWarning((object)"[ClientLogRelay] ZRoutedRpc.instance null at push time — cancelling."); return false; } 5__3 = ZRoutedRpc.instance.GetServerPeerID(); if (5__3 == 0) { Debug.LogWarning((object)"[ClientLogRelay] ServerPeerID is 0 at push time — connection no longer valid; cancelling."); return false; } <>2__current = ((MonoBehaviour)ZNet.instance).StartCoroutine(PushLogToServerCoroutine(5__3)); <>1__state = 3; return true; case 3: { <>1__state = -1; return false; } IL_009f: if (!<>8__1.playerReady && Time.realtimeSinceStartup < 5__2) { <>2__current = null; <>1__state = 1; return true; } if (!<>8__1.playerReady) { Debug.LogWarning((object)"[ClientLogRelay] Player not ready within 60s — cancelling log push for this session."); return false; } Debug.Log((object)"[ClientLogRelay][DIAG] DelayedClientPush playerReady=true, entering 2s post-ready grace"); <>2__current = (object)new WaitForSeconds(2f); <>1__state = 2; return true; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__43 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private long 5__1; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__43(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; break; case 1: <>1__state = -1; if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { Debug.Log((object)"[ClientLogRelay] Repush skipped: ZNet or ZRoutedRpc is null (will retry next interval)"); break; } 5__1 = ZRoutedRpc.instance.GetServerPeerID(); if (5__1 == 0) { Debug.Log((object)"[ClientLogRelay] Repush skipped: not connected to server (will retry next interval)"); break; } <>2__current = ((MonoBehaviour)ZNet.instance).StartCoroutine(RepushLogToServerCoroutine(5__1)); <>1__state = 2; return true; case 2: <>1__state = -1; break; } <>2__current = (object)new WaitForSeconds(900f); <>1__state = 1; return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__44 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__44(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; break; case 1: <>1__state = -1; ClientLogChunkedTransfer.CleanupTimedOutTransfers(); break; } <>2__current = (object)new WaitForSeconds(30f); <>1__state = 1; return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__52 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string platformId; public string playerName; public string requestedByName; private Exception 5__1; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__52(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; <>1__state = -2; } private bool MoveNext() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(1f); <>1__state = 1; return true; case 1: <>1__state = -1; try { PostDisconnectLog(platformId, playerName, requestedByName); } catch (Exception ex) { 5__1 = ex; Debug.LogWarning((object)("[ClientLogRelay] PostDisconnectLogAsync threw: " + 5__1.Message)); } return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__42 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public long serverId; private byte[] 5__1; private Dictionary 5__2; private string 5__3; private int 5__4; private int 5__5; private Exception 5__6; private Exception 5__7; private ZPackage 5__8; private byte[] 5__9; private Dictionary.Enumerator <>s__10; private KeyValuePair 5__11; private int 5__12; private ZPackage 5__13; private int 5__14; private int 5__15; private byte[] 5__16; private byte[] 5__17; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__42(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; 5__2 = null; 5__3 = null; 5__6 = null; 5__7 = null; 5__8 = null; 5__9 = null; <>s__10 = default(Dictionary.Enumerator); 5__11 = default(KeyValuePair); 5__13 = null; 5__16 = null; 5__17 = null; <>1__state = -2; } private bool MoveNext() { //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_0211: Expected O, but got Unknown //IL_0433: Unknown result type (might be due to invalid IL or missing references) //IL_043d: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; Debug.Log((object)$"[ClientLogRelay] PushLogToServer entered (serverId={serverId}) - reading local LogOutput.log..."); 5__1 = null; 5__2 = null; try { ClientLogCollector.TryCollect(out 5__1, out 5__2); if (5__2 == null) { 5__2 = new Dictionary(0, StringComparer.OrdinalIgnoreCase); } byte[] array2 = 5__1; Debug.Log((object)$"[ClientLogRelay] PushLogToServer read complete - logBytes={((array2 != null) ? array2.Length : 0)}B, mods={5__2.Count}."); } catch (Exception ex) { 5__6 = ex; Debug.LogWarning((object)("[ClientLogRelay] TryCollect failed: " + 5__6.Message)); return false; } <>2__current = null; <>1__state = 1; return true; case 1: { <>1__state = -1; 5__3 = string.Empty; try { 5__3 = GetOwnSteamId() ?? string.Empty; } catch (Exception ex) { 5__7 = ex; Debug.Log((object)("[ClientLogRelay] SteamID unavailable (" + 5__7.GetType().Name + ": " + 5__7.Message + "); pushing without it.")); } 5__1 = 5__1 ?? new byte[0]; 5__4 = 5__1.Length; 5__5 = ((5__4 == 0) ? 1 : ((5__4 + 409600 - 1) / 409600)); Debug.Log((object)$"[ClientLogRelay] Pushing log to server: {5__4}B in {5__5} chunk(s) + metadata, mods={5__2.Count}"); 5__8 = new ZPackage(); 5__8.Write(-1); 5__8.Write(5__5); 5__8.Write(5__2.Count); <>s__10 = 5__2.GetEnumerator(); try { while (<>s__10.MoveNext()) { 5__11 = <>s__10.Current; 5__8.Write(5__11.Key ?? string.Empty); 5__8.Write(5__11.Value ?? string.Empty); 5__11 = default(KeyValuePair); } } finally { ((IDisposable)<>s__10).Dispose(); } <>s__10 = default(Dictionary.Enumerator); 5__8.Write(5__3 ?? string.Empty); 5__9 = 5__8.GetArray(); if (5__9 != null && 5__9.Length > 420000) { Debug.LogWarning((object)$"[ClientLogRelay] Metadata ZPackage is {5__9.Length}B - very large mod list!"); } if (ZRoutedRpc.instance == null) { Debug.LogWarning((object)"[ClientLogRelay] ZRoutedRpc gone before metadata send - aborting push."); return false; } ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_SubmitClientLog", new object[1] { 5__8 }); byte[] array = 5__9; Debug.Log((object)$"[ClientLogRelay] -> Sent metadata ({((array != null) ? array.Length : 0)}B total, {5__2.Count} mods)"); 5__8 = null; 5__9 = null; <>2__current = null; <>1__state = 2; return true; } case 2: <>1__state = -1; 5__12 = 0; break; case 3: <>1__state = -1; 5__13 = null; 5__16 = null; 5__17 = null; 5__12++; break; } if (5__12 < 5__5) { if (ZRoutedRpc.instance == null) { Debug.LogWarning((object)$"[ClientLogRelay] ZRoutedRpc gone at chunk {5__12 + 1}/{5__5} - aborting push."); return false; } 5__13 = new ZPackage(); 5__13.Write(5__12); 5__13.Write(5__5); 5__14 = 5__12 * 409600; 5__15 = Math.Min(409600, 5__4 - 5__14); 5__16 = new byte[5__15]; if (5__15 > 0) { Array.Copy(5__1, 5__14, 5__16, 0, 5__15); } 5__13.Write(5__16); 5__17 = 5__13.GetArray(); if (5__17 != null && 5__17.Length > 420000) { Debug.LogWarning((object)$"[ClientLogRelay] CRITICAL: Chunk {5__12 + 1} ZPackage is {5__17.Length}B!"); } ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_SubmitClientLog", new object[1] { 5__13 }); if (5__5 > 1) { object[] obj = new object[4] { 5__12 + 1, 5__5, 5__15, null }; byte[] array3 = 5__17; obj[3] = ((array3 != null) ? array3.Length : 0); Debug.Log((object)string.Format("[ClientLogRelay] -> Sent chunk {0}/{1} ({2}B data, {3}B total)", obj)); } <>2__current = null; <>1__state = 3; return true; } Debug.Log((object)$"[ClientLogRelay] Push complete: {5__4}B in {5__5} chunk(s)"); return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__45 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public long serverId; private byte[] 5__1; private string 5__2; private int 5__3; private int 5__4; private Dictionary 5__5; private Exception 5__6; private ZPackage 5__7; private int 5__8; private ZPackage 5__9; private int 5__10; private int 5__11; private byte[] 5__12; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__45(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; 5__2 = null; 5__5 = null; 5__6 = null; 5__7 = null; 5__9 = null; 5__12 = null; <>1__state = -2; } private bool MoveNext() { //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Expected O, but got Unknown //IL_01dc: Unknown result type (might be due to invalid IL or missing references) //IL_01e6: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; 5__1 = null; try { ClientLogCollector.TryCollect(out 5__1, out 5__5); 5__5 = null; } catch (Exception ex) { 5__6 = ex; Debug.LogWarning((object)("[ClientLogRelay] Repush TryCollect failed: " + 5__6.Message)); return false; } <>2__current = null; <>1__state = 1; return true; case 1: <>1__state = -1; 5__2 = string.Empty; try { 5__2 = GetOwnSteamId() ?? string.Empty; } catch { } 5__1 = 5__1 ?? new byte[0]; 5__3 = 5__1.Length; if (5__3 <= 409600) { if (ZRoutedRpc.instance == null) { return false; } 5__7 = new ZPackage(); 5__7.Write(5__1); 5__7.Write(5__2); ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_UpdateClientLog", new object[1] { 5__7 }); Debug.Log((object)string.Format("[ClientLogRelay] Re-pushed '{0}' to server: {1}B (single message)", "VAG_UpdateClientLog", 5__3)); return false; } 5__4 = (5__3 + 409600 - 1) / 409600; Debug.Log((object)$"[ClientLogRelay] Re-pushing large log to server: {5__3}B in {5__4} chunk(s)"); 5__8 = 0; break; case 2: <>1__state = -1; 5__9 = null; 5__12 = null; 5__8++; break; } if (5__8 < 5__4) { if (ZRoutedRpc.instance == null) { return false; } 5__9 = new ZPackage(); 5__9.Write(5__8); 5__9.Write(5__4); 5__10 = 5__8 * 409600; 5__11 = Math.Min(409600, 5__3 - 5__10); 5__12 = new byte[5__11]; if (5__11 > 0) { Array.Copy(5__1, 5__10, 5__12, 0, 5__11); } 5__9.Write(5__12); 5__9.Write(5__2); ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_UpdateClientLog", new object[1] { 5__9 }); if (5__4 > 1) { Debug.Log((object)$"[ClientLogRelay] -> Re-pushed chunk {5__8 + 1}/{5__4} ({5__11}B)"); } <>2__current = null; <>1__state = 2; return true; } Debug.Log((object)$"[ClientLogRelay] Re-push complete: {5__3}B in {5__4} chunk(s)"); return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string MOD_ID = "FiresDiscordIntegration"; private const string PLUGIN_BRAND = "FiresDiscordIntegration"; private const string FallbackWebhookDisplayName = "Server Relay"; private const string RPC_SUBMIT = "VAG_SubmitClientLog"; private const string RPC_UPDATE = "VAG_UpdateClientLog"; private const float CLIENT_PUSH_DELAY_SECONDS = 15f; private const float CLIENT_REPUSH_INTERVAL_SECONDS = 900f; private const float GLOBAL_MIN_POST_INTERVAL_SECONDS = 10f; private static readonly HashSet _postedPeers = new HashSet(); private static readonly object _debounceLock = new object(); private static DateTime _lastPostUtc = DateTime.MinValue; private static bool _clientPushedThisSession; private static bool _initialized; private static bool _isServerSide; private static readonly Dictionary _peerToPlatformId = new Dictionary(); private static readonly Dictionary _peerToPlayerName = new Dictionary(); private static bool MasterEnabled => DiscordIntegrationConfig.NotifyClientLoginArtifacts?.Value ?? true; private static string PrimaryWebhookUrl => DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.ClientLog) ?? string.Empty; private static string SecondaryWebhookUrl => string.Empty; private static bool AutoSendFullLog => DiscordIntegrationConfig.AttachFullClientLogOnLogin?.Value ?? false; private static string BotTokenValue => BotTokenFile.Token; private static string ServerManagerWebhookUrl => DiscordIntegrationConfig.ServerManagerWebhookUrl?.Value ?? string.Empty; private static string ServerManagerChannelIdValue { get { string text = DiscordIntegrationConfig.ServerManagerChannelId?.Value; if (!string.IsNullOrEmpty(text)) { return text; } return DiscordIntegrationConfig.StatusChannelId?.Value ?? string.Empty; } } public static string ClientLogsRoot => ClientLogRelayPaths.GetDefaultClientLogsDir("FiresDiscordIntegration"); private static string ResolveWebhookName() { string text = DiscordIntegrationConfig.WebhookDisplayName?.Value; string fallback = ((!string.IsNullOrEmpty(text) && text != "Valheim Server") ? text : "Server Relay"); return DiscordBotIdentity.ResolveUsername(fallback); } private static string ResolveAvatarUrl() { string text = DiscordIntegrationConfig.WebhookAvatarUrl?.Value; if (!string.IsNullOrEmpty(text)) { return text; } return DiscordBotIdentity.ResolveAvatarUrl(null); } public static void InitServer() { if (!_initialized) { _initialized = true; _isServerSide = true; if (!MasterEnabled) { Debug.Log((object)"[ClientLogRelay] Disabled by config (Discord.ClientLogs.NotifyClientLoginArtifacts=false) - skipping initialisation."); return; } TryRun("RegisterDiskConsumer", RegisterDiskConsumer); TryRun("RegisterDiscordConsumer", RegisterDiscordConsumer); TryRun("StartReactionPoller", StartReactionPoller); TryRun("StartServerStatusPoster", StartServerStatusPoster); Debug.Log((object)"[ClientLogRelay] Server-side integration initialised."); } } public static void InitClient() { if (!_initialized) { _initialized = true; _isServerSide = false; Debug.Log((object)"[ClientLogRelay] Client-side integration initialised (will push log on connect)."); } } private static void TryRun(string label, Action action) { try { action(); } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] " + label + " failed: " + ex.GetType().Name + ": " + ex.Message)); } } private static void RegisterDiskConsumer() { DiskConsumer consumer = new DiskConsumer("FiresDiscordIntegration.ClientLogRelay.Disk", () => ClientLogRelayPaths.GetDefaultClientLogsDir("FiresDiscordIntegration"), () => true); ClientLogRelay.RegisterConsumer(consumer); Debug.Log((object)("[ClientLogRelay] Disk consumer will persist artifacts under: " + ClientLogsRoot)); } private static void RegisterDiscordConsumer() { DiscordWebhookConsumer discordWebhookConsumer = new DiscordWebhookConsumer("FiresDiscordIntegration.ClientLogRelay.Discord.Primary"); discordWebhookConsumer.EnabledGate = () => MasterEnabled && !string.IsNullOrEmpty(PrimaryWebhookUrl); discordWebhookConsumer.WebhookUrl = () => PrimaryWebhookUrl; discordWebhookConsumer.WebhookName = ResolveWebhookName; discordWebhookConsumer.AvatarUrl = ResolveAvatarUrl; discordWebhookConsumer.BrandLabel = () => "FiresDiscordIntegration"; discordWebhookConsumer.AttachFullLog = () => AutoSendFullLog; discordWebhookConsumer.AttachModList = () => true; discordWebhookConsumer.AttachErrorsWarnings = () => true; discordWebhookConsumer.OnlyIfErrorsOrWarnings = () => false; discordWebhookConsumer.EnableLogRequestReaction = ReactionEnabled; discordWebhookConsumer.BotToken = () => BotTokenValue; discordWebhookConsumer.ServerName = () => ((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetWorldName() : "Unknown"; DiscordWebhookConsumer consumer = discordWebhookConsumer; ClientLogRelay.RegisterConsumer(consumer); discordWebhookConsumer = new DiscordWebhookConsumer("FiresDiscordIntegration.ClientLogRelay.Discord.Secondary"); discordWebhookConsumer.EnabledGate = () => MasterEnabled && !string.IsNullOrEmpty(SecondaryWebhookUrl); discordWebhookConsumer.WebhookUrl = () => SecondaryWebhookUrl; discordWebhookConsumer.WebhookName = ResolveWebhookName; discordWebhookConsumer.AvatarUrl = ResolveAvatarUrl; discordWebhookConsumer.BrandLabel = () => "FiresDiscordIntegration"; discordWebhookConsumer.AttachFullLog = () => AutoSendFullLog; discordWebhookConsumer.AttachModList = () => true; discordWebhookConsumer.AttachErrorsWarnings = () => true; discordWebhookConsumer.OnlyIfErrorsOrWarnings = () => false; discordWebhookConsumer.EnableLogRequestReaction = () => false; discordWebhookConsumer.BotToken = () => null; discordWebhookConsumer.ServerName = () => ((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetWorldName() : "Unknown"; DiscordWebhookConsumer consumer2 = discordWebhookConsumer; ClientLogRelay.RegisterConsumer(consumer2); static bool ReactionEnabled() { return !AutoSendFullLog; } } private static void StartReactionPoller() { string botTokenValue = BotTokenValue; if (string.IsNullOrEmpty(botTokenValue)) { Debug.Log((object)"[ClientLogRelay] No bot token configured \ufffd reaction poller disabled."); return; } if (string.IsNullOrEmpty(PrimaryWebhookUrl)) { Debug.Log((object)"[ClientLogRelay] No primary webhook URL configured — reaction poller disabled."); return; } ReactionPoller.Create(() => BotTokenValue, () => PrimaryWebhookUrl, () => ClientLogsRoot, ResolveWebhookName); Debug.Log((object)"[ClientLogRelay] Reaction poller started — polling every 15s for log requests."); } private static void StartServerStatusPoster() { string botTokenValue = BotTokenValue; string serverManagerChannelIdValue = ServerManagerChannelIdValue; if (string.IsNullOrEmpty(botTokenValue)) { Debug.Log((object)"[ClientLogRelay] No bot token configured (Discord.BotListener.BotToken) - Server Manager panel disabled."); } else if (string.IsNullOrEmpty(serverManagerChannelIdValue)) { Debug.Log((object)"[ClientLogRelay] No channel ID configured (Discord.ServerManager.ServerManagerChannelId or Discord.BotListener.StatusChannelId) - Server Manager panel disabled."); } else { Debug.Log((object)"[ClientLogRelay] Server Manager panel configured - will start when ZNet is ready."); } } [HarmonyPatch(typeof(ZNet), "Awake")] [HarmonyPostfix] public static void ZNet_Awake_Postfix(ZNet __instance) { try { if (!_initialized) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { InitServer(); } else { InitClient(); } } if (ZRoutedRpc.instance == null) { Debug.LogWarning((object)"[ClientLogRelay] ZNet.Awake postfix fired but ZRoutedRpc.instance is null - RPC registration skipped."); } else if (_isServerSide) { ZRoutedRpc.instance.Register("VAG_SubmitClientLog", (Action)OnServerReceiveLog); ZRoutedRpc.instance.Register("VAG_UpdateClientLog", (Action)OnServerReceiveLogUpdate); Debug.Log((object)"[ClientLogRelay] Registered server-side handlers for 'VAG_SubmitClientLog' + 'VAG_UpdateClientLog'."); ((MonoBehaviour)ZNet.instance).StartCoroutine(PeriodicTransferCleanup()); string botTokenValue = BotTokenValue; string serverManagerWebhookUrl = ServerManagerWebhookUrl; string serverManagerChannelIdValue = ServerManagerChannelIdValue; if (!string.IsNullOrEmpty(botTokenValue) && !string.IsNullOrEmpty(serverManagerChannelIdValue)) { ServerHeartbeat.OnServerStart(botTokenValue, serverManagerWebhookUrl, serverManagerChannelIdValue, "FiresDiscordIntegration Relay", "FiresDiscordIntegration"); Debug.Log((object)"[ClientLogRelay] Server heartbeat + control panel started."); } } else { _clientPushedThisSession = false; Debug.Log((object)"[ClientLogRelay] Client-side ZNet awake — push-on-connect armed."); } } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] ZNet.Awake postfix threw: " + ex.GetType().Name + ": " + ex.Message)); } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPostfix] public static void ZNet_RPC_PeerInfo_Postfix(ZNet __instance, ZRpc rpc) { try { if (!_isServerSide && !((Object)(object)__instance == (Object)null) && rpc != null) { if (_clientPushedThisSession) { Debug.Log((object)"[ClientLogRelay] PeerInfo postfix fired but client already pushed this session — skipping."); return; } _clientPushedThisSession = true; Debug.Log((object)$"[ClientLogRelay] PeerInfo handshake complete — scheduling log push in {15f:F0}s."); ((MonoBehaviour)__instance).StartCoroutine(DelayedClientPush()); ((MonoBehaviour)__instance).StartCoroutine(PeriodicClientRepush()); } } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] ZNet.RPC_PeerInfo postfix threw: " + ex.GetType().Name + ": " + ex.Message)); } } [IteratorStateMachine(typeof(d__41))] private static IEnumerator DelayedClientPush() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__41(0); } [IteratorStateMachine(typeof(d__42))] private static IEnumerator PushLogToServerCoroutine(long serverId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__42(0) { serverId = serverId }; } [IteratorStateMachine(typeof(d__43))] private static IEnumerator PeriodicClientRepush() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__43(0); } [IteratorStateMachine(typeof(d__44))] private static IEnumerator PeriodicTransferCleanup() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__44(0); } [IteratorStateMachine(typeof(d__45))] private static IEnumerator RepushLogToServerCoroutine(long serverId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__45(0) { serverId = serverId }; } public static void ResetDebounce() { lock (_debounceLock) { int count = _postedPeers.Count; _postedPeers.Clear(); _lastPostUtc = DateTime.MinValue; Debug.Log((object)$"[ClientLogRelay] Debounce reset (cleared {count} posted peers)."); } } private static void OnServerReceiveLog(long sender, ZPackage pkg) { try { if (pkg == null) { Debug.LogWarning((object)$"[ClientLogRelay] Submit from peer {sender} had a null payload."); return; } int num = pkg.ReadInt(); int num2 = pkg.ReadInt(); if (num == -1) { int num3 = pkg.ReadInt(); if (num3 < 0) { num3 = 0; } Dictionary dictionary = new Dictionary(num3, StringComparer.OrdinalIgnoreCase); for (int i = 0; i < num3; i++) { string text = pkg.ReadString(); string text2 = pkg.ReadString(); if (!string.IsNullOrEmpty(text)) { dictionary[text] = text2 ?? string.Empty; } } string steamId = null; try { steamId = pkg.ReadString(); } catch { } ClientLogChunkedTransfer.StoreMetadata(sender, num2, dictionary, steamId); Debug.Log((object)$"[ClientLogRelay] ? Received metadata from peer {sender}: {dictionary.Count} mods, expecting {num2} chunk(s)"); return; } byte[] array = pkg.ReadByteArray(); Debug.Log((object)$"[ClientLogRelay] ? Received chunk {num + 1}/{num2} from peer {sender} ({array.Length}B)"); ClientLogChunkedTransfer.TransferResult transferResult = ClientLogChunkedTransfer.ReceiveChunk(sender, num, num2, array); if (transferResult == null) { return; } byte[] logBytes = transferResult.LogBytes; Dictionary dictionary2 = transferResult.ModList; string steamId2 = transferResult.SteamId; if (dictionary2 == null) { dictionary2 = new Dictionary(0, StringComparer.OrdinalIgnoreCase); } Debug.Log((object)$"[ClientLogRelay] ? Complete log received from peer {sender}: {logBytes.Length}B, {dictionary2.Count} mods"); string peerPlayerName = GetPeerPlayerName(sender); string text3 = ((!string.IsNullOrEmpty(steamId2)) ? steamId2 : sender.ToString()); lock (_debounceLock) { if (!_postedPeers.Add(sender)) { Debug.Log((object)("[ClientLogRelay] Dropping duplicate submission from '" + peerPlayerName + "' (" + text3 + ") — already posted this session.")); return; } TimeSpan timeSpan = DateTime.UtcNow - _lastPostUtc; if (timeSpan.TotalSeconds < 10.0) { _postedPeers.Remove(sender); Debug.Log((object)$"[ClientLogRelay] Rate-limiting post from '{peerPlayerName}' ({text3}); {timeSpan.TotalSeconds:F1}s since last post (floor {10f:F0}s)."); return; } _lastPostUtc = DateTime.UtcNow; } Dictionary dictionary3 = null; try { dictionary3 = ClientLogCollector.BuildLocalModList(); } catch (Exception ex) { Debug.Log((object)("[ClientLogRelay] Could not enumerate server-side mod list: " + ex.GetType().Name + ": " + ex.Message)); } ClientLogArtifacts artifacts = new ClientLogArtifacts(text3, peerPlayerName, logBytes, dictionary2, dictionary3, "FiresDiscordIntegration"); ClientLogRelay.ReportArtifacts(artifacts); lock (_debounceLock) { _peerToPlatformId[sender] = text3; _peerToPlayerName[sender] = peerPlayerName; } Debug.Log((object)$"[ClientLogRelay] Processed log from '{peerPlayerName}' ({text3}): {logBytes.Length} bytes, {dictionary2.Count} client mods, {dictionary3?.Count ?? 0} server mods."); } catch (Exception ex2) { Debug.LogWarning((object)("[ClientLogRelay] Failed to process submitted log: " + ex2.Message)); } } private static void OnServerReceiveLogUpdate(long sender, ZPackage pkg) { try { if (pkg == null) { return; } pkg.SetPos(0); int num = pkg.ReadInt(); pkg.SetPos(0); string text = null; byte[] array2; if (num >= 0 && num < 1000) { int num2 = pkg.ReadInt(); int num3 = pkg.ReadInt(); byte[] array = pkg.ReadByteArray(); try { text = pkg.ReadString(); } catch { } Debug.Log((object)$"[ClientLogRelay] ? Received update chunk {num2 + 1}/{num3} from peer {sender} ({array.Length}B)"); ClientLogChunkedTransfer.TransferResult transferResult = ClientLogChunkedTransfer.ReceiveUpdateChunk(sender, num2, num3, array, text); if (transferResult == null) { return; } array2 = transferResult.LogBytes; text = transferResult.SteamId; Debug.Log((object)$"[ClientLogRelay] ? Complete update received from peer {sender}: {array2.Length}B"); } else { array2 = pkg.ReadByteArray(); try { text = pkg.ReadString(); } catch { } } string peerPlayerName = GetPeerPlayerName(sender); string text2 = ((!string.IsNullOrEmpty(text)) ? text : sender.ToString()); if (array2 == null || array2.Length == 0) { Debug.Log((object)("[ClientLogRelay] Update from '" + peerPlayerName + "' (" + text2 + ") had empty log \ufffd skipping.")); return; } string clientLogsRoot = ClientLogsRoot; if (!string.IsNullOrEmpty(clientLogsRoot)) { Dictionary dictionary = new Dictionary(0, StringComparer.OrdinalIgnoreCase); string text3 = peerPlayerName ?? "unknown"; char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); text3 = string.Concat(text3.Split(invalidFileNameChars)); string text4 = (text2 ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_"); string path = Path.Combine(clientLogsRoot, text3 + "_" + text4, "modlist.txt"); if (File.Exists(path)) { try { string[] array3 = File.ReadAllLines(path); foreach (string text5 in array3) { if (!string.IsNullOrEmpty(text5) && !text5.StartsWith("#")) { int num4 = text5.IndexOf('='); if (num4 > 0) { dictionary[text5.Substring(0, num4)] = text5.Substring(num4 + 1); } } } } catch { } } Dictionary dictionary2 = null; try { dictionary2 = ClientLogCollector.BuildLocalModList(); } catch { } ClientLogArtifacts clientLogArtifacts = new ClientLogArtifacts(text2, peerPlayerName, array2, dictionary, dictionary2, "FiresDiscordIntegration"); try { LogErrorWarningExtractor.Result result = LogErrorWarningExtractor.Extract(array2, peerPlayerName, text2); clientLogArtifacts.ErrorsWarningsReport = result.Report; clientLogArtifacts.ErrorCount = result.ErrorCount; clientLogArtifacts.WarningCount = result.WarningCount; clientLogArtifacts.SourceBreakdown = result.SourceBreakdown; } catch { } if (dictionary2 != null && dictionary2.Count > 0 && dictionary.Count > 0) { try { clientLogArtifacts.ModDiff = ModListDiff.Compute(dictionary, dictionary2, peerPlayerName, text2, "FiresDiscordIntegration", clientLogArtifacts.CapturedUtc); } catch { } } ClientLogArtifactWriter.Write(clientLogsRoot, clientLogArtifacts); } lock (_debounceLock) { _peerToPlatformId[sender] = text2; _peerToPlayerName[sender] = peerPlayerName; } Debug.Log((object)$"[ClientLogRelay] ← Updated all cached artifacts for '{peerPlayerName}' ({text2}): {array2.Length} bytes."); } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] Failed to process log update: " + ex.Message)); } } [HarmonyPatch(typeof(ZNet), "Disconnect")] [HarmonyPrefix] public static void ZNet_Disconnect_Prefix(ZNetPeer peer) { try { if (!_isServerSide || peer == null) { return; } ClientLogChunkedTransfer.CancelTransfer(peer.m_uid); string value; string value2; lock (_debounceLock) { _peerToPlatformId.TryGetValue(peer.m_uid, out value); _peerToPlayerName.TryGetValue(peer.m_uid, out value2); _peerToPlatformId.Remove(peer.m_uid); _peerToPlayerName.Remove(peer.m_uid); _postedPeers.Remove(peer.m_uid); } if (string.IsNullOrEmpty(value)) { value = peer.m_uid.ToString(); } if (string.IsNullOrEmpty(value2)) { value2 = (string.IsNullOrEmpty(peer.m_playerName) ? "unknown" : peer.m_playerName); } DisconnectLogRegistry.Entry entry = DisconnectLogRegistry.TakeIfRegistered(value); if (entry != null) { Debug.Log((object)("[ClientLogRelay] Player '" + value2 + "' (" + value + ") disconnected \ufffd posting session log (requested by " + entry.RequestedByName + ").")); if ((Object)(object)ZNet.instance != (Object)null) { ((MonoBehaviour)ZNet.instance).StartCoroutine(PostDisconnectLogAsync(value, value2, entry.RequestedByName)); } } } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] Disconnect hook threw: " + ex.Message)); } } [IteratorStateMachine(typeof(d__52))] private static IEnumerator PostDisconnectLogAsync(string platformId, string playerName, string requestedByName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__52(0) { platformId = platformId, playerName = playerName, requestedByName = requestedByName }; } private static void PostDisconnectLog(string platformId, string playerName, string requestedByName) { try { string clientLogsRoot = ClientLogsRoot; if (string.IsNullOrEmpty(clientLogsRoot)) { return; } string text = playerName ?? "unknown"; char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); text = string.Concat(text.Split(invalidFileNameChars)); string text2 = (platformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_"); string text3 = Path.Combine(clientLogsRoot, text + "_" + text2, "LogOutput.log"); if (!File.Exists(text3)) { Debug.LogWarning((object)("[ClientLogRelay] No cached log at '" + text3 + "' for disconnect post.")); return; } byte[] array = File.ReadAllBytes(text3); if (array.Length == 0) { return; } List list = new List(2); string[] array2 = new string[2] { PrimaryWebhookUrl, SecondaryWebhookUrl }; foreach (string text4 in array2) { if (!string.IsNullOrEmpty(text4) && MinimalWebhookPoster.IsValidWebhookUrl(text4)) { list.Add(text4); } } if (list.Count == 0) { return; } string text5 = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss"); MinimalWebhookPoster.Embed embed = new MinimalWebhookPoster.Embed().SetTitle("♻\ufe0f Session Log — Player Disconnected").SetColor(3066993).AddField("\ud83d\udc64 Player", playerName, inline: true) .AddField("\ud83d\udd94 Steam ID", platformId ?? "?", inline: true) .AddField("\ud83d\udcc4 Log Size", FormatBytes(array.Length), inline: true) .SetFooter("Requested by " + requestedByName); if (array.Length <= 8323072) { List list2 = new List(); list2.Add(new MinimalWebhookPoster.Attachment("session_log_" + text2 + "_" + text5 + ".log", array)); List files = list2; foreach (string item in list) { MinimalWebhookPoster.Post(item, embed, files, ResolveWebhookName()); } } else { int num = 0; int num2 = 0; while (num2 < array.Length) { num++; int num3 = Math.Min(8323072, array.Length - num2); byte[] array3 = new byte[num3]; Buffer.BlockCopy(array, num2, array3, 0, num3); num2 += num3; int num4 = (array.Length + 8323072 - 1) / 8323072; MinimalWebhookPoster.Embed embed2 = new MinimalWebhookPoster.Embed().SetTitle($"♻\ufe0f Session Log — Part {num}/{num4}").SetColor(3066993).AddField("\ud83d\udc64 Player", playerName, inline: true) .AddField("\ud83d\udcc4 Size", FormatBytes(array3.Length), inline: true) .SetFooter("Requested by " + requestedByName); List files2 = new List { new MinimalWebhookPoster.Attachment($"session_log_{text2}_{text5}_part{num}.log", array3) }; foreach (string item2 in list) { MinimalWebhookPoster.Post(item2, embed2, files2, ResolveWebhookName()); } } } Debug.Log((object)$"[ClientLogRelay] Posted disconnect session log for '{playerName}' ({platformId}), {array.Length} bytes."); } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] Failed to post disconnect log: " + ex.Message)); } } private static string FormatBytes(long bytes) { if (bytes <= 0) { return "0 B"; } string[] array = new string[4] { "B", "KB", "MB", "GB" }; double num = bytes; int num2 = 0; while (num >= 1024.0 && num2 < array.Length - 1) { num /= 1024.0; num2++; } return $"{num:0.##} {array[num2]}"; } private static string GetPeerPlayerName(long uid) { if ((Object)(object)ZNet.instance == (Object)null) { return "unknown"; } foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer != null && peer.m_uid == uid) { return string.IsNullOrEmpty(peer.m_playerName) ? "unknown" : peer.m_playerName; } } return "unknown"; } private static string GetOwnSteamId() { try { Type type = Type.GetType("Steamworks.SteamAPI, Steamworks.NET") ?? AccessTools.TypeByName("Steamworks.SteamAPI"); Type type2 = Type.GetType("Steamworks.SteamUser, Steamworks.NET") ?? AccessTools.TypeByName("Steamworks.SteamUser"); if (type == null || type2 == null) { return string.Empty; } MethodInfo method = type.GetMethod("IsSteamRunning", BindingFlags.Static | BindingFlags.Public); if (method == null) { return string.Empty; } if (!(method.Invoke(null, null) is bool flag) || !flag) { return string.Empty; } MethodInfo method2 = type2.GetMethod("GetSteamID", BindingFlags.Static | BindingFlags.Public); if (method2 == null) { return string.Empty; } object obj = method2.Invoke(null, null); if (obj == null) { return string.Empty; } FieldInfo field = obj.GetType().GetField("m_SteamID", BindingFlags.Instance | BindingFlags.Public); if (field != null) { object value = field.GetValue(obj); if (value != null) { return value.ToString(); } } return obj.ToString(); } catch { } return string.Empty; } } public static class ClientLogRelayPaths { public const string DefaultFolderName = "ClientLogs"; public static string GetDefaultClientLogsDir(string modId) { if (string.IsNullOrEmpty(modId)) { throw new ArgumentException("modId must be provided", "modId"); } string text = Path.Combine(Paths.ConfigPath, modId, "ClientLogs"); TryEnsureDirectory(text); return text; } public static string GetDefaultClientLogsDir(string modId, string subfolder) { string defaultClientLogsDir = GetDefaultClientLogsDir(modId); if (string.IsNullOrEmpty(subfolder)) { return defaultClientLogsDir; } string text = Path.Combine(defaultClientLogsDir, subfolder); TryEnsureDirectory(text); return text; } private static void TryEnsureDirectory(string dir) { try { if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] Failed to create directory '" + dir + "': " + ex.Message)); } } } public interface IClientLogConsumer { string ConsumerId { get; } void OnClientArtifacts(ClientLogArtifacts artifacts); } public static class LogErrorWarningExtractor { public struct SourceIssueCount { public string SourceTag; public int ErrorCount; public int WarningCount; } public struct Result { public string Report; public int ErrorCount; public int WarningCount; public int BenignSkipped; public int DuplicatesCollapsed; public List SourceBreakdown; } private sealed class SourceGroup { public string Source; public int Errors; public int Warnings; public List ErrorLines = new List(); public List WarningLines = new List(); } public static readonly List BenignPatterns = new List { "Failed to find expected binary shader data", "The texture is not suitable to be used as a single mip level texture", "The AssetBundle", "audio clip could not be loaded" }; public static Result Extract(byte[] logBytes, string playerName, string platformId) { Result result = default(Result); if (logBytes == null || logBytes.Length == 0) { result.Report = BuildHeader(playerName, platformId, 0, 0, 0, 0) + "# (no client log available)\n"; return result; } Dictionary dictionary = new Dictionary(StringComparer.Ordinal); List list = new List(); Dictionary dictionary2 = new Dictionary(StringComparer.Ordinal); List list2 = new List(); List list3 = new List(); try { string @string = Encoding.UTF8.GetString(logBytes); string[] array = @string.Split(new char[2] { '\r', '\n' }, StringSplitOptions.None); bool flag = false; string text = null; bool item = false; foreach (string text2 in array) { if (string.IsNullOrEmpty(text2)) { flag = false; text = null; continue; } bool flag2 = text2.IndexOf("[Error", StringComparison.OrdinalIgnoreCase) >= 0 || text2.IndexOf("Exception:", StringComparison.Ordinal) >= 0 || text2.IndexOf("NullReferenceException", StringComparison.Ordinal) >= 0 || text2.IndexOf("ArgumentException", StringComparison.Ordinal) >= 0 || text2.IndexOf("InvalidOperationException", StringComparison.Ordinal) >= 0; bool flag3 = !flag2 && text2.IndexOf("[Warning", StringComparison.OrdinalIgnoreCase) >= 0; if (!flag2 && !flag3) { if (flag && text != null && IsStackTraceContinuation(text2)) { list2.Add(text2); list3.Add(item); } else { flag = false; text = null; } continue; } if (IsBenign(text2)) { result.BenignSkipped++; flag = false; text = null; continue; } string text3 = NormalizeForDedup(text2); if (dictionary.TryGetValue(text3, out var value)) { dictionary[text3] = value + 1; result.DuplicatesCollapsed++; flag = false; text = null; continue; } dictionary[text3] = 1; list.Add(text3); dictionary2[text3] = list2.Count; list2.Add(text2); list3.Add(flag2); if (flag2) { result.ErrorCount++; flag = true; item = true; } else { result.WarningCount++; flag = false; item = false; } text = text3; } foreach (string item2 in list) { int num = dictionary[item2]; if (num > 1) { list2[dictionary2[item2]] += $" [repeated {num} times]"; } } } catch (Exception ex) { list2.Add("# Parser failure: " + ex.Message); list3.Add(item: true); } List list4 = GroupBySource(list2, list3); result.SourceBreakdown = new List(list4.Count); foreach (SourceGroup item3 in list4) { result.SourceBreakdown.Add(new SourceIssueCount { SourceTag = item3.Source, ErrorCount = item3.Errors, WarningCount = item3.Warnings }); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(BuildHeader(playerName, platformId, result.ErrorCount, result.WarningCount, result.BenignSkipped, result.DuplicatesCollapsed)); if (list4.Count > 0) { stringBuilder.AppendLine("# ??????????????????????????????????????????????????????????"); stringBuilder.AppendLine("# PER-SOURCE BREAKDOWN"); stringBuilder.AppendLine("# ??????????????????????????????????????????????????????????"); stringBuilder.AppendLine(); foreach (SourceGroup item4 in from g in list4 orderby g.Errors descending, g.Warnings descending select g) { string source = item4.Source; stringBuilder.AppendLine(string.Format("## {0} ({1} error{2}, {3} warning{4})", source, item4.Errors, (item4.Errors == 1) ? "" : "s", item4.Warnings, (item4.Warnings == 1) ? "" : "s")); if (item4.ErrorLines.Count > 0) { stringBuilder.AppendLine(" [ERRORS]"); foreach (string errorLine in item4.ErrorLines) { stringBuilder.Append(" ").AppendLine(errorLine); } } if (item4.WarningLines.Count > 0) { stringBuilder.AppendLine(" [WARNINGS]"); foreach (string warningLine in item4.WarningLines) { stringBuilder.Append(" ").AppendLine(warningLine); } } stringBuilder.AppendLine(); } } stringBuilder.AppendLine("# ??????????????????????????????????????????????????????????"); stringBuilder.AppendLine("# FULL CHRONOLOGICAL LIST"); stringBuilder.AppendLine("# ??????????????????????????????????????????????????????????"); stringBuilder.AppendLine(); foreach (string item5 in list2) { stringBuilder.AppendLine(item5); } result.Report = stringBuilder.ToString(); return result; } private static List GroupBySource(List lines, List isError) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < lines.Count; i++) { string line = lines[i]; if (!IsStackTraceContinuation(line)) { string text = ExtractSourceTag(line); if (string.IsNullOrEmpty(text)) { text = "(General / Unity)"; } if (!dictionary.TryGetValue(text, out var value)) { value = (dictionary[text] = new SourceGroup { Source = text }); } string item = TrimBepInExPrefix(line); if (isError[i]) { value.Errors++; value.ErrorLines.Add(item); } else { value.Warnings++; value.WarningLines.Add(item); } } } return dictionary.Values.ToList(); } private static string ExtractSourceTag(string line) { if (string.IsNullOrEmpty(line)) { return null; } int num = line.IndexOf('['); if (num < 0) { return null; } int num2 = line.IndexOf(']', num + 1); if (num2 < 0) { return null; } string text = line.Substring(num + 1, num2 - num - 1).Trim(); int num3 = text.IndexOf(':'); if (num3 >= 0 && num3 < text.Length - 1) { string text2 = text.Substring(num3 + 1).Trim(); if (text2.Length >= 3 && !string.Equals(text2, "Unity Log", StringComparison.OrdinalIgnoreCase)) { return text2; } } string text3 = line.Substring(num2 + 1); int num4 = text3.IndexOf('['); if (num4 >= 0) { int num5 = text3.IndexOf(']', num4 + 1); if (num5 > num4) { string text4 = text3.Substring(num4 + 1, num5 - num4 - 1).Trim(); if (text4.Length >= 3 && !char.IsDigit(text4[0]) && !text4.StartsWith("repeated ", StringComparison.OrdinalIgnoreCase)) { return text4; } } } string text5 = text3.TrimStart(Array.Empty()); int num6 = text5.IndexOf(": ", StringComparison.Ordinal); if (num6 > 0 && num6 <= 60) { string text6 = text5.Substring(0, num6).Trim(); if (text6.Length >= 3 && text6.IndexOf(' ') < 0) { return text6; } } return null; } private static string TrimBepInExPrefix(string line) { int num = line.IndexOf("] ", StringComparison.Ordinal); if (num >= 0 && num + 2 < line.Length) { return line.Substring(num + 2); } return line; } private static string BuildHeader(string playerName, string platformId, int errorCount, int warningCount, int benignSkipped, int duplicatesCollapsed) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Client Errors + Warnings Report"); stringBuilder.AppendLine("# Player: " + (playerName ?? "unknown")); stringBuilder.AppendLine("# SteamID: " + (platformId ?? "unknown")); stringBuilder.AppendLine($"# Captured: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); stringBuilder.AppendLine($"# Errors: {errorCount}"); stringBuilder.AppendLine($"# Warnings: {warningCount}"); if (benignSkipped > 0) { stringBuilder.AppendLine($"# Benign: {benignSkipped} (filtered — known-harmless patterns)"); } if (duplicatesCollapsed > 0) { stringBuilder.AppendLine($"# Deduped: {duplicatesCollapsed} (repeated duplicates collapsed)"); } stringBuilder.AppendLine(); return stringBuilder.ToString(); } private static bool IsBenign(string line) { if (string.IsNullOrEmpty(line)) { return false; } for (int i = 0; i < BenignPatterns.Count; i++) { if (line.IndexOf(BenignPatterns[i], StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } private static bool IsStackTraceContinuation(string line) { return line.StartsWith(" at ", StringComparison.Ordinal) || line.StartsWith("Stack trace:", StringComparison.Ordinal) || line.StartsWith("UnityEngine.", StringComparison.Ordinal) || line.StartsWith("System.", StringComparison.Ordinal) || line.StartsWith("\t", StringComparison.Ordinal) || line.StartsWith("(wrapper ", StringComparison.Ordinal); } private static string NormalizeForDedup(string line) { if (string.IsNullOrEmpty(line)) { return line ?? string.Empty; } int num = line.IndexOf("] ", StringComparison.Ordinal); string text = ((num >= 0 && num + 2 <= line.Length) ? line.Substring(num + 2) : line); if (text.Length > 2 && text[0] == '[') { int num2 = text.IndexOf("] ", StringComparison.Ordinal); if (num2 > 0 && num2 < 30) { text = text.Substring(num2 + 2); } } return text.Trim(); } } public static class ModListDiff { public sealed class VersionMismatch { public string Guid; public string ClientVersion; public string ServerVersion; } public sealed class Result { public List ClientOnly = new List(); public List ServerOnly = new List(); public List VersionMismatches = new List(); public int SharedMatching; public string Report; } public static Result Compute(IReadOnlyDictionary clientMods, IReadOnlyDictionary serverMods, string playerName, string platformId, string brandLabel, DateTime capturedUtc) { Result result = new Result(); if (clientMods == null) { clientMods = new Dictionary(0, StringComparer.OrdinalIgnoreCase); } if (serverMods == null) { serverMods = new Dictionary(0, StringComparer.OrdinalIgnoreCase); } Dictionary dictionary = new Dictionary(clientMods.Count, StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair clientMod in clientMods) { dictionary[clientMod.Key] = clientMod.Value; } Dictionary dictionary2 = new Dictionary(serverMods.Count, StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair serverMod in serverMods) { dictionary2[serverMod.Key] = serverMod.Value; } foreach (KeyValuePair item in dictionary) { if (!dictionary2.TryGetValue(item.Key, out var value)) { result.ClientOnly.Add(item.Key); } else if (!string.Equals(item.Value ?? string.Empty, value ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { result.VersionMismatches.Add(new VersionMismatch { Guid = item.Key, ClientVersion = (item.Value ?? string.Empty), ServerVersion = (value ?? string.Empty) }); } else { result.SharedMatching++; } } foreach (KeyValuePair item2 in dictionary2) { if (!dictionary.ContainsKey(item2.Key)) { result.ServerOnly.Add(item2.Key); } } result.ClientOnly.Sort(StringComparer.OrdinalIgnoreCase); result.ServerOnly.Sort(StringComparer.OrdinalIgnoreCase); result.VersionMismatches = result.VersionMismatches.OrderBy((VersionMismatch v) => v.Guid, StringComparer.OrdinalIgnoreCase).ToList(); result.Report = Render(result, dictionary, dictionary2, playerName, platformId, brandLabel, capturedUtc); return result; } private static string Render(Result r, IReadOnlyDictionary client, IReadOnlyDictionary server, string playerName, string platformId, string brandLabel, DateTime capturedUtc) { StringBuilder stringBuilder = new StringBuilder(); string text = (string.IsNullOrEmpty(brandLabel) ? "ClientLogRelay" : brandLabel); stringBuilder.AppendLine("# " + text + " — Client vs Server Mod List Diff"); stringBuilder.AppendLine("# Player: " + (playerName ?? "unknown")); stringBuilder.AppendLine("# PlatformID: " + (platformId ?? "unknown")); stringBuilder.AppendLine($"# Captured: {capturedUtc:yyyy-MM-dd HH:mm:ss} UTC"); stringBuilder.AppendLine($"# Client mods: {client.Count}"); stringBuilder.AppendLine($"# Server mods: {server.Count}"); stringBuilder.AppendLine($"# Shared matching: {r.SharedMatching}"); stringBuilder.AppendLine($"# Client-only: {r.ClientOnly.Count}"); stringBuilder.AppendLine($"# Server-only: {r.ServerOnly.Count}"); stringBuilder.AppendLine($"# Version mismatches: {r.VersionMismatches.Count}"); stringBuilder.AppendLine(); if (r.VersionMismatches.Count > 0) { stringBuilder.AppendLine("## Version mismatches (client ? server)"); foreach (VersionMismatch versionMismatch in r.VersionMismatches) { stringBuilder.AppendLine($" {versionMismatch.Guid,-55} client={versionMismatch.ClientVersion} server={versionMismatch.ServerVersion}"); } stringBuilder.AppendLine(); } if (r.ClientOnly.Count > 0) { stringBuilder.AppendLine("## Client-only (installed on client, missing on server)"); foreach (string item in r.ClientOnly) { stringBuilder.AppendLine($" {item,-55} version={SafeLookup(client, item)}"); } stringBuilder.AppendLine(); } if (r.ServerOnly.Count > 0) { stringBuilder.AppendLine("## Server-only (installed on server, missing on client)"); foreach (string item2 in r.ServerOnly) { stringBuilder.AppendLine($" {item2,-55} version={SafeLookup(server, item2)}"); } stringBuilder.AppendLine(); } stringBuilder.AppendLine("## All client mods"); foreach (KeyValuePair item3 in client.OrderBy, string>((KeyValuePair kv) => kv.Key, StringComparer.OrdinalIgnoreCase)) { stringBuilder.Append(" C ").Append(item3.Key).Append('=') .AppendLine(item3.Value); } stringBuilder.AppendLine(); stringBuilder.AppendLine("## All server mods"); foreach (KeyValuePair item4 in server.OrderBy, string>((KeyValuePair kv) => kv.Key, StringComparer.OrdinalIgnoreCase)) { stringBuilder.Append(" S ").Append(item4.Key).Append('=') .AppendLine(item4.Value); } return stringBuilder.ToString(); } private static string SafeLookup(IReadOnlyDictionary d, string key) { if (d == null) { return string.Empty; } string value; return d.TryGetValue(key, out value) ? (value ?? string.Empty) : string.Empty; } } } namespace FiresDiscordIntegration.ClientLogRelay.Webhook { public static class MinimalWebhookPoster { public sealed class Embed { public string Title; public string Description; public int Color = 3447003; public string Footer; public readonly List<(string name, string value, bool inline)> Fields = new List<(string, string, bool)>(); public Embed SetTitle(string title) { Title = title; return this; } public Embed SetDescription(string desc) { Description = desc; return this; } public Embed SetColor(int rgbInt) { Color = rgbInt; return this; } public Embed SetFooter(string footer) { Footer = footer; return this; } public Embed AddField(string name, string value, bool inline = false) { Fields.Add((name, value, inline)); return this; } } public sealed class Attachment { public string FileName; public byte[] Content; public string ContentType = "text/plain"; public Attachment(string fileName, byte[] bytes, string contentType = "text/plain") { FileName = fileName; Content = bytes ?? Array.Empty(); ContentType = contentType; } public Attachment(string fileName, string text, string contentType = "text/plain") : this(fileName, Encoding.UTF8.GetBytes(text ?? string.Empty), contentType) { } } public sealed class CoroutineHost : MonoBehaviour { } [CompilerGenerated] private sealed class d__15 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string botToken; public string channelId; public string messageId; public string emoji; private string 5__1; private string 5__2; private UnityWebRequest 5__3; private bool 5__4; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__15(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } 5__1 = null; 5__2 = null; 5__3 = null; <>1__state = -2; } private bool MoveNext() { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Expected O, but got Unknown //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Expected O, but got Unknown //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Invalid comparison between Unknown and I4 //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Invalid comparison between Unknown and I4 //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; 5__1 = Uri.EscapeDataString(emoji); 5__2 = "https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId + "/reactions/" + 5__1 + "/@me"; 5__3 = new UnityWebRequest(5__2, "PUT"); <>1__state = -3; 5__3.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); 5__3.SetRequestHeader("Authorization", "Bot " + botToken); 5__3.timeout = 15; <>2__current = 5__3.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; 5__4 = (int)5__3.result != 2 && (int)5__3.result != 3; if (!5__4) { Debug.LogWarning((object)$"[ClientLogRelay] AddReaction failed ({5__3.responseCode}): {5__3.error}"); } else { Debug.Log((object)("[ClientLogRelay] Bot pre-reacted with " + emoji + " on message " + messageId)); } <>m__Finally1(); 5__3 = null; <>2__current = (object)new WaitForSeconds(0.5f); <>1__state = 2; return true; case 2: <>1__state = -1; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (5__3 != null) { ((IDisposable)5__3).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__14 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string botToken; public string channelId; public string messageId; public string[] emojis; private int 5__1; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__14(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } <>1__state = -1; goto IL_007d; } <>1__state = -1; 5__1 = 0; goto IL_008d; IL_007d: 5__1++; goto IL_008d; IL_008d: if (5__1 < emojis.Length) { if (string.IsNullOrEmpty(emojis[5__1])) { goto IL_007d; } <>2__current = AddReactionCoroutine(botToken, channelId, messageId, emojis[5__1]); <>1__state = 1; return true; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__17 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string botToken; public string channelId; public int keepCount; private string 5__1; private UnityWebRequest 5__2; private bool 5__3; private JArray 5__4; private List 5__5; private int 5__6; private Exception 5__7; private IEnumerator <>s__8; private JToken 5__9; private JObject 5__10; private JObject 5__11; private JToken 5__12; private bool 5__13; private JArray 5__14; private bool 5__15; private IEnumerator <>s__16; private JToken 5__17; private JObject 5__18; private string 5__19; private string <desc>5__20; private string <messageId>5__21; private int <i>5__22; private string <messageId>5__23; private string <deleteUrl>5__24; private UnityWebRequest <delReq>5__25; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <CleanupOldClientLogsCoroutine>d__17(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if ((uint)(num - -4) <= 1u || (uint)(num - 2) <= 2u) { try { if (num == -4 || num == 3) { try { } finally { <>m__Finally2(); } } } finally { <>m__Finally1(); } } <url>5__1 = null; <req>5__2 = null; <messages>5__4 = null; <clientLogMessages>5__5 = null; <ex>5__7 = null; <>s__8 = null; <msg>5__9 = null; <msgObj>5__10 = null; <author>5__11 = null; <botProp>5__12 = null; <embeds>5__14 = null; <>s__16 = null; <embedItem>5__17 = null; <embedObj>5__18 = null; <title>5__19 = null; <desc>5__20 = null; <messageId>5__21 = null; <messageId>5__23 = null; <deleteUrl>5__24 = null; <delReq>5__25 = null; <>1__state = -2; } private bool MoveNext() { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected O, but got Unknown //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Invalid comparison between Unknown and I4 //IL_064b: Unknown result type (might be due to invalid IL or missing references) //IL_0651: Invalid comparison between Unknown and I4 //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Invalid comparison between Unknown and I4 //IL_0659: Unknown result type (might be due to invalid IL or missing references) //IL_065f: Invalid comparison between Unknown and I4 //IL_06ed: Unknown result type (might be due to invalid IL or missing references) //IL_06f7: Expected O, but got Unknown //IL_02aa: Unknown result type (might be due to invalid IL or missing references) //IL_02b1: Invalid comparison between Unknown and I4 bool result; try { switch (<>1__state) { default: result = false; goto end_IL_0000; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(2f); <>1__state = 1; result = true; goto end_IL_0000; case 1: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/channels/" + channelId + "/messages?limit=50"; <req>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <req>5__2.SetRequestHeader("Authorization", "Bot " + botToken); <req>5__2.timeout = 15; <>2__current = <req>5__2.SendWebRequest(); <>1__state = 2; result = true; goto end_IL_0000; case 2: <>1__state = -3; <ok>5__3 = (int)<req>5__2.result != 2 && (int)<req>5__2.result != 3; if (!<ok>5__3) { if (<req>5__2.responseCode != 403 && <req>5__2.responseCode != 404) { Debug.LogWarning((object)$"[ClientLogRelay] Failed to fetch messages for cleanup ({<req>5__2.responseCode})"); } result = false; break; } try { <messages>5__4 = JArray.Parse(<req>5__2.downloadHandler.text); } catch (Exception ex) { <ex>5__7 = ex; Debug.LogWarning((object)("[ClientLogRelay] Failed to parse messages for cleanup: " + <ex>5__7.Message)); result = false; break; } if (<messages>5__4 == null || ((JContainer)<messages>5__4).Count <= keepCount) { result = false; break; } <clientLogMessages>5__5 = new List<string>(); <>s__8 = <messages>5__4.GetEnumerator(); try { while (<>s__8.MoveNext()) { <msg>5__9 = <>s__8.Current; ref JObject reference = ref <msgObj>5__10; JToken obj = <msg>5__9; reference = (JObject)(object)((obj is JObject) ? obj : null); if (<msgObj>5__10 == null) { continue; } ref JObject reference2 = ref <author>5__11; JToken obj2 = <msgObj>5__10["author"]; reference2 = (JObject)(object)((obj2 is JObject) ? obj2 : null); if (<author>5__11 == null) { continue; } <botProp>5__12 = <author>5__11["bot"]; <isBot>5__13 = <botProp>5__12 != null && (int)<botProp>5__12.Type == 9 && (bool)<botProp>5__12; if (!<isBot>5__13) { continue; } ref JArray reference3 = ref <embeds>5__14; JToken obj3 = <msgObj>5__10["embeds"]; reference3 = (JArray)(object)((obj3 is JArray) ? obj3 : null); if (<embeds>5__14 == null || ((JContainer)<embeds>5__14).Count == 0) { continue; } <isClientLog>5__15 = false; <>s__16 = <embeds>5__14.GetEnumerator(); try { while (<>s__16.MoveNext()) { <embedItem>5__17 = <>s__16.Current; ref JObject reference4 = ref <embedObj>5__18; JToken obj4 = <embedItem>5__17; reference4 = (JObject)(object)((obj4 is JObject) ? obj4 : null); JObject obj5 = <embedObj>5__18; <title>5__19 = ((obj5 == null) ? null : ((object)obj5["title"])?.ToString()) ?? ""; JObject obj6 = <embedObj>5__18; <desc>5__20 = ((obj6 == null) ? null : ((object)obj6["description"])?.ToString()) ?? ""; if (<title>5__19.Contains("Client") || <title>5__19.Contains("Login") || <title>5__19.Contains("Snapshot") || <desc>5__20.Contains("Platform ID") || <desc>5__20.Contains("Captured")) { <isClientLog>5__15 = true; break; } <embedObj>5__18 = null; <title>5__19 = null; <desc>5__20 = null; <embedItem>5__17 = null; } } finally { if (<>s__16 != null) { <>s__16.Dispose(); } } <>s__16 = null; if (<isClientLog>5__15) { <messageId>5__21 = ((object)<msgObj>5__10["id"])?.ToString(); if (!string.IsNullOrEmpty(<messageId>5__21)) { <clientLogMessages>5__5.Add(<messageId>5__21); } <messageId>5__21 = null; } <msgObj>5__10 = null; <author>5__11 = null; <botProp>5__12 = null; <embeds>5__14 = null; <msg>5__9 = null; } } finally { if (<>s__8 != null) { <>s__8.Dispose(); } } <>s__8 = null; <toDelete>5__6 = <clientLogMessages>5__5.Count - keepCount; if (<toDelete>5__6 <= 0) { result = false; break; } Debug.Log((object)$"[ClientLogRelay] Found {<clientLogMessages>5__5.Count} client log messages, deleting oldest {<toDelete>5__6} to keep {keepCount}"); <i>5__22 = keepCount; goto IL_072e; case 3: <>1__state = -4; <ok>5__3 = (int)<delReq>5__25.result != 2 && (int)<delReq>5__25.result != 3; if (!<ok>5__3 && <delReq>5__25.responseCode != 404 && <delReq>5__25.responseCode != 403) { Debug.LogWarning((object)$"[ClientLogRelay] Failed to delete message {<messageId>5__23} ({<delReq>5__25.responseCode})"); } <>m__Finally2(); <delReq>5__25 = null; <>2__current = (object)new WaitForSeconds(0.5f); <>1__state = 4; result = true; goto end_IL_0000; case 4: { <>1__state = -3; <messageId>5__23 = null; <deleteUrl>5__24 = null; <i>5__22++; goto IL_072e; } IL_072e: if (<i>5__22 < <clientLogMessages>5__5.Count) { <messageId>5__23 = <clientLogMessages>5__5[<i>5__22]; <deleteUrl>5__24 = "https://discord.com/api/v10/channels/" + channelId + "/messages/" + <messageId>5__23; <delReq>5__25 = UnityWebRequest.Delete(<deleteUrl>5__24); <>1__state = -4; <delReq>5__25.SetRequestHeader("Authorization", "Bot " + botToken); <delReq>5__25.timeout = 10; <>2__current = <delReq>5__25.SendWebRequest(); <>1__state = 3; result = true; } else { Debug.Log((object)$"[ClientLogRelay] Client log cleanup complete - kept {keepCount} most recent messages"); <messages>5__4 = null; <clientLogMessages>5__5 = null; <>m__Finally1(); <req>5__2 = null; result = false; } goto end_IL_0000; } <>m__Finally1(); end_IL_0000:; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__2 != null) { ((IDisposable)<req>5__2).Dispose(); } } private void <>m__Finally2() { <>1__state = -3; if (<delReq>5__25 != null) { ((IDisposable)<delReq>5__25).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PostCoroutine>d__10 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string webhookUrl; public Embed embed; public List<Attachment> files; public string username; public string avatarUrl; public Action<string, string> onPosted; private string <boundary>5__1; private byte[] <body>5__2; private string <url>5__3; private UnityWebRequest <req>5__4; private bool <ok>5__5; private string <messageId>5__6; private string <channelId>5__7; private string <respBody>5__8; private string <trimmed>5__9; private string <respText>5__10; private Dictionary<string, object> <obj>5__11; private object <idObj>5__12; private object <chObj>5__13; private Exception <ex>5__14; private Exception <ex>5__15; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PostCoroutine>d__10(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <boundary>5__1 = null; <body>5__2 = null; <url>5__3 = null; <req>5__4 = null; <messageId>5__6 = null; <channelId>5__7 = null; <respBody>5__8 = null; <trimmed>5__9 = null; <respText>5__10 = null; <obj>5__11 = null; <idObj>5__12 = null; <chObj>5__13 = null; <ex>5__14 = null; <ex>5__15 = null; <>1__state = -2; } private bool MoveNext() { //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Expected O, but got Unknown //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Expected O, but got Unknown //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Expected O, but got Unknown //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Invalid comparison between Unknown and I4 //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Invalid comparison between Unknown and I4 bool result; try { switch (<>1__state) { default: result = false; break; case 0: <>1__state = -1; <boundary>5__1 = "------ClientLogRelayBoundary" + Guid.NewGuid().ToString("N"); <body>5__2 = BuildMultipart(<boundary>5__1, embed, files, username, avatarUrl); <url>5__3 = ((webhookUrl.IndexOf('?') >= 0) ? (webhookUrl + "&wait=true") : (webhookUrl + "?wait=true")); <req>5__4 = new UnityWebRequest(<url>5__3, "POST"); <>1__state = -3; <req>5__4.uploadHandler = (UploadHandler)new UploadHandlerRaw(<body>5__2); <req>5__4.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); <req>5__4.SetRequestHeader("Content-Type", "multipart/form-data; boundary=" + <boundary>5__1); <req>5__4.timeout = 30; <>2__current = <req>5__4.SendWebRequest(); <>1__state = 1; result = true; break; case 1: <>1__state = -3; <ok>5__5 = (int)<req>5__4.result != 2 && (int)<req>5__4.result != 3; if (!<ok>5__5) { <respBody>5__8 = null; try { DownloadHandler downloadHandler = <req>5__4.downloadHandler; <respBody>5__8 = ((downloadHandler != null) ? downloadHandler.text : null); } catch { } <trimmed>5__9 = (string.IsNullOrEmpty(<respBody>5__8) ? "<empty>" : ((<respBody>5__8.Length > 500) ? (<respBody>5__8.Substring(0, 500) + "…") : <respBody>5__8)); Debug.LogWarning((object)$"[ClientLogRelay] Webhook POST failed ({<req>5__4.responseCode}): {<req>5__4.error} | body: {<trimmed>5__9}"); onPosted?.Invoke(null, null); result = false; <>m__Finally1(); break; } <messageId>5__6 = null; <channelId>5__7 = null; try { DownloadHandler downloadHandler2 = <req>5__4.downloadHandler; <respText>5__10 = ((downloadHandler2 != null) ? downloadHandler2.text : null); if (!string.IsNullOrEmpty(<respText>5__10)) { <obj>5__11 = JsonConvert.DeserializeObject<Dictionary<string, object>>(<respText>5__10); if (<obj>5__11 != null) { if (<obj>5__11.TryGetValue("id", out <idObj>5__12) && <idObj>5__12 != null) { <messageId>5__6 = <idObj>5__12.ToString(); } if (<obj>5__11.TryGetValue("channel_id", out <chObj>5__13) && <chObj>5__13 != null) { <channelId>5__7 = <chObj>5__13.ToString(); } <idObj>5__12 = null; <chObj>5__13 = null; } <obj>5__11 = null; } <respText>5__10 = null; } catch (Exception ex) { <ex>5__14 = ex; Debug.LogWarning((object)("[ClientLogRelay] Webhook response parse failed: " + <ex>5__14.Message)); } try { onPosted?.Invoke(<messageId>5__6, <channelId>5__7); } catch (Exception ex) { <ex>5__15 = ex; Debug.LogWarning((object)("[ClientLogRelay] onPosted callback threw: " + <ex>5__15.Message)); } <messageId>5__6 = null; <channelId>5__7 = null; <>m__Finally1(); <req>5__4 = null; result = false; break; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__4 != null) { ((IDisposable)<req>5__4).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string BoundaryPrefix = "------ClientLogRelayBoundary"; private const float ReactionPutCooldownSeconds = 0.5f; private const float MessageDeleteCooldownSeconds = 0.5f; private const float PostMessageCleanupDelaySeconds = 2f; private static CoroutineHost _host; public static bool IsValidWebhookUrl(string url) { if (string.IsNullOrEmpty(url) || !url.StartsWith("https://", StringComparison.Ordinal)) { return false; } return url.IndexOf("discord.com/api/webhooks/", StringComparison.Ordinal) >= 0 || url.IndexOf("discordapp.com/api/webhooks/", StringComparison.Ordinal) >= 0; } public static void Post(string webhookUrl, Embed embed, List<Attachment> files, string username = null, string avatarUrl = null) { Post(webhookUrl, embed, files, username, avatarUrl, null); } public static void Post(string webhookUrl, Embed embed, List<Attachment> files, string username, string avatarUrl, Action<string, string> onPosted) { if (!IsValidWebhookUrl(webhookUrl)) { Debug.LogWarning((object)"[ClientLogRelay] Invalid webhook URL; skipping post"); onPosted?.Invoke(null, null); } else { EnsureHost(); ((MonoBehaviour)_host).StartCoroutine(PostCoroutine(webhookUrl, embed, files, username, avatarUrl, onPosted)); } } [IteratorStateMachine(typeof(<PostCoroutine>d__10))] private static IEnumerator PostCoroutine(string webhookUrl, Embed embed, List<Attachment> files, string username, string avatarUrl, Action<string, string> onPosted) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PostCoroutine>d__10(0) { webhookUrl = webhookUrl, embed = embed, files = files, username = username, avatarUrl = avatarUrl, onPosted = onPosted }; } private static byte[] BuildMultipart(string boundary, Embed embed, List<Attachment> files, string username, string avatarUrl) { Dictionary<string, object> dictionary = new Dictionary<string, object>(); if (!string.IsNullOrEmpty(username)) { dictionary["username"] = username; } if (!string.IsNullOrEmpty(avatarUrl)) { dictionary["avatar_url"] = avatarUrl; } if (embed != null) { Dictionary<string, object> dictionary2 = new Dictionary<string, object>(); if (!string.IsNullOrEmpty(embed.Title)) { dictionary2["title"] = embed.Title; } if (!string.IsNullOrEmpty(embed.Description)) { dictionary2["description"] = embed.Description; } dictionary2["color"] = embed.Color; if (!string.IsNullOrEmpty(embed.Footer)) { dictionary2["footer"] = new Dictionary<string, object> { ["text"] = embed.Footer }; } if (embed.Fields.Count > 0) { List<object> list = new List<object>(embed.Fields.Count); foreach (var field in embed.Fields) { list.Add(new Dictionary<string, object> { ["name"] = field.name ?? string.Empty, ["value"] = (string.IsNullOrEmpty(field.value) ? "\ufffd" : field.value), ["inline"] = field.inline }); } dictionary2["fields"] = list; } dictionary["embeds"] = new Dictionary<string, object>[1] { dictionary2 }; } string s = JsonConvert.SerializeObject((object)dictionary); using MemoryStream memoryStream = new MemoryStream(); byte[] bytes = Encoding.UTF8.GetBytes("--" + boundary + "\r\n"); byte[] bytes2 = Encoding.UTF8.GetBytes("\r\n"); byte[] bytes3 = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n"); memoryStream.Write(bytes, 0, bytes.Length); byte[] bytes4 = Encoding.UTF8.GetBytes("Content-Disposition: form-data; name=\"payload_json\"\r\nContent-Type: application/json\r\n\r\n"); memoryStream.Write(bytes4, 0, bytes4.Length); byte[] bytes5 = Encoding.UTF8.GetBytes(s); memoryStream.Write(bytes5, 0, bytes5.Length); memoryStream.Write(bytes2, 0, bytes2.Length); if (files != null) { for (int i = 0; i < files.Count; i++) { Attachment attachment = files[i]; memoryStream.Write(bytes, 0, bytes.Length); string s2 = $"Content-Disposition: form-data; name=\"files[{i}]\"; filename=\"{attachment.FileName}\"\r\n" + "Content-Type: " + attachment.ContentType + "\r\n\r\n"; byte[] bytes6 = Encoding.UTF8.GetBytes(s2); memoryStream.Write(bytes6, 0, bytes6.Length); memoryStream.Write(attachment.Content, 0, attachment.Content.Length); memoryStream.Write(bytes2, 0, bytes2.Length); } } memoryStream.Write(bytes3, 0, bytes3.Length); return memoryStream.ToArray(); } public static void AddReaction(string botToken, string channelId, string messageId, string emoji) { if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(channelId) && !string.IsNullOrEmpty(messageId) && !string.IsNullOrEmpty(emoji)) { EnsureHost(); ((MonoBehaviour)_host).StartCoroutine(AddReactionCoroutine(botToken, channelId, messageId, emoji)); } } public static void AddReactions(string botToken, string channelId, string messageId, params string[] emojis) { if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(channelId) && !string.IsNullOrEmpty(messageId) && emojis != null && emojis.Length != 0) { EnsureHost(); ((MonoBehaviour)_host).StartCoroutine(AddReactionsCoroutine(botToken, channelId, messageId, emojis)); } } [IteratorStateMachine(typeof(<AddReactionsCoroutine>d__14))] private static IEnumerator AddReactionsCoroutine(string botToken, string channelId, string messageId, string[] emojis) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <AddReactionsCoroutine>d__14(0) { botToken = botToken, channelId = channelId, messageId = messageId, emojis = emojis }; } [IteratorStateMachine(typeof(<AddReactionCoroutine>d__15))] private static IEnumerator AddReactionCoroutine(string botToken, string channelId, string messageId, string emoji) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <AddReactionCoroutine>d__15(0) { botToken = botToken, channelId = channelId, messageId = messageId, emoji = emoji }; } public static void StartCleanupOldClientLogs(string botToken, string channelId, int keepCount = 10) { if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(channelId)) { EnsureHost(); ((MonoBehaviour)_host).StartCoroutine(CleanupOldClientLogsCoroutine(botToken, channelId, keepCount)); } } [IteratorStateMachine(typeof(<CleanupOldClientLogsCoroutine>d__17))] private static IEnumerator CleanupOldClientLogsCoroutine(string botToken, string channelId, int keepCount) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <CleanupOldClientLogsCoroutine>d__17(0) { botToken = botToken, channelId = channelId, keepCount = keepCount }; } private static void EnsureHost() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown if (!((Object)(object)_host != (Object)null)) { GameObject val = new GameObject("ClientLogRelay_WebhookHost"); Object.DontDestroyOnLoad((Object)(object)val); _host = val.AddComponent<CoroutineHost>(); } } } } namespace FiresDiscordIntegration.ClientLogRelay.Transport { public static class ClientLogChunkedTransfer { private class TransferState { public long PeerId; public Dictionary<int, byte[]> Chunks = new Dictionary<int, byte[]>(); public int TotalChunks; public float LastChunkTime; public Dictionary<string, string> ModList; public string SteamId; } public class TransferResult { public byte[] LogBytes; public Dictionary<string, string> ModList; public string SteamId; } private static readonly Dictionary<long, TransferState> _activeTransfers = new Dictionary<long, TransferState>(); private const float TRANSFER_TIMEOUT = 60f; private const long UpdateTransferKeyOffset = 1000000000L; public static void StoreMetadata(long peerId, int totalChunks, Dictionary<string, string> modList, string steamId) { if (!_activeTransfers.TryGetValue(peerId, out var value)) { value = new TransferState { PeerId = peerId, TotalChunks = totalChunks, LastChunkTime = Time.realtimeSinceStartup, ModList = modList, SteamId = steamId }; _activeTransfers[peerId] = value; Debug.Log((object)$"[ClientLogChunkedTransfer] Stored metadata for peer {peerId}: {modList?.Count ?? 0} mods, expecting {totalChunks} chunks"); } else { value.ModList = modList; value.SteamId = steamId; value.TotalChunks = totalChunks; value.LastChunkTime = Time.realtimeSinceStartup; Debug.Log((object)$"[ClientLogChunkedTransfer] Updated metadata for peer {peerId}: {modList?.Count ?? 0} mods"); } } public static TransferResult ReceiveChunk(long peerId, int chunkIndex, int totalChunks, byte[] chunkData, Dictionary<string, string> modList = null, string steamId = null) { if (!_activeTransfers.TryGetValue(peerId, out var value)) { value = new TransferState { PeerId = peerId, TotalChunks = totalChunks, LastChunkTime = Time.realtimeSinceStartup }; _activeTransfers[peerId] = value; } if (modList != null || steamId != null) { if (modList != null) { value.ModList = modList; } if (steamId != null) { value.SteamId = steamId; } } value.Chunks[chunkIndex] = chunkData; value.LastChunkTime = Time.realtimeSinceStartup; Debug.Log((object)$"[ClientLogChunkedTransfer] Received chunk {chunkIndex + 1}/{totalChunks} from peer {peerId} ({chunkData.Length} bytes)"); if (value.Chunks.Count == totalChunks) { Debug.Log((object)$"[ClientLogChunkedTransfer] Transfer complete for peer {peerId} - reassembling {totalChunks} chunks"); int num = value.Chunks.Values.Sum((byte[] c) => c.Length); byte[] array = new byte[num]; int num2 = 0; for (int i = 0; i < totalChunks; i++) { if (!value.Chunks.TryGetValue(i, out var value2)) { Debug.LogWarning((object)$"[ClientLogChunkedTransfer] Missing chunk {i} for peer {peerId} - transfer corrupt!"); _activeTransfers.Remove(peerId); return null; } Array.Copy(value2, 0, array, num2, value2.Length); num2 += value2.Length; } TransferResult result = new TransferResult { LogBytes = array, ModList = value.ModList, SteamId = value.SteamId }; _activeTransfers.Remove(peerId); Debug.Log((object)$"[ClientLogChunkedTransfer] Reassembled {num} bytes from {totalChunks} chunks for peer {peerId}"); return result; } return null; } public static bool TryGetMetadata(long peerId, out Dictionary<string, string> modList, out string steamId) { if (_activeTransfers.TryGetValue(peerId, out var value)) { modList = value.ModList; steamId = value.SteamId; return true; } modList = null; steamId = null; return false; } public static void CleanupTimedOutTransfers() { float now = Time.realtimeSinceStartup; List<long> list = (from kvp in _activeTransfers where now - kvp.Value.LastChunkTime > 60f select kvp.Key).ToList(); foreach (long item in list) { Debug.LogWarning((object)$"[ClientLogChunkedTransfer] Transfer from peer {item} timed out after {60f}s - cleaning up"); _activeTransfers.Remove(item); } } public static void CancelTransfer(long peerId) { if (_activeTransfers.Remove(peerId)) { Debug.Log((object)$"[ClientLogChunkedTransfer] Cancelled transfer for peer {peerId}"); } } public static TransferResult ReceiveUpdateChunk(long peerId, int chunkIndex, int totalChunks, byte[] chunkData, string steamId = null) { long num = peerId + 1000000000; if (!_activeTransfers.TryGetValue(num, out var value)) { value = new TransferState { PeerId = num, TotalChunks = totalChunks, LastChunkTime = Time.realtimeSinceStartup, SteamId = steamId }; _activeTransfers[num] = value; } value.LastChunkTime = Time.realtimeSinceStartup; value.Chunks[chunkIndex] = chunkData; Debug.Log((object)$"[ClientLogChunkedTransfer] Update chunk {chunkIndex + 1}/{totalChunks} received from peer {peerId} ({chunkData.Length} bytes)"); if (value.Chunks.Count == totalChunks) { int num2 = value.Chunks.Values.Sum((byte[] c) => c.Length); byte[] array = new byte[num2]; int num3 = 0; for (int i = 0; i < totalChunks; i++) { if (!value.Chunks.TryGetValue(i, out var value2)) { Debug.LogWarning((object)$"[ClientLogChunkedTransfer] Missing update chunk {i} for peer {peerId}"); _activeTransfers.Remove(num); return null; } Array.Copy(value2, 0, array, num3, value2.Length); num3 += value2.Length; } TransferResult result = new TransferResult { LogBytes = array, ModList = null, SteamId = value.SteamId }; _activeTransfers.Remove(num); Debug.Log((object)$"[ClientLogChunkedTransfer] Reassembled update: {num2} bytes from {totalChunks} chunks for peer {peerId}"); return result; } return null; } } public static class ClientLogCollector { public static byte[] ReadLocalBepInExLog() { try { string path = Path.Combine(Paths.BepInExRootPath, "LogOutput.log"); if (!File.Exists(path)) { return null; } using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); long length = fileStream.Length; if (length <= 0) { return null; } byte[] array = new byte[length]; int i; int num; for (i = 0; i < array.Length; i += num) { num = fileStream.Read(array, i, array.Length - i); if (num <= 0) { break; } } if (i == array.Length) { return array; } byte[] array2 = new byte[i]; Array.Copy(array, 0, array2, 0, i); return array2; } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] ReadLocalBepInExLog failed: " + ex.Message)); return null; } } public static Dictionary<string, string> BuildLocalModList() { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); try { Dictionary<string, PluginInfo> pluginInfos = Chainloader.PluginInfos; if (pluginInfos == null) { return dictionary; } foreach (KeyValuePair<string, PluginInfo> item in pluginInfos) { PluginInfo value = item.Value; BepInPlugin val = ((value != null) ? value.Metadata : null); if (val != null && !string.IsNullOrEmpty(val.GUID)) { dictionary[val.GUID] = ((val.Version != null) ? val.Version.ToString() : "?"); } } } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] BuildLocalModList failed: " + ex.Message)); } return dictionary; } public static bool TryCollect(out byte[] logBytes, out Dictionary<string, string> modList) { logBytes = ReadLocalBepInExLog(); modList = BuildLocalModList(); return modList != null; } public static void ReadLocalBepInExLogAsync(Action<byte[]> onComplete) { if (onComplete == null) { return; } ThreadPool.QueueUserWorkItem(delegate { byte[] array = null; try { array = ReadLocalBepInExLog(); } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] ReadLocalBepInExLogAsync worker failed: " + ex.Message)); } byte[] obj = array; try { onComplete(obj); } catch (Exception ex2) { Debug.LogWarning((object)("[ClientLogRelay] ReadLocalBepInExLogAsync callback threw: " + ex2.Message)); } }); } } } namespace FiresDiscordIntegration.ClientLogRelay.Interactions { public static class DisconnectLogRegistry { public sealed class Entry { public string PlatformId; public string PlayerName; public DateTime RegisteredUtc; public string RequestedByName; } private static readonly object _lock = new object(); private static readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry>(StringComparer.OrdinalIgnoreCase); public static int Count { get { lock (_lock) { return _entries.Count; } } } public static void Register(string platformId, string playerName, string requestedByName) { if (string.IsNullOrEmpty(platformId)) { return; } lock (_lock) { _entries[platformId] = new Entry { PlatformId = platformId, PlayerName = (playerName ?? "unknown"), RegisteredUtc = DateTime.UtcNow, RequestedByName = (requestedByName ?? "unknown") }; Debug.Log((object)("[ClientLogRelay] DisconnectLogRegistry: registered '" + playerName + "' (" + platformId + ") for disconnect capture, requested by " + requestedByName)); } } public static Entry TakeIfRegistered(string platformId) { if (string.IsNullOrEmpty(platformId)) { return null; } lock (_lock) { if (_entries.TryGetValue(platformId, out var value)) { _entries.Remove(platformId); return value; } return null; } } public static bool IsRegistered(string platformId) { if (string.IsNullOrEmpty(platformId)) { return false; } lock (_lock) { return _entries.ContainsKey(platformId); } } } public interface ILogRequestHandler { bool IsAuthorized(string discordUserId); void HandleRequest(LogRequestContext ctx, string discordUserId, string emoji); } public sealed class LogRequestContext { public string MessageId { get; } public string ChannelId { get; } public string PlatformId { get; } public string PlayerName { get; } public DateTime CapturedUtc { get; } public string ConsumerId { get; } public LogRequestContext(string messageId, string platformId, string playerName, DateTime capturedUtc, string consumerId, string channelId = null) { MessageId = messageId ?? string.Empty; ChannelId = channelId ?? string.Empty; PlatformId = platformId ?? string.Empty; PlayerName = playerName ?? "unknown"; CapturedUtc = capturedUtc; ConsumerId = consumerId ?? string.Empty; } } public static class LogRequestRegistry { private struct Entry { public LogRequestContext Context; public DateTime ExpiresUtc; } public static readonly TimeSpan DefaultTtl = TimeSpan.FromHours(24.0); private static readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry>(StringComparer.Ordinal); private static readonly object _lock = new object(); private static DateTime _lastJanitorUtc = DateTime.UtcNow; public static int Count { get { lock (_lock) { return _entries.Count; } } } public static void Register(LogRequestContext ctx, TimeSpan? ttl = null) { if (ctx == null || string.IsNullOrEmpty(ctx.MessageId)) { return; } DateTime expiresUtc = DateTime.UtcNow + (ttl ?? DefaultTtl); lock (_lock) { _entries[ctx.MessageId] = new Entry { Context = ctx, ExpiresUtc = expiresUtc }; RunJanitorIfDue(); } } public static bool TryGet(string messageId, out LogRequestContext ctx) { ctx = null; if (string.IsNullOrEmpty(messageId)) { return false; } lock (_lock) { if (!_entries.TryGetValue(messageId, out var value)) { return false; } if (value.ExpiresUtc < DateTime.UtcNow) { _entries.Remove(messageId); return false; } ctx = value.Context; return true; } } public static void Forget(string messageId) { if (string.IsNullOrEmpty(messageId)) { return; } lock (_lock) { _entries.Remove(messageId); } } public static void Clear() { lock (_lock) { _entries.Clear(); } } public static LogRequestContext[] Snapshot() { lock (_lock) { DateTime utcNow = DateTime.UtcNow; List<LogRequestContext> list = new List<LogRequestContext>(_entries.Count); foreach (KeyValuePair<string, Entry> entry in _entries) { if (entry.Value.ExpiresUtc >= utcNow) { list.Add(entry.Value.Context); } } return list.ToArray(); } } private static void RunJanitorIfDue() { DateTime utcNow = DateTime.UtcNow; if (utcNow - _lastJanitorUtc < TimeSpan.FromMinutes(1.0)) { return; } _lastJanitorUtc = utcNow; List<string> list = null; foreach (KeyValuePair<string, Entry> entry in _entries) { if (entry.Value.ExpiresUtc < utcNow) { if (list == null) { list = new List<string>(); } list.Add(entry.Key); } } if (list != null) { for (int i = 0; i < list.Count; i++) { _entries.Remove(list[i]); } if (list.Count > 0) { Debug.Log((object)$"[ClientLogRelay] LogRequestRegistry swept {list.Count} expired entr(ies)"); } } } } public sealed class ReactionPoller : MonoBehaviour { [CompilerGenerated] private sealed class <>c__DisplayClass23_0 { public bool wasRateLimited; internal void <PollLoop>b__0(bool limited) { wasRateLimited = limited; } } [CompilerGenerated] private sealed class <Fulfill>d__26 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public LogRequestContext ctx; public string emoji; public string reqByName; public string reqById; public ReactionPoller <>4__this; private string <logsRoot>5__1; private string <folder>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <Fulfill>d__26(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <logsRoot>5__1 = null; <folder>5__2 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <logsRoot>5__1 = <>4__this._clientLogsRootResolver?.Invoke(); if (string.IsNullOrEmpty(<logsRoot>5__1)) { return false; } <folder>5__2 = BuildPlayerFolder(<logsRoot>5__1, ctx); if (emoji == "\ud83d\udce9") { <>2__current = <>4__this.FulfillFullLog(ctx, <folder>5__2, reqByName); <>1__state = 1; return true; } if (emoji == "⛔") { <>2__current = <>4__this.FulfillErrorsWarnings(ctx, <folder>5__2, reqByName); <>1__state = 2; return true; } if (emoji == "\ud83e\udde9") { <>2__current = <>4__this.FulfillMods(ctx, <folder>5__2, reqByName); <>1__state = 3; return true; } if (emoji == "♻\ufe0f") { <>4__this.FulfillDisconnect(ctx, reqByName); } break; case 1: <>1__state = -1; break; case 2: <>1__state = -1; break; case 3: <>1__state = -1; break; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <FulfillErrorsWarnings>d__28 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public LogRequestContext ctx; public string folder; public string reqByName; public ReactionPoller <>4__this; private string <path>5__1; private byte[] <bytes>5__2; private string <stamp>5__3; private string <safeId>5__4; private MinimalWebhookPoster.Embed <embed>5__5; private List<MinimalWebhookPoster.Attachment> <files>5__6; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <FulfillErrorsWarnings>d__28(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <path>5__1 = null; <bytes>5__2 = null; <stamp>5__3 = null; <safeId>5__4 = null; <embed>5__5 = null; <files>5__6 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <path>5__1 = Path.Combine(folder, "errors_warnings.txt"); <bytes>5__2 = SafeReadFile(<path>5__1); if (<bytes>5__2 == null || <bytes>5__2.Length == 0) { Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller: no errors_warnings.txt for " + ctx.PlatformId)); return false; } <stamp>5__3 = ctx.CapturedUtc.ToString("yyyyMMdd_HHmmss"); <safeId>5__4 = SafePlatformId(ctx); <embed>5__5 = new MinimalWebhookPoster.Embed().SetTitle("⛔ Errors + Warnings — Requested").SetColor(15548997).AddField("\ud83d\udc64 Player", ctx.PlayerName, inline: true) .AddField("\ud83d\udd94 Steam ID", ctx.PlatformId ?? "?", inline: true) .AddField("\ud83d\udcc4 Size", FormatBytes(<bytes>5__2.Length), inline: true) .SetFooter("Requested by " + reqByName); <files>5__6 = new List<MinimalWebhookPoster.Attachment> { new MinimalWebhookPoster.Attachment("errors_warnings_" + <safeId>5__4 + "_" + <stamp>5__3 + ".txt", <bytes>5__2) }; <>2__current = <>4__this.PostAndWait(<embed>5__5, <files>5__6); <>1__state = 1; return true; case 1: <>1__state = -1; Debug.Log((object)("[ClientLogRelay] ReactionPoller: posted errors/warnings for " + ctx.PlatformId + ", requested by " + reqByName)); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <FulfillFullLog>d__27 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public LogRequestContext ctx; public string folder; public string reqByName; public ReactionPoller <>4__this; private string <logPath>5__1; private byte[] <logBytes>5__2; private string <stamp>5__3; private string <safeId>5__4; private List<byte[]> <chunks>5__5; private int <i>5__6; private string <suffix>5__7; private string <fileName>5__8; private string <title>5__9; private MinimalWebhookPoster.Embed <embed>5__10; private List<MinimalWebhookPoster.Attachment> <files>5__11; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <FulfillFullLog>d__27(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <logPath>5__1 = null; <logBytes>5__2 = null; <stamp>5__3 = null; <safeId>5__4 = null; <chunks>5__5 = null; <suffix>5__7 = null; <fileName>5__8 = null; <title>5__9 = null; <embed>5__10 = null; <files>5__11 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <logPath>5__1 = Path.Combine(folder, "LogOutput.log"); <logBytes>5__2 = SafeReadFile(<logPath>5__1); if (<logBytes>5__2 == null || <logBytes>5__2.Length == 0) { Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller: no cached log for " + ctx.PlatformId + " at '" + <logPath>5__1 + "'")); return false; } <stamp>5__3 = ctx.CapturedUtc.ToString("yyyyMMdd_HHmmss"); <safeId>5__4 = SafePlatformId(ctx); <chunks>5__5 = ChunkBytes(<logBytes>5__2, 8323072); <i>5__6 = 0; break; case 1: <>1__state = -1; <suffix>5__7 = null; <fileName>5__8 = null; <title>5__9 = null; <embed>5__10 = null; <files>5__11 = null; <i>5__6++; break; } if (<i>5__6 < <chunks>5__5.Count) { <suffix>5__7 = ((<chunks>5__5.Count == 1) ? "" : $"_part{<i>5__6 + 1}of{<chunks>5__5.Count}"); <fileName>5__8 = "client_log_" + <safeId>5__4 + "_" + <stamp>5__3 + <suffix>5__7 + ".log"; <title>5__9 = ((<chunks>5__5.Count == 1) ? "\ud83d\udce9 Full Log — Requested" : $"\ud83d\udce9 Full Log — Part {<i>5__6 + 1}/{<chunks>5__5.Count}"); <embed>5__10 = new MinimalWebhookPoster.Embed().SetTitle(<title>5__9).SetColor(3447003).AddField("\ud83d\udc64 Player", ctx.PlayerName, inline: true) .AddField("\ud83d\udd94 Steam ID", ctx.PlatformId ?? "?", inline: true) .AddField("\ud83d\udcc4 Size", FormatBytes(<chunks>5__5[<i>5__6].Length), inline: true) .SetFooter("Requested by " + reqByName); <files>5__11 = new List<MinimalWebhookPoster.Attachment> { new MinimalWebhookPoster.Attachment(<fileName>5__8, <chunks>5__5[<i>5__6]) }; <>2__current = <>4__this.PostAndWait(<embed>5__10, <files>5__11); <>1__state = 1; return true; } Debug.Log((object)("[ClientLogRelay] ReactionPoller: posted full log for " + ctx.PlatformId + " " + $"({<logBytes>5__2.Length} bytes, {<chunks>5__5.Count} part(s)), requested by {reqByName}")); return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <FulfillMods>d__29 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public LogRequestContext ctx; public string folder; public string reqByName; public ReactionPoller <>4__this; private string <stamp>5__1; private string <safeId>5__2; private byte[] <modListBytes>5__3; private byte[] <modDiffBytes>5__4; private bool <hasModList>5__5; private bool <hasDiff>5__6; private MinimalWebhookPoster.Embed <embed>5__7; private List<MinimalWebhookPoster.Attachment> <files>5__8; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <FulfillMods>d__29(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <stamp>5__1 = null; <safeId>5__2 = null; <modListBytes>5__3 = null; <modDiffBytes>5__4 = null; <embed>5__7 = null; <files>5__8 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <stamp>5__1 = ctx.CapturedUtc.ToString("yyyyMMdd_HHmmss"); <safeId>5__2 = SafePlatformId(ctx); <modListBytes>5__3 = SafeReadFile(Path.Combine(folder, "modlist.txt")); <modDiffBytes>5__4 = SafeReadFile(Path.Combine(folder, "mod_diff.txt")); <hasModList>5__5 = <modListBytes>5__3 != null && <modListBytes>5__3.Length != 0; <hasDiff>5__6 = <modDiffBytes>5__4 != null && <modDiffBytes>5__4.Length != 0; if (!<hasModList>5__5 && !<hasDiff>5__6) { Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller: no mod artifacts for " + ctx.PlatformId)); return false; } <embed>5__7 = new MinimalWebhookPoster.Embed().SetTitle("\ud83e\udde9 Mod Lists — Requested").SetColor(5763719).AddField("\ud83d\udc64 Player", ctx.PlayerName, inline: true) .AddField("\ud83d\udd94 Steam ID", ctx.PlatformId ?? "?", inline: true) .SetFooter("Requested by " + reqByName); <files>5__8 = new List<MinimalWebhookPoster.Attachment>(); if (<hasModList>5__5) { <files>5__8.Add(new MinimalWebhookPoster.Attachment("modlist_" + <safeId>5__2 + "_" + <stamp>5__1 + ".txt", <modListBytes>5__3)); } if (<hasDiff>5__6) { <files>5__8.Add(new MinimalWebhookPoster.Attachment("mod_diff_" + <safeId>5__2 + "_" + <stamp>5__1 + ".txt", <modDiffBytes>5__4)); } <>2__current = <>4__this.PostAndWait(<embed>5__7, <files>5__8); <>1__state = 1; return true; case 1: <>1__state = -1; Debug.Log((object)("[ClientLogRelay] ReactionPoller: posted mod artifacts for " + ctx.PlatformId + ", requested by " + reqByName)); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PollLoop>d__23 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public ReactionPoller <>4__this; private string <botToken>5__1; private LogRequestContext[] <contexts>5__2; private int <polledCount>5__3; private float <timeSinceLimit>5__4; private float <waitTime>5__5; private LogRequestContext[] <>s__6; private int <>s__7; private LogRequestContext <ctx>5__8; private bool <hitRateLimit>5__9; private int <i>5__10; private <>c__DisplayClass23_0 <>8__11; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PollLoop>d__23(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <botToken>5__1 = null; <contexts>5__2 = null; <>s__6 = null; <ctx>5__8 = null; <>8__11 = null; <>1__state = -2; } private bool MoveNext() { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected O, but got Unknown //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Expected O, but got Unknown //IL_0308: Unknown result type (might be due to invalid IL or missing references) //IL_0312: Expected O, but got Unknown //IL_037a: Unknown result type (might be due to invalid IL or missing references) //IL_0384: Expected O, but got Unknown //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(30f); <>1__state = 1; return true; case 1: <>1__state = -1; goto IL_03ff; case 2: <>1__state = -1; goto IL_010b; case 3: <>1__state = -1; <botToken>5__1 = <>4__this._botTokenResolver?.Invoke(); if (string.IsNullOrEmpty(<botToken>5__1)) { goto IL_03ff; } if (!<>4__this._botUserIdResolved) { <>2__current = <>4__this.ResolveBotUserId(<botToken>5__1); <>1__state = 4; return true; } goto IL_01a5; case 4: <>1__state = -1; <>4__this._botUserIdResolved = true; goto IL_01a5; case 5: <>1__state = -1; if (<>8__11.wasRateLimited) { <hitRateLimit>5__9 = true; Debug.Log((object)$"[ClientLogRelay] ReactionPoller: Hit rate limit after {<polledCount>5__3} messages, pausing for this cycle"); goto IL_0354; } if (<i>5__10 < _emojiRaw.Length - 1) { <>2__current = (object)new WaitForSeconds(0.5f); <>1__state = 6; return true; } goto IL_0322; case 6: <>1__state = -1; goto IL_0322; case 7: { <>1__state = -1; <ctx>5__8 = null; goto IL_039c; } IL_039c: <>s__7++; goto IL_03aa; IL_0322: <>8__11 = null; <i>5__10++; goto IL_033c; IL_0354: if (<hitRateLimit>5__9) { goto IL_03bd; } <polledCount>5__3++; <>2__current = (object)new WaitForSeconds(2f); <>1__state = 7; return true; IL_03bd: <>s__6 = null; if (<polledCount>5__3 > 0) { Debug.Log((object)$"[ClientLogRelay] ReactionPoller: Polled {<polledCount>5__3} message(s) this cycle"); } <botToken>5__1 = null; <contexts>5__2 = null; goto IL_03ff; IL_033c: if (<i>5__10 < _emojiRaw.Length) { <>8__11 = new <>c__DisplayClass23_0(); <>8__11.wasRateLimited = false; <>2__current = <>4__this.PollReaction(<botToken>5__1, <ctx>5__8, _emojiRaw[<i>5__10], _emojiEncoded[<i>5__10], delegate(bool limited) { <>8__11.wasRateLimited = limited; }); <>1__state = 5; return true; } goto IL_0354; IL_010b: <>2__current = (object)new WaitForSeconds(30f); <>1__state = 3; return true; IL_01a5: <contexts>5__2 = LogRequestRegistry.Snapshot(); if (<contexts>5__2 == null || <contexts>5__2.Length == 0) { goto IL_03ff; } <polledCount>5__3 = 0; <>s__6 = <contexts>5__2; <>s__7 = 0; goto IL_03aa; IL_03ff: if (_consecutiveRateLimits > 0) { <timeSinceLimit>5__4 = Time.realtimeSinceStartup - _lastRateLimitTime; if (<timeSinceLimit>5__4 < 120f) { <waitTime>5__5 = 120f - <timeSinceLimit>5__4; Debug.Log((object)$"[ClientLogRelay] ReactionPoller: Rate limited, backing off for {<waitTime>5__5:F0}s (attempt {_consecutiveRateLimits})"); <>2__current = (object)new WaitForSeconds(<waitTime>5__5); <>1__state = 2; return true; } } goto IL_010b; IL_03aa: if (<>s__7 < <>s__6.Length) { <ctx>5__8 = <>s__6[<>s__7]; if (string.IsNullOrEmpty(<ctx>5__8.ChannelId) || string.IsNullOrEmpty(<ctx>5__8.MessageId)) { goto IL_039c; } <hitRateLimit>5__9 = false; <i>5__10 = 0; goto IL_033c; } goto IL_03bd; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PollReaction>d__25 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string botToken; public LogRequestContext ctx; public string emojiRaw; public string emojiEncoded; public Action<bool> rateLimitCallback; public ReactionPoller <>4__this; private string <url>5__1; private UnityWebRequest <req>5__2; private bool <ok>5__3; private JArray <users>5__4; private Exception <ex>5__5; private IEnumerator<JToken> <>s__6; private JToken <userToken>5__7; private string <userId>5__8; private string <key>5__9; private string <userName>5__10; private string <emojiLabel>5__11; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PollReaction>d__25(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if ((uint)(num - -4) <= 1u || (uint)(num - 1) <= 1u) { try { if (num == -4 || num == 2) { try { } finally { <>m__Finally2(); } } } finally { <>m__Finally1(); } } <url>5__1 = null; <req>5__2 = null; <users>5__4 = null; <ex>5__5 = null; <>s__6 = null; <userToken>5__7 = null; <userId>5__8 = null; <key>5__9 = null; <userName>5__10 = null; <emojiLabel>5__11 = null; <>1__state = -2; } private bool MoveNext() { //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Invalid comparison between Unknown and I4 //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Invalid comparison between Unknown and I4 bool result; try { switch (<>1__state) { default: result = false; goto end_IL_0000; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/channels/" + ctx.ChannelId + "/messages/" + ctx.MessageId + "/reactions/" + emojiEncoded; <req>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <req>5__2.SetRequestHeader("Authorization", "Bot " + botToken); <req>5__2.timeout = 15; <>2__current = <req>5__2.SendWebRequest(); <>1__state = 1; result = true; goto end_IL_0000; case 1: <>1__state = -3; <ok>5__3 = (int)<req>5__2.result != 2 && (int)<req>5__2.result != 3; if (!<ok>5__3) { if (<req>5__2.responseCode == 429) { _consecutiveRateLimits++; _lastRateLimitTime = Time.realtimeSinceStartup; rateLimitCallback?.Invoke(obj: true); Debug.LogWarning((object)$"[ClientLogRelay] ReactionPoller rate limited (429) - backing off (consecutive: {_consecutiveRateLimits})"); } else if (<req>5__2.responseCode != 404) { Debug.LogWarning((object)$"[ClientLogRelay] ReactionPoller GET reactions failed ({<req>5__2.responseCode}): {<req>5__2.error}"); } result = false; break; } if (_consecutiveRateLimits > 0) { Debug.Log((object)$"[ClientLogRelay] ReactionPoller: Rate limit cleared after {_consecutiveRateLimits} consecutive 429s"); _consecutiveRateLimits = 0; } rateLimitCallback?.Invoke(obj: false); try { <users>5__4 = JArray.Parse(<req>5__2.downloadHandler.text); } catch (Exception ex) { <ex>5__5 = ex; Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller parse failed: " + <ex>5__5.Message)); result = false; break; } <>s__6 = <users>5__4.GetEnumerator(); <>1__state = -4; goto IL_0488; case 2: { <>1__state = -4; <userId>5__8 = null; <key>5__9 = null; <userName>5__10 = null; <emojiLabel>5__11 = null; <userToken>5__7 = null; goto IL_0488; } IL_0488: while (true) { if (<>s__6.MoveNext()) { <userToken>5__7 = <>s__6.Current; <userId>5__8 = ((object)<userToken>5__7[(object)"id"])?.ToString(); if (!string.IsNullOrEmpty(<userId>5__8) && (string.IsNullOrEmpty(<>4__this._botUserId) || !(<userId>5__8 == <>4__this._botUserId))) { <key>5__9 = ctx.MessageId + ":" + <userId>5__8 + ":" + emojiRaw; if (!<>4__this._fulfilled.Contains(<key>5__9)) { <>4__this._fulfilled.Add(<key>5__9); <userName>5__10 = ((object)<userToken>5__7[(object)"username"])?.ToString() ?? <userId>5__8; <emojiLabel>5__11 = EmojiLabel(emojiRaw); Debug.Log((object)("[ClientLogRelay] ReactionPoller: '" + <userName>5__10 + "' reacted " + <emojiLabel>5__11 + " on " + ctx.PlayerName + " (" + ctx.PlatformId + ")")); <>2__current = <>4__this.Fulfill(ctx, emojiRaw, <userName>5__10, <userId>5__8); <>1__state = 2; result = true; break; } } continue; } <>m__Finally2(); <>s__6 = null; <users>5__4 = null; <>m__Finally1(); <req>5__2 = null; result = false; break; } goto end_IL_0000; } <>m__Finally1(); end_IL_0000:; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__2 != null) { ((IDisposable)<req>5__2).Dispose(); } } private void <>m__Finally2() { <>1__state = -3; if (<>s__6 != null) { <>s__6.Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PostAndWait>d__31 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public MinimalWebhookPoster.Embed embed; public List<MinimalWebhookPoster.Attachment> files; public ReactionPoller <>4__this; private string <webhookUrl>5__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PostAndWait>d__31(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <webhookUrl>5__1 = null; <>1__state = -2; } private bool MoveNext() { //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <webhookUrl>5__1 = <>4__this._webhookUrlResolver?.Invoke(); if (string.IsNullOrEmpty(<webhookUrl>5__1) || !MinimalWebhookPoster.IsValidWebhookUrl(<webhookUrl>5__1)) { return false; } MinimalWebhookPoster.Post(<webhookUrl>5__1, embed, files, <>4__this._webhookNameResolver?.Invoke()); <>2__current = (object)new WaitForSeconds(1.5f); <>1__state = 1; return true; case 1: <>1__state = -1; return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <ResolveBotUserId>d__24 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string botToken; public ReactionPoller <>4__this; private UnityWebRequest <req>5__1; private bool <ok>5__2; private Dictionary<string, object> <obj>5__3; private object <idObj>5__4; private Exception <ex>5__5; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ResolveBotUserId>d__24(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <req>5__1 = null; <obj>5__3 = null; <idObj>5__4 = null; <ex>5__5 = null; <>1__state = -2; } private bool MoveNext() { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Invalid comparison between Unknown and I4 //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Invalid comparison between Unknown and I4 try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <req>5__1 = UnityWebRequest.Get("https://discord.com/api/v10/users/@me"); <>1__state = -3; <req>5__1.SetRequestHeader("Authorization", "Bot " + botToken); <req>5__1.timeout = 15; <>2__current = <req>5__1.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; <ok>5__2 = (int)<req>5__1.result != 2 && (int)<req>5__1.result != 3; if (<ok>5__2 && <req>5__1.downloadHandler != null) { try { <obj>5__3 = JsonConvert.DeserializeObject<Dictionary<string, object>>(<req>5__1.downloadHandler.text); if (<obj>5__3 != null && <obj>5__3.TryGetValue("id", out <idObj>5__4) && <idObj>5__4 != null) { <>4__this._botUserId = <idObj>5__4.ToString(); Debug.Log((object)("[ClientLogRelay] ReactionPoller resolved bot user id: " + <>4__this._botUserId)); } <obj>5__3 = null; <idObj>5__4 = null; } catch (Exception ex) { <ex>5__5 = ex; Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller /users/@me parse failed: " + <ex>5__5.Message)); } } else { Debug.LogWarning((object)$"[ClientLogRelay] ReactionPoller /users/@me failed ({<req>5__1.responseCode}): {<req>5__1.error}"); } <>m__Finally1(); <req>5__1 = null; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__1 != null) { ((IDisposable)<req>5__1).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const float POLL_INTERVAL_SECONDS = 30f; private const float RATE_LIMIT_BACKOFF_SECONDS = 120f; private const float InitialPollStartupDelaySeconds = 30f; private const float InterEmojiPollGapSeconds = 0.5f; private const float InterMessagePollGapSeconds = 2f; private const float PostPublishCooldownSeconds = 1.5f; private static int _consecutiveRateLimits = 0; private static float _lastRateLimitTime = 0f; private const int DiscordFreeWebhookFileLimitBytes = 8388608; private const int MultipartEnvelopeMarginBytes = 65536; private const int MAX_ATTACHMENT_BYTES = 8323072; private Func<string> _botTokenResolver; private Func<string> _webhookUrlResolver; private Func<string> _clientLogsRootResolver; private Func<string> _webhookNameResolver; private readonly HashSet<string> _fulfilled = new HashSet<string>(StringComparer.Ordinal); private string _botUserId; private bool _botUserIdResolved; private static readonly string[] _emojiRaw = new string[4] { "\ud83d\udce9", "⛔", "\ud83e\udde9", "♻\ufe0f" }; private static readonly string[] _emojiEncoded = _emojiRaw.Select((string e) => Uri.EscapeDataString(e)).ToArray(); private static string EmojiLabel(string emoji) { return emoji switch { "\ud83d\udce9" => "LOG", "⛔" => "ERRORS", "\ud83e\udde9" => "MODS", "♻\ufe0f" => "DISCONNECT", "\ud83d\udd04" => "RESTART", _ => emoji, }; } public static ReactionPoller Create(Func<string> botTokenResolver, Func<string> webhookUrlResolver, Func<string> clientLogsRootResolver, Func<string> webhookNameResolver = null) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown GameObject val = new GameObject("ClientLogRelay_ReactionPoller"); Object.DontDestroyOnLoad((Object)(object)val); ReactionPoller reactionPoller = val.AddComponent<ReactionPoller>(); reactionPoller._botTokenResolver = botTokenResolver; reactionPoller._webhookUrlResolver = webhookUrlResolver; reactionPoller._clientLogsRootResolver = clientLogsRootResolver; reactionPoller._webhookNameResolver = webhookNameResolver; return reactionPoller; } private void Start() { ((MonoBehaviour)this).StartCoroutine(PollLoop()); } [IteratorStateMachine(typeof(<PollLoop>d__23))] private IEnumerator PollLoop() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PollLoop>d__23(0) { <>4__this = this }; } [IteratorStateMachine(typeof(<ResolveBotUserId>d__24))] private IEnumerator ResolveBotUserId(string botToken) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ResolveBotUserId>d__24(0) { <>4__this = this, botToken = botToken }; } [IteratorStateMachine(typeof(<PollReaction>d__25))] private IEnumerator PollReaction(string botToken, LogRequestContext ctx, string emojiRaw, string emojiEncoded, Action<bool> rateLimitCallback = null) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PollReaction>d__25(0) { <>4__this = this, botToken = botToken, ctx = ctx, emojiRaw = emojiRaw, emojiEncoded = emojiEncoded, rateLimitCallback = rateLimitCallback }; } [IteratorStateMachine(typeof(<Fulfill>d__26))] private IEnumerator Fulfill(LogRequestContext ctx, string emoji, string reqByName, string reqById) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <Fulfill>d__26(0) { <>4__this = this, ctx = ctx, emoji = emoji, reqByName = reqByName, reqById = reqById }; } [IteratorStateMachine(typeof(<FulfillFullLog>d__27))] private IEnumerator FulfillFullLog(LogRequestContext ctx, string folder, string reqByName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <FulfillFullLog>d__27(0) { <>4__this = this, ctx = ctx, folder = folder, reqByName = reqByName }; } [IteratorStateMachine(typeof(<FulfillErrorsWarnings>d__28))] private IEnumerator FulfillErrorsWarnings(LogRequestContext ctx, string folder, string reqByName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <FulfillErrorsWarnings>d__28(0) { <>4__this = this, ctx = ctx, folder = folder, reqByName = reqByName }; } [IteratorStateMachine(typeof(<FulfillMods>d__29))] private IEnumerator FulfillMods(LogRequestContext ctx, string folder, string reqByName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <FulfillMods>d__29(0) { <>4__this = this, ctx = ctx, folder = folder, reqByName = reqByName }; } private void FulfillDisconnect(LogRequestContext ctx, string reqByName) { DisconnectLogRegistry.Register(ctx.PlatformId, ctx.PlayerName, reqByName); } [IteratorStateMachine(typeof(<PostAndWait>d__31))] private IEnumerator PostAndWait(MinimalWebhookPoster.Embed embed, List<MinimalWebhookPoster.Attachment> files) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PostAndWait>d__31(0) { <>4__this = this, embed = embed, files = files }; } private static string BuildPlayerFolder(string logsRoot, LogRequestContext ctx) { string text = (ctx.PlatformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_"); string text2 = ctx.PlayerName ?? "unknown"; if (!string.IsNullOrEmpty(text2)) { char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); text2 = string.Concat(text2.Split(invalidFileNameChars)); } return Path.Combine(logsRoot, text2 + "_" + text); } private static string SafePlatformId(LogRequestContext ctx) { return (ctx.PlatformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_"); } private static byte[] SafeReadFile(string path) { try { if (!string.IsNullOrEmpty(path) && File.Exists(path)) { return File.ReadAllBytes(path); } } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller: read failed '" + path + "': " + ex.Message)); } return null; } private static List<byte[]> ChunkBytes(byte[] data, int maxBytes) { if (data == null || data.Length == 0) { return new List<byte[]>(0); } if (data.Length <= maxBytes) { return new List<byte[]>(1) { data }; } List<byte[]> list = new List<byte[]>(); int num; for (int i = 0; i < data.Length; i += num) { num = Math.Min(maxBytes, data.Length - i); byte[] array = new byte[num]; Buffer.BlockCopy(data, i, array, 0, num); list.Add(array); } return list; } private static string FormatBytes(long bytes) { if (bytes <= 0) { return "0 B"; } string[] array = new string[4] { "B", "KB", "MB", "GB" }; double num = bytes; int num2 = 0; while (num >= 1024.0 && num2 < array.Length - 1) { num /= 1024.0; num2++; } return $"{num:0.##} {array[num2]}"; } } public static class ServerHeartbeat { private class HeartbeatHost : MonoBehaviour { } [CompilerGenerated] private sealed class <AddReaction>d__35 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string channelId; public string messageId; public string emoji; private string <encoded>5__1; private string <url>5__2; private int <attempt>5__3; private long <responseCode>5__4; private string <errorMsg>5__5; private string <responseBody>5__6; private float <retryAfterSeconds>5__7; private bool <canRetry>5__8; private UnityWebRequest <req>5__9; private float <wait>5__10; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <AddReaction>d__35(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <encoded>5__1 = null; <url>5__2 = null; <errorMsg>5__5 = null; <responseBody>5__6 = null; <req>5__9 = null; <>1__state = -2; } private bool MoveNext() { //IL_025c: Unknown result type (might be due to invalid IL or missing references) //IL_0266: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <encoded>5__1 = Uri.EscapeDataString(emoji); <url>5__2 = "https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId + "/reactions/" + <encoded>5__1 + "/@me"; <attempt>5__3 = 0; break; case 1: <>1__state = -3; <responseCode>5__4 = <req>5__9.responseCode; <errorMsg>5__5 = <req>5__9.error; try { DownloadHandler downloadHandler = <req>5__9.downloadHandler; <responseBody>5__6 = ((downloadHandler != null) ? downloadHandler.text : null); } catch { <responseBody>5__6 = null; } if (<responseCode>5__4 == 429) { <retryAfterSeconds>5__7 = ParseRetryAfter(<req>5__9, <responseBody>5__6); } <>m__Finally1(); <req>5__9 = null; if (<responseCode>5__4 >= 200 && <responseCode>5__4 < 300) { return false; } <canRetry>5__8 = <responseCode>5__4 == 429 && <attempt>5__3 < 2; if (<canRetry>5__8) { <wait>5__10 = ((<retryAfterSeconds>5__7 > 0f) ? <retryAfterSeconds>5__7 : 1f) + 0.15f; Debug.Log((object)$"[ServerHeartbeat] AddReaction {emoji} rate-limited, retrying in {<wait>5__10:F2}s"); <>2__current = (object)new WaitForSeconds(<wait>5__10); <>1__state = 2; return true; } Debug.LogWarning((object)($"[ServerHeartbeat] AddReaction {emoji} failed ({<responseCode>5__4}): " + <errorMsg>5__5 + " | body: " + TrimForLog(<responseBody>5__6))); return false; case 2: <>1__state = -1; <attempt>5__3++; break; } if (<attempt>5__3 < 3) { <retryAfterSeconds>5__7 = 0f; <req>5__9 = UnityWebRequest.Put(<url>5__2, Array.Empty<byte>()); <>1__state = -3; <req>5__9.method = "PUT"; <req>5__9.SetRequestHeader("Authorization", "Bot " + _botToken); <req>5__9.timeout = 10; <>2__current = <req>5__9.SendWebRequest(); <>1__state = 1; return true; } return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__9 != null) { ((IDisposable)<req>5__9).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <CleanupAndPostNewMessage>d__21 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private string <url>5__1; private UnityWebRequest <req>5__2; private JArray <messages>5__3; private Exception <ex>5__4; private int <deletedCount>5__5; private IEnumerator<JToken> <>s__6; private JToken <msg>5__7; private JObject <msgObj>5__8; private JObject <author>5__9; private bool <isBot>5__10; private JArray <embeds>5__11; private bool <isServerStatus>5__12; private string <messageId>5__13; private string <deleteUrl>5__14; private IEnumerator<JToken> <>s__15; private JToken <embedItem>5__16; private JObject <embedObj>5__17; private string <title>5__18; private UnityWebRequest <deleteReq>5__19; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <CleanupAndPostNewMessage>d__21(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if ((uint)(num - -5) <= 2u || (uint)(num - 1) <= 2u) { try { if ((uint)(num - -5) <= 1u || (uint)(num - 2) <= 1u) { try { if (num == -5 || num == 2) { try { } finally { <>m__Finally3(); } } } finally { <>m__Finally2(); } } } finally { <>m__Finally1(); } } <url>5__1 = null; <req>5__2 = null; <messages>5__3 = null; <ex>5__4 = null; <>s__6 = null; <msg>5__7 = null; <msgObj>5__8 = null; <author>5__9 = null; <embeds>5__11 = null; <messageId>5__13 = null; <deleteUrl>5__14 = null; <>s__15 = null; <embedItem>5__16 = null; <embedObj>5__17 = null; <title>5__18 = null; <deleteReq>5__19 = null; <>1__state = -2; } private bool MoveNext() { //IL_04da: Unknown result type (might be due to invalid IL or missing references) //IL_04e4: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; Debug.Log((object)"[ServerHeartbeat] Cleaning up old status messages..."); <url>5__1 = "https://discord.com/api/v10/channels/" + _channelId + "/messages?limit=50"; <req>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <req>5__2.SetRequestHeader("Authorization", "Bot " + _botToken); <req>5__2.timeout = 15; <>2__current = <req>5__2.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<req>5__2.responseCode == 200) { try { <messages>5__3 = JArray.Parse(<req>5__2.downloadHandler.text); } catch (Exception ex) { <ex>5__4 = ex; Debug.LogWarning((object)("[ServerHeartbeat] Failed to parse messages: " + <ex>5__4.Message)); <messages>5__3 = null; } if (<messages>5__3 != null && ((JContainer)<messages>5__3).Count > 0) { <deletedCount>5__5 = 0; <>s__6 = <messages>5__3.GetEnumerator(); <>1__state = -4; goto IL_0525; } goto IL_056e; } Debug.LogWarning((object)$"[ServerHeartbeat] Failed to fetch messages ({<req>5__2.responseCode})"); goto IL_059a; case 2: <>1__state = -5; if (<deleteReq>5__19.responseCode == 204) { <deletedCount>5__5++; Debug.Log((object)("[ServerHeartbeat] Deleted old status message: " + <messageId>5__13)); } <>m__Finally3(); <deleteReq>5__19 = null; if (<deletedCount>5__5 > 0 && <deletedCount>5__5 % 3 == 0) { <>2__current = (object)new WaitForSeconds(0.5f); <>1__state = 3; return true; } goto IL_04fa; case 3: <>1__state = -4; goto IL_04fa; case 4: { <>1__state = -1; return false; } IL_056e: <messages>5__3 = null; goto IL_059a; IL_059a: <>m__Finally1(); <req>5__2 = null; <>2__current = PostStatusMessage(); <>1__state = 4; return true; IL_0525: while (<>s__6.MoveNext()) { <msg>5__7 = <>s__6.Current; ref JObject reference = ref <msgObj>5__8; JToken obj = <msg>5__7; reference = (JObject)(object)((obj is JObject) ? obj : null); if (<msgObj>5__8 == null) { continue; } ref JObject reference2 = ref <author>5__9; JToken obj2 = <msgObj>5__8["author"]; reference2 = (JObject)(object)((obj2 is JObject) ? obj2 : null); if (<author>5__9 == null) { continue; } JToken obj3 = <author>5__9["bot"]; <isBot>5__10 = obj3 != null && Extensions.Value<bool>((IEnumerable<JToken>)obj3); if (!<isBot>5__10) { continue; } ref JArray reference3 = ref <embeds>5__11; JToken obj4 = <msgObj>5__8["embeds"]; reference3 = (JArray)(object)((obj4 is JArray) ? obj4 : null); if (<embeds>5__11 == null || ((JContainer)<embeds>5__11).Count == 0) { continue; } <isServerStatus>5__12 = false; <>s__15 = <embeds>5__11.GetEnumerator(); try { while (<>s__15.MoveNext()) { <embedItem>5__16 = <>s__15.Current; ref JObject reference4 = ref <embedObj>5__17; JToken obj5 = <embedItem>5__16; reference4 = (JObject)(object)((obj5 is JObject) ? obj5 : null); JObject obj6 = <embedObj>5__17; <title>5__18 = ((obj6 == null) ? null : ((object)obj6["title"])?.ToString()) ?? ""; if (<title>5__18.Contains("Server Online") || <title>5__18.Contains("Server Offline") || <title>5__18.Contains("Server Restarting") || <title>5__18.Contains("Server Stopped") || <title>5__18.Contains("Server Crash")) { <isServerStatus>5__12 = true; break; } <embedObj>5__17 = null; <title>5__18 = null; <embedItem>5__16 = null; } } finally { if (<>s__15 != null) { <>s__15.Dispose(); } } <>s__15 = null; if (<isServerStatus>5__12) { <messageId>5__13 = ((object)<msgObj>5__8["id"])?.ToString(); if (!string.IsNullOrEmpty(<messageId>5__13)) { <deleteUrl>5__14 = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + <messageId>5__13; <deleteReq>5__19 = UnityWebRequest.Delete(<deleteUrl>5__14); <>1__state = -5; <deleteReq>5__19.SetRequestHeader("Authorization", "Bot " + _botToken); <deleteReq>5__19.timeout = 10; <>2__current = <deleteReq>5__19.SendWebRequest(); <>1__state = 2; return true; } } } <>m__Finally2(); <>s__6 = null; if (<deletedCount>5__5 > 0) { Debug.Log((object)$"[ServerHeartbeat] Cleaned up {<deletedCount>5__5} old status message(s)"); } goto IL_056e; IL_04fa: <msgObj>5__8 = null; <author>5__9 = null; <embeds>5__11 = null; <messageId>5__13 = null; <deleteUrl>5__14 = null; <msg>5__7 = null; goto IL_0525; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__2 != null) { ((IDisposable)<req>5__2).Dispose(); } } private void <>m__Finally2() { <>1__state = -3; if (<>s__6 != null) { <>s__6.Dispose(); } } private void <>m__Finally3() { <>1__state = -4; if (<deleteReq>5__19 != null) { ((IDisposable)<deleteReq>5__19).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <DoRestart>d__38 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string reqByName; private Dictionary<string, object> <embed>5__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <DoRestart>d__38(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <embed>5__1 = null; <>1__state = -2; } private bool MoveNext() { //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; Debug.Log((object)("[ServerHeartbeat] Server RESTART requested by '" + reqByName + "'")); _cleanShutdown = true; try { WriteSentinel("restarting"); } catch { } <embed>5__1 = new Dictionary<string, object> { { "title", "\ud83d\udd04 Server Restarting..." }, { "description", "Restart requested by " + reqByName }, { "color", 15105570 } }; <>2__current = EditStatusMessageCoroutine(_statusMessageId, <embed>5__1); <>1__state = 1; return true; case 1: <>1__state = -1; WriteFlag("restart_requested.flag", $"Restart requested by {reqByName} at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); <>2__current = (object)new WaitForSeconds(3f); <>1__state = 2; return true; case 2: <>1__state = -1; Application.Quit(); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <DoStop>d__39 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string reqByName; private Dictionary<string, object> <embed>5__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <DoStop>d__39(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <embed>5__1 = null; <>1__state = -2; } private bool MoveNext() { //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; Debug.Log((object)("[ServerHeartbeat] Server STOP requested by '" + reqByName + "'")); _cleanShutdown = true; try { WriteSentinel("stopped"); } catch { } <embed>5__1 = new Dictionary<string, object> { { "title", "⛔ Server Stopped" }, { "description", "Stop requested by " + reqByName }, { "color", 15548997 } }; <>2__current = EditStatusMessageCoroutine(_statusMessageId, <embed>5__1); <>1__state = 1; return true; case 1: <>1__state = -1; WriteFlag("stop_requested.flag", $"Stop requested by {reqByName} at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); <>2__current = (object)new WaitForSeconds(3f); <>1__state = 2; return true; case 2: <>1__state = -1; Application.Quit(); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <EditStatusMessageCoroutine>d__43 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string messageId; public Dictionary<string, object> embed; private string <json>5__1; private string <url>5__2; private UnityWebRequest <req>5__3; private byte[] <body>5__4; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <EditStatusMessageCoroutine>d__43(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <json>5__1 = null; <url>5__2 = null; <req>5__3 = null; <body>5__4 = null; <>1__state = -2; } private bool MoveNext() { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Expected O, but got Unknown //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Expected O, but got Unknown //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (string.IsNullOrEmpty(messageId) || string.IsNullOrEmpty(_channelId)) { return false; } <json>5__1 = BuildEmbedPayload(embed); <url>5__2 = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + messageId; <req>5__3 = new UnityWebRequest(<url>5__2, "PATCH"); <>1__state = -3; <body>5__4 = Encoding.UTF8.GetBytes(<json>5__1); <req>5__3.uploadHandler = (UploadHandler)new UploadHandlerRaw(<body>5__4); <req>5__3.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); <req>5__3.SetRequestHeader("Authorization", "Bot " + _botToken); <req>5__3.SetRequestHeader("Content-Type", "application/json"); <req>5__3.timeout = 10; <>2__current = <req>5__3.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; <body>5__4 = null; <>m__Finally1(); <req>5__3 = null; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__3 != null) { ((IDisposable)<req>5__3).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <HeartbeatLoop>d__24 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private Dictionary<string, object> <embed>5__1; private string <json>5__2; private string <url>5__3; private bool <useWebhook>5__4; private string[] <parts>5__5; private UnityWebRequest <req>5__6; private byte[] <body>5__7; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <HeartbeatLoop>d__24(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 2) { try { } finally { <>m__Finally1(); } } <embed>5__1 = null; <json>5__2 = null; <url>5__3 = null; <parts>5__5 = null; <req>5__6 = null; <body>5__7 = null; <>1__state = -2; } private bool MoveNext() { //IL_0255: Unknown result type (might be due to invalid IL or missing references) //IL_025b: Invalid comparison between Unknown and I4 //IL_0263: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Invalid comparison between Unknown and I4 //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Expected O, but got Unknown //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01c1: Expected O, but got Unknown //IL_01c8: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; break; case 1: <>1__state = -1; if (_cleanShutdown) { return false; } try { WriteSentinel("running"); } catch { } if (string.IsNullOrEmpty(_statusMessageId)) { break; } <embed>5__1 = BuildOnlineEmbed(); <json>5__2 = BuildEmbedPayload(<embed>5__1); <useWebhook>5__4 = !string.IsNullOrEmpty(_statusWebhookUrl); if (<useWebhook>5__4) { <parts>5__5 = _statusWebhookUrl.Split(new string[1] { "/webhooks/" }, StringSplitOptions.None); if (<parts>5__5.Length == 2) { <url>5__3 = _statusWebhookUrl + "/messages/" + _statusMessageId; } else { <useWebhook>5__4 = false; <url>5__3 = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + _statusMessageId; } <parts>5__5 = null; } else { <url>5__3 = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + _statusMessageId; } <req>5__6 = new UnityWebRequest(<url>5__3, "PATCH"); <>1__state = -3; <body>5__7 = Encoding.UTF8.GetBytes(<json>5__2); <req>5__6.uploadHandler = (UploadHandler)new UploadHandlerRaw(<body>5__7); <req>5__6.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); if (!<useWebhook>5__4) { <req>5__6.SetRequestHeader("Authorization", "Bot " + _botToken); } <req>5__6.SetRequestHeader("Content-Type", "application/json"); <req>5__6.timeout = 10; <>2__current = <req>5__6.SendWebRequest(); <>1__state = 2; return true; case 2: <>1__state = -3; if ((int)<req>5__6.result == 2 || (int)<req>5__6.result == 3) { object arg = <req>5__6.responseCode; DownloadHandler downloadHandler = <req>5__6.downloadHandler; Debug.LogWarning((object)$"[ServerHeartbeat] Heartbeat edit failed ({arg}): {((downloadHandler != null) ? downloadHandler.text : null) ?? <req>5__6.error}"); } <body>5__7 = null; <>m__Finally1(); <req>5__6 = null; <embed>5__1 = null; <json>5__2 = null; <url>5__3 = null; break; } if (!_cleanShutdown) { <>2__current = (object)new WaitForSecondsRealtime(60f); <>1__state = 1; return true; } return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__6 != null) { ((IDisposable)<req>5__6).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PostStatusMessage>d__22 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private Dictionary<string, object> <embed>5__1; private string <json>5__2; private string <url>5__3; private UnityWebRequest <req>5__4; private byte[] <body>5__5; private string <responseText>5__6; private JObject <resp>5__7; private Exception <ex>5__8; private string <errBody>5__9; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PostStatusMessage>d__22(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <embed>5__1 = null; <json>5__2 = null; <url>5__3 = null; <req>5__4 = null; <body>5__5 = null; <responseText>5__6 = null; <resp>5__7 = null; <ex>5__8 = null; <errBody>5__9 = null; <>1__state = -2; } private bool MoveNext() { //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Expected O, but got Unknown //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <embed>5__1 = BuildOnlineEmbed(); <json>5__2 = BuildEmbedPayload(<embed>5__1); <url>5__3 = ((!string.IsNullOrEmpty(_statusWebhookUrl)) ? (_statusWebhookUrl + "?wait=true") : ("https://discord.com/api/v10/channels/" + _channelId + "/messages")); <req>5__4 = new UnityWebRequest(<url>5__3, "POST"); <>1__state = -3; <body>5__5 = Encoding.UTF8.GetBytes(<json>5__2); <req>5__4.uploadHandler = (UploadHandler)new UploadHandlerRaw(<body>5__5); <req>5__4.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); if (string.IsNullOrEmpty(_statusWebhookUrl)) { <req>5__4.SetRequestHeader("Authorization", "Bot " + _botToken); } <req>5__4.SetRequestHeader("Content-Type", "application/json"); <req>5__4.timeout = 15; <>2__current = <req>5__4.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<req>5__4.responseCode == 200 || <req>5__4.responseCode == 201) { try { DownloadHandler downloadHandler = <req>5__4.downloadHandler; <responseText>5__6 = ((downloadHandler != null) ? downloadHandler.text : null); if (!string.IsNullOrEmpty(<responseText>5__6)) { <resp>5__7 = JObject.Parse(<responseText>5__6); _statusMessageId = ((JToken)<resp>5__7).Value<string>((object)"id"); Debug.Log((object)("[ServerHeartbeat] Status message posted to channel " + _channelId + " (msg ID: " + _statusMessageId + ")")); <resp>5__7 = null; } <responseText>5__6 = null; } catch (Exception ex) { <ex>5__8 = ex; Debug.LogWarning((object)("[ServerHeartbeat] Posted but failed to parse message ID: " + <ex>5__8.Message)); } } else { DownloadHandler downloadHandler2 = <req>5__4.downloadHandler; <errBody>5__9 = ((downloadHandler2 != null) ? downloadHandler2.text : null) ?? <req>5__4.error; Debug.LogWarning((object)$"[ServerHeartbeat] Failed to post status message ({<req>5__4.responseCode}): {<errBody>5__9}"); <errBody>5__9 = null; } <body>5__5 = null; <>m__Finally1(); <req>5__4 = null; _heartbeatCoroutine = _host.StartCoroutine(HeartbeatLoop()); _host.StartCoroutine(SeedReactions()); return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__4 != null) { ((IDisposable)<req>5__4).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <ReactionPollLoop>d__25 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private string[] <emojis>5__1; private string[] <>s__2; private int <>s__3; private string <emoji>5__4; private string <encoded>5__5; private string <url>5__6; private UnityWebRequest <req>5__7; private string <body>5__8; private JArray <users>5__9; private IEnumerator<JToken> <>s__10; private JToken <userToken>5__11; private string <userId>5__12; private string <key>5__13; private string <userName>5__14; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ReactionPollLoop>d__25(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if ((uint)(num - -4) <= 1u || (uint)(num - 4) <= 2u) { try { if (num == -4 || (uint)(num - 5) <= 1u) { try { } finally { <>m__Finally2(); } } } finally { <>m__Finally1(); } } <emojis>5__1 = null; <>s__2 = null; <emoji>5__4 = null; <encoded>5__5 = null; <url>5__6 = null; <req>5__7 = null; <body>5__8 = null; <users>5__9 = null; <>s__10 = null; <userToken>5__11 = null; <userId>5__12 = null; <key>5__13 = null; <userName>5__14 = null; <>1__state = -2; } private bool MoveNext() { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Expected O, but got Unknown bool result; try { switch (<>1__state) { default: result = false; goto end_IL_0000; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(10f); <>1__state = 1; result = true; goto end_IL_0000; case 1: <>1__state = -1; <>2__current = ResolveBotUserId(); <>1__state = 2; result = true; goto end_IL_0000; case 2: <>1__state = -1; <emojis>5__1 = new string[2] { "\ud83d\udd04", "⛔" }; break; case 3: <>1__state = -1; if (!_cleanShutdown) { if (string.IsNullOrEmpty(_statusMessageId) || string.IsNullOrEmpty(_channelId)) { break; } <>s__2 = <emojis>5__1; <>s__3 = 0; goto IL_0500; } result = false; goto end_IL_0000; case 4: <>1__state = -3; if (<req>5__7.responseCode == 200) { DownloadHandler downloadHandler = <req>5__7.downloadHandler; <body>5__8 = ((downloadHandler != null) ? downloadHandler.text : null); if (!string.IsNullOrEmpty(<body>5__8)) { try { <users>5__9 = JArray.Parse(<body>5__8); } catch { goto IL_04c3; } if (<users>5__9 != null) { <>s__10 = <users>5__9.GetEnumerator(); <>1__state = -4; while (true) { if (!<>s__10.MoveNext()) { <>m__Finally2(); <>s__10 = null; <body>5__8 = null; <users>5__9 = null; <>m__Finally1(); <req>5__7 = null; <encoded>5__5 = null; <url>5__6 = null; <emoji>5__4 = null; break; } <userToken>5__11 = <>s__10.Current; JToken obj2 = <userToken>5__11; JToken obj3 = ((obj2 is JObject) ? obj2 : null); <userId>5__12 = ((obj3 != null) ? obj3.Value<string>((object)"id") : null); if (string.IsNullOrEmpty(<userId>5__12) || (!string.IsNullOrEmpty(_botUserId) && <userId>5__12 == _botUserId)) { continue; } <key>5__13 = _statusMessageId + ":" + <userId>5__12 + ":" + <emoji>5__4; if (_fulfilledReactions.Contains(<key>5__13)) { continue; } _fulfilledReactions.Add(<key>5__13); JToken obj4 = <userToken>5__11; JToken obj5 = ((obj4 is JObject) ? obj4 : null); <userName>5__14 = ((obj5 == null) ? null : ((object)((JObject)obj5)["username"])?.ToString()) ?? <userId>5__12; Debug.Log((object)("[ServerHeartbeat] '" + <userName>5__14 + "' clicked " + <emoji>5__4)); if (<emoji>5__4 == "\ud83d\udd04") { <>2__current = DoRestart(<userName>5__14); <>1__state = 5; result = true; } else { if (!(<emoji>5__4 == "⛔")) { <userId>5__12 = null; <key>5__13 = null; <userName>5__14 = null; <userToken>5__11 = null; continue; } <>2__current = DoStop(<userName>5__14); <>1__state = 6; result = true; } goto end_IL_0000; } goto IL_04f2; } } } goto IL_04c3; case 5: <>1__state = -4; result = false; goto IL_049b; case 6: { <>1__state = -4; result = false; goto IL_049b; } IL_049b: <>m__Finally2(); <>m__Finally1(); goto end_IL_0000; IL_04c3: <>m__Finally1(); goto IL_04f2; IL_04f2: <>s__3++; goto IL_0500; IL_0500: if (<>s__3 >= <>s__2.Length) { <>s__2 = null; break; } <emoji>5__4 = <>s__2[<>s__3]; <encoded>5__5 = Uri.EscapeDataString(<emoji>5__4); <url>5__6 = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + _statusMessageId + "/reactions/" + <encoded>5__5 + "?limit=100"; <req>5__7 = UnityWebRequest.Get(<url>5__6); <>1__state = -3; <req>5__7.SetRequestHeader("Authorization", "Bot " + _botToken); <req>5__7.timeout = 10; <>2__current = <req>5__7.SendWebRequest(); <>1__state = 4; result = true; goto end_IL_0000; } if (!_cleanShutdown) { <>2__current = (object)new WaitForSecondsRealtime(30f); <>1__state = 3; result = true; } else { result = false; } end_IL_0000:; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__7 != null) { ((IDisposable)<req>5__7).Dispose(); } } private void <>m__Finally2() { <>1__state = -3; if (<>s__10 != null) { <>s__10.Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <ResolveBotUserId>d__26 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private string <url>5__1; private UnityWebRequest <req>5__2; private JObject <json>5__3; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ResolveBotUserId>d__26(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <req>5__2 = null; <json>5__3 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/users/@me"; <req>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <req>5__2.SetRequestHeader("Authorization", "Bot " + _botToken); <req>5__2.timeout = 10; <>2__current = <req>5__2.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<req>5__2.responseCode == 200) { try { <json>5__3 = JObject.Parse(<req>5__2.downloadHandler.text); _botUserId = ((object)<json>5__3["id"])?.ToString(); Debug.Log((object)("[ServerHeartbeat] Bot user ID resolved: " + _botUserId)); <json>5__3 = null; } catch { } } <>m__Finally1(); <req>5__2 = null; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__2 != null) { ((IDisposable)<req>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <SeedReactions>d__23 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SeedReactions>d__23(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(2f); <>1__state = 1; return true; case 1: <>1__state = -1; if (string.IsNullOrEmpty(_statusMessageId) || string.IsNullOrEmpty(_channelId)) { Debug.LogWarning((object)"[ServerHeartbeat] Cannot seed reactions - message ID or channel ID missing"); return false; } Debug.Log((object)("[ServerHeartbeat] Seeding control reactions on message " + _statusMessageId)); <>2__current = AddReaction(_channelId, _statusMessageId, "\ud83d\udd04"); <>1__state = 2; return true; case 2: <>1__state = -1; <>2__current = AddReaction(_channelId, _statusMessageId, "⛔"); <>1__state = 3; return true; case 3: <>1__state = -1; Debug.Log((object)"[ServerHeartbeat] Control reactions seeded successfully"); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string DiscordApiBase = "https://discord.com/api/v10"; private const float HeartbeatIntervalSeconds = 60f; private const string SentinelFileName = "server_heartbeat.json"; private const string SentinelDirName = "FiresDiscordIntegration"; private const string EMOJI_RESTART = "\ud83d\udd04"; private const string EMOJI_STOP = "⛔"; private static string _sentinelPath; private static string _statusMessageId; private static string _botToken; private static string _channelId; private static string _statusWebhookUrl; private static string _webhookName; private static string _brandLabel; private static string _botUserId; private static Coroutine _heartbeatCoroutine; private static MonoBehaviour _host; private static DateTime _serverStartUtc; private static bool _cleanShutdown; private static readonly HashSet<string> _fulfilledReactions = new HashSet<string>(StringComparer.Ordinal); private const int MaxReactionAttempts = 3; private const int HttpStatusRateLimited = 429; private const int HttpStatusSuccessRangeStart = 200; private const int HttpStatusSuccessRangeEndExcl = 300; private const int AddReactionTimeoutSeconds = 10; private const int ResponseBodyLogMaxChars = 300; private const float DefaultRateLimitWaitSeconds = 1f; private const float RateLimitWaitBufferSeconds = 0.15f; public static void OnServerStart(string botToken, string statusWebhookUrl, string channelIdOverride, string webhookName, string brandLabel) { _botToken = botToken; _statusWebhookUrl = statusWebhookUrl; _webhookName = webhookName; _brandLabel = brandLabel; _channelId = channelIdOverride; if (string.IsNullOrEmpty(_botToken) || string.IsNullOrEmpty(_channelId)) { Debug.Log((object)"[ServerHeartbeat] BotToken or StatusChannelId not set - heartbeat disabled."); InitSentinel(); SpawnCrashNotifier(); return; } _serverStartUtc = DateTime.UtcNow; _cleanShutdown = false; _statusMessageId = null; InitSentinel(); EnsureHost(); if (!((Object)(object)_host == (Object)null)) { Debug.Log((object)$"[ServerHeartbeat] Starting - server start time: {_serverStartUtc:yyyy-MM-dd HH:mm:ss} UTC"); _host.StartCoroutine(CleanupAndPostNewMessage()); SpawnCrashNotifier(); _host.StartCoroutine(ReactionPollLoop()); } } public static void OnServerStop() { _cleanShutdown = true; try { WriteSentinel("stopped"); } catch { } if (_heartbeatCoroutine != null && (Object)(object)_host != (Object)null) { _host.StopCoroutine(_heartbeatCoroutine); _heartbeatCoroutine = null; } if (!string.IsNullOrEmpty(_statusMessageId) && !string.IsNullOrEmpty(_botToken) && !string.IsNullOrEmpty(_channelId)) { try { EditStatusMessageSync(_statusMessageId, BuildOfflineEmbed()); } catch (Exception ex) { Debug.LogWarning((object)("[ServerHeartbeat] Failed to edit status on shutdown: " + ex.Message)); } } } [IteratorStateMachine(typeof(<CleanupAndPostNewMessage>d__21))] private static IEnumerator CleanupAndPostNewMessage() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <CleanupAndPostNewMessage>d__21(0); } [IteratorStateMachine(typeof(<PostStatusMessage>d__22))] private static IEnumerator PostStatusMessage() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PostStatusMessage>d__22(0); } [IteratorStateMachine(typeof(<SeedReactions>d__23))] private static IEnumerator SeedReactions() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SeedReactions>d__23(0); } [IteratorStateMachine(typeof(<HeartbeatLoop>d__24))] private static IEnumerator HeartbeatLoop() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <HeartbeatLoop>d__24(0); } [IteratorStateMachine(typeof(<ReactionPollLoop>d__25))] private static IEnumerator ReactionPollLoop() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ReactionPollLoop>d__25(0); } [IteratorStateMachine(typeof(<ResolveBotUserId>d__26))] private static IEnumerator ResolveBotUserId() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ResolveBotUserId>d__26(0); } [IteratorStateMachine(typeof(<AddReaction>d__35))] private static IEnumerator AddReaction(string channelId, string messageId, string emoji) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <AddReaction>d__35(0) { channelId = channelId, messageId = messageId, emoji = emoji }; } private static float ParseRetryAfter(UnityWebRequest req, string responseBody) { string responseHeader = req.GetResponseHeader("Retry-After"); if (!string.IsNullOrEmpty(responseHeader) && float.TryParse(responseHeader, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return result; } if (string.IsNullOrEmpty(responseBody)) { return 0f; } try { JObject val = JObject.Parse(responseBody); JToken val2 = val["retry_after"]; return (val2 != null) ? Extensions.Value<float>((IEnumerable<JToken>)val2) : 0f; } catch { return 0f; } } private static string TrimForLog(string body) { if (string.IsNullOrEmpty(body)) { return "<empty>"; } if (body.Length <= 300) { return body; } return body.Substring(0, 300) + "..."; } [IteratorStateMachine(typeof(<DoRestart>d__38))] private static IEnumerator DoRestart(string reqByName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <DoRestart>d__38(0) { reqByName = reqByName }; } [IteratorStateMachine(typeof(<DoStop>d__39))] private static IEnumerator DoStop(string reqByName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <DoStop>d__39(0) { reqByName = reqByName }; } private static Dictionary<string, object> BuildOnlineEmbed() { string serverName = GetServerName(); int num = 0; string value = "*No players online*"; try { List<ZNetPeer> list = null; if ((Object)(object)ZNet.instance != (Object)null) { List<ZNetPeer> peers = ZNet.instance.GetPeers(); if (peers != null && peers.Count > 0) { list = new List<ZNetPeer>(peers); } } if (list != null && list.Count > 0) { num = list.Count; List<string> list2 = new List<string>(); foreach (ZNetPeer item in list) { if (item != null && !string.IsNullOrEmpty(item.m_playerName)) { list2.Add("• " + item.m_playerName); } } if (list2.Count > 0) { value = string.Join("\n", list2); } } } catch (Exception ex) { Debug.LogWarning((object)("[ServerHeartbeat] BuildOnlineEmbed exception: " + ex.Message)); } TimeSpan timeSpan = DateTime.UtcNow - _serverStartUtc; string value2 = ((timeSpan.TotalHours >= 1.0) ? $"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m" : $"{(int)timeSpan.TotalMinutes}m"); return new Dictionary<string, object> { { "title", "\ud83d\udfe2 Server Online" }, { "description", "**" + serverName + "** is running." }, { "color", 5763719 }, { "fields", new List<object> { new Dictionary<string, object> { { "name", $"Players Online ({num})" }, { "value", value }, { "inline", false } }, new Dictionary<string, object> { { "name", "Uptime" }, { "value", value2 }, { "inline", true } }, new Dictionary<string, object> { { "name", "Last Heartbeat" }, { "value", DateTime.UtcNow.ToString("HH:mm:ss") + " UTC" }, { "inline", true } } } }, { "footer", new Dictionary<string, object> { { "text", "Updates every 60s — " + (_brandLabel ?? "ServerHeartbeat") } } } }; } private static Dictionary<string, object> BuildOfflineEmbed() { string serverName = GetServerName(); TimeSpan timeSpan = DateTime.UtcNow - _serverStartUtc; string value = ((timeSpan.TotalHours >= 1.0) ? $"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m" : $"{(int)timeSpan.TotalMinutes}m"); return new Dictionary<string, object> { { "title", "\ud83d\udd34 Server Offline" }, { "description", "**" + serverName + "** shut down cleanly." }, { "color", 15548997 }, { "fields", new List<object> { new Dictionary<string, object> { { "name", "Session Duration" }, { "value", value }, { "inline", true } }, new Dictionary<string, object> { { "name", "Stopped At" }, { "value", DateTime.UtcNow.ToString("HH:mm:ss") + " UTC" }, { "inline", true } } } }, { "footer", new Dictionary<string, object> { { "text", _brandLabel ?? "ServerHeartbeat" } } } }; } private static string BuildEmbedPayload(Dictionary<string, object> embed) { Dictionary<string, object> dictionary = new Dictionary<string, object> { { "embeds", new List<object> { embed } } }; string text = DiscordIntegrationConfig.WebhookDisplayName?.Value; string fallback = ((!string.IsNullOrEmpty(text) && text != "Valheim Server") ? text : (_webhookName ?? "Valheim Server")); string value = DiscordBotIdentity.ResolveUsername(fallback); dictionary["username"] = value; string text2 = DiscordIntegrationConfig.WebhookAvatarUrl?.Value; string value2 = ((!string.IsNullOrEmpty(text2)) ? text2 : DiscordBotIdentity.ResolveAvatarUrl(null)); if (!string.IsNullOrEmpty(value2)) { dictionary["avatar_url"] = value2; } return JsonConvert.SerializeObject((object)dictionary); } [IteratorStateMachine(typeof(<EditStatusMessageCoroutine>d__43))] private static IEnumerator EditStatusMessageCoroutine(string messageId, Dictionary<string, object> embed) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <EditStatusMessageCoroutine>d__43(0) { messageId = messageId, embed = embed }; } private static void EditStatusMessageSync(string messageId, Dictionary<string, object> embed) { string data = BuildEmbedPayload(embed); string address = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + messageId; try { using WebClient webClient = new WebClient(); webClient.Headers.Add("Authorization", "Bot " + _botToken); webClient.Headers.Add("Content-Type", "application/json"); webClient.UploadString(address, "PATCH", data); } catch { } } private static void InitSentinel() { try { string text = Path.Combine(Paths.ConfigPath, "FiresDiscordIntegration"); Directory.CreateDirectory(text); _sentinelPath = Path.Combine(text, "server_heartbeat.json"); WriteSentinel("running"); Debug.Log((object)("[ServerHeartbeat] Sentinel initialised at " + _sentinelPath)); } catch (Exception ex) { Debug.LogWarning((object)("[ServerHeartbeat] Sentinel init failed: " + ex.Message)); } } private static void WriteSentinel(string status) { if (!string.IsNullOrEmpty(_sentinelPath)) { int id = Process.GetCurrentProcess().Id; string text = _statusMessageId ?? ""; string contents = $"{{\"status\":\"{status}\",\"heartbeat\":\"{DateTime.UtcNow:O}\",\"pid\":{id},\"messageId\":\"{text}\"}}"; File.WriteAllText(_sentinelPath, contents); } } private static void SpawnCrashNotifier() { try { if (!string.IsNullOrEmpty(_statusWebhookUrl) && !string.IsNullOrEmpty(_sentinelPath)) { int id = Process.GetCurrentProcess().Id; string serverName = GetServerName(); string text = _sentinelPath.Replace("'", "''"); string text2 = _statusWebhookUrl.Replace("'", "''"); string text3 = serverName.Replace("'", "''").Replace("\"", "\\\""); string text4 = (_webhookName ?? "Valheim Server").Replace("'", "''"); string value = "try{ (Get-Process -Id " + id + " -EA SilentlyContinue).WaitForExit() }catch{}\nStart-Sleep 3\n$s=Get-Content '" + text + "' -Raw -EA SilentlyContinue\nif($s -notmatch '\"running\"'){exit}\n$t=(Get-Date).ToUniversalTime().ToString('HH:mm:ss')\n$emoji=[char]::ConvertFromUtf32(0x1F4A5)\n$b=@{username='" + text4 + "';embeds=@(@{title=\"\"$emoji Server Crash Detected\"\";description=\"\"**" + text3 + "** crashed or was killed.\"\";color=15105570;fields=@(@{name='Detected At';value=\"\"$t UTC\"\";inline=$true});footer=@{text='" + (_brandLabel ?? "ServerHeartbeat") + "'}})}|ConvertTo-Json -Depth 5 -Compress\ntry{(New-Object Net.WebClient).UploadString('" + text2 + "','POST',$b)|Out-Null}catch{}\ntry{Set-Content '" + text + "' '{\"status\":\"crashed\"}' -EA SilentlyContinue}catch{}"; ProcessStartInfo startInfo = new ProcessStartInfo { FileName = "powershell.exe", Arguments = "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command -", UseShellExecute = false, RedirectStandardInput = true, CreateNoWindow = true }; Process process = Process.Start(startInfo); if (process != null) { process.StandardInput.Write(value); process.StandardInput.Close(); Debug.Log((object)$"[ServerHeartbeat] Crash notifier spawned (watchdog PID {process.Id}, monitoring server PID {id})"); } } } catch (Exception ex) { Debug.LogWarning((object)("[ServerHeartbeat] Failed to spawn crash notifier: " + ex.Message)); } } private static void WriteFlag(string filename, string content) { try { string path = Path.Combine(Paths.BepInExRootPath, "plugins"); string text = Path.Combine(path, filename); File.WriteAllText(text, content); Debug.Log((object)("[ServerHeartbeat] Wrote flag: " + text)); } catch (Exception ex) { Debug.LogWarning((object)("[ServerHeartbeat] Failed to write flag '" + filename + "': " + ex.Message)); } } private static string GetServerName() { return ServerNameResolver.GetOperatorServerName(); } private static void EnsureHost() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown if (!((Object)(object)_host != (Object)null)) { GameObject val = new GameObject("ServerHeartbeat_Host"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; _host = (MonoBehaviour)(object)val.AddComponent<HeartbeatHost>(); } } } } namespace FiresDiscordIntegration.ClientLogRelay.Consumers { public sealed class DiscordWebhookConsumer : IClientLogConsumer { private struct ModIssueHit { public string DisplayName; public int ErrorCount; public int WarningCount; } public const string EMOJI_LOG = "\ud83d\udce9"; public const string EMOJI_ERRORS = "⛔"; public const string EMOJI_MODS = "\ud83e\udde9"; public const string EMOJI_DISCONNECT = "♻\ufe0f"; public const string EMOJI_RESTART = "\ud83d\udd04"; public Func<bool> EnabledGate; public Func<string> WebhookUrl; public Func<string> WebhookName; public Func<string> BrandLabel; public Func<string> AvatarUrl; public Func<bool> AttachFullLog; public Func<bool> AttachModList; public Func<bool> AttachErrorsWarnings; public Func<bool> OnlyIfErrorsOrWarnings; public Func<string> ServerName; public Func<bool> EnableLogRequestReaction; public Func<string> BotToken; private const int ProblemModsMaxShown = 5; private static readonly HashSet<string> NonModSourceTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "(General / Unity)", "Unity Log", "BepInEx" }; public string ConsumerId { get; } public DiscordWebhookConsumer(string consumerId) { ConsumerId = consumerId ?? throw new ArgumentNullException("consumerId"); } public void OnClientArtifacts(ClientLogArtifacts artifacts) { if (EnabledGate != null && !EnabledGate()) { return; } string text = WebhookUrl?.Invoke(); if (!MinimalWebhookPoster.IsValidWebhookUrl(text) || (OnlyIfErrorsOrWarnings != null && OnlyIfErrorsOrWarnings() && artifacts.ErrorCount == 0 && artifacts.WarningCount == 0)) { return; } int color = ((artifacts.ErrorCount > 0) ? 15548997 : ((artifacts.WarningCount > 0) ? 15105570 : 5763719)); string text2 = artifacts.CapturedUtc.ToString("yyyyMMdd_HHmmss"); string safePlatformId = artifacts.SafePlatformId; string text3 = BrandLabel?.Invoke(); bool flag = artifacts.ServerMods != null && artifacts.ServerMods.Count > 0; bool flag2 = artifacts.ModDiff != null; bool enableReaction = EnableLogRequestReaction != null && EnableLogRequestReaction(); string title = ((!flag) ? "\ud83e\udeb5 Client Login — Log Snapshot" : (string.IsNullOrEmpty(text3) ? "\ud83e\udeb5 Client Login Snapshot" : ("\ud83e\udeb5 " + text3 + " — Client Login Snapshot"))); MinimalWebhookPoster.Embed embed = new MinimalWebhookPoster.Embed().SetTitle(title).SetColor(color).AddField("\ud83d\udc64 Player", artifacts.PlayerName, inline: true) .AddField("\ud83d\udd94 Steam ID", artifacts.PlatformId ?? "Unknown", inline: true) .AddField("\ud83e\udde9 Client Mods", (artifacts.ModList?.Count ?? 0).ToString(), inline: true); if (flag) { embed.AddField("\ud83e\udde9 Server Mods", artifacts.ServerMods.Count.ToString(), inline: true); } embed.AddField("\ud83d\udd34 Errors", artifacts.ErrorCount.ToString(), inline: true).AddField("⚠\ufe0f Warnings", artifacts.WarningCount.ToString(), inline: true); List<ModIssueHit> list = FindModsWithIssues(artifacts); if (list.Count > 0) { embed.AddField("\ud83d\udcac Mods w/ Issues", list.Count.ToString(), inline: true); } byte[] logBytes = artifacts.LogBytes; embed.AddField("\ud83d\udcc4 Log Size", FormatBytes((logBytes != null) ? logBytes.Length : 0), inline: true); if (list.Count > 0) { StringBuilder stringBuilder = new StringBuilder(); int num = Math.Min(list.Count, 5); for (int i = 0; i < num; i++) { stringBuilder.AppendLine(FormatProblemModLine(list[i])); } if (list.Count > 5) { stringBuilder.AppendLine($"*({list.Count - 5} more)*"); } embed.AddField("⚠\ufe0f Problem Mods", stringBuilder.ToString().TrimEnd(Array.Empty<char>())); } if (flag2) { ModListDiff.Result modDiff = artifacts.ModDiff; string value = $"client-only: {modDiff.ClientOnly.Count} server-only: {modDiff.ServerOnly.Count} mismatched: {modDiff.VersionMismatches.Count}"; embed.AddField("\ud83d\udd04 Mod Diff (C vs S)", value); if (modDiff.VersionMismatches.Count > 0) { int num2 = 0; int num3 = 0; foreach (ModListDiff.VersionMismatch versionMismatch in modDiff.VersionMismatches) { int num4 = CompareVersions(versionMismatch.ClientVersion, versionMismatch.ServerVersion); if (num4 < 0) { num2++; continue; } if (num4 > 0) { num3++; continue; } num2++; num3++; } string value2 = $"⬆\ufe0f Client: {num2} │ ⬆\ufe0f Server: {num3}"; embed.AddField("\ud83d\udd27 Needs Update", value2); } } string text4 = ServerName?.Invoke(); string text5 = (string.IsNullOrEmpty(text3) ? "ClientLogRelay" : text3); string text6; if (!enableReaction) { text6 = (string.IsNullOrEmpty(text4) ? text5 : (text5 + " — " + text4)); } else { string text7 = "\ud83d\udce9 Log │ ⛔ Errors │ \ud83e\udde9 Mods │ ♻\ufe0f Disconnect"; text6 = (string.IsNullOrEmpty(text4) ? text7 : (text5 + " — " + text4 + "\n" + text7)); } if (!string.IsNullOrEmpty(text6)) { embed.SetFooter(text6); } List<MinimalWebhookPoster.Attachment> list2 = new List<MinimalWebhookPoster.Attachment>(); if (!enableReaction) { if ((AttachFullLog == null || AttachFullLog()) && artifacts.LogBytes != null && artifacts.LogBytes.Length != 0) { list2.Add(new MinimalWebhookPoster.Attachment("client_log_" + safePlatformId + "_" + text2 + ".log", artifacts.LogBytes)); } if ((AttachModList == null || AttachModList()) && artifacts.ModList != null && artifacts.ModList.Count > 0) { list2.Add(new MinimalWebhookPoster.Attachment("modlist_" + safePlatformId + "_" + text2 + ".txt", BuildModListText(artifacts))); } if ((AttachErrorsWarnings == null || AttachErrorsWarnings()) && !string.IsNullOrEmpty(artifacts.ErrorsWarningsReport)) { list2.Add(new MinimalWebhookPoster.Attachment("errors_warnings_" + safePlatformId + "_" + text2 + ".txt", artifacts.ErrorsWarningsReport)); } if (artifacts.ModDiff != null && !string.IsNullOrEmpty(artifacts.ModDiff.Report)) { list2.Add(new MinimalWebhookPoster.Attachment("mod_diff_" + safePlatformId + "_" + text2 + ".txt", artifacts.ModDiff.Report)); } } string botToken = BotToken?.Invoke(); MinimalWebhookPoster.Post(text, embed, list2, WebhookName?.Invoke(), AvatarUrl?.Invoke(), delegate(string messageId, string channelId) { if (enableReaction && !string.IsNullOrEmpty(messageId)) { LogRequestRegistry.Register(new LogRequestContext(messageId, artifacts.PlatformId, artifacts.PlayerName, artifacts.CapturedUtc, ConsumerId, channelId)); if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(channelId)) { MinimalWebhookPoster.AddReactions(botToken, channelId, messageId, "\ud83d\udce9", "⛔", "\ud83e\udde9", "♻\ufe0f"); MinimalWebhookPoster.StartCleanupOldClientLogs(botToken, channelId); } } }); Debug.Log((object)("[ClientLogRelay:" + ConsumerId + "] Posted login snapshot for " + artifacts.PlatformId + " " + $"({list2.Count} attachment(s), err={artifacts.ErrorCount} warn={artifacts.WarningCount})")); } internal static string BuildModListText(ClientLogArtifacts artifacts) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Client Mod List — " + artifacts.PlayerName + " (" + artifacts.PlatformId + ")"); stringBuilder.AppendLine($"# Captured: {artifacts.CapturedUtc:yyyy-MM-dd HH:mm:ss} UTC"); stringBuilder.AppendLine($"# Count: {artifacts.ModList.Count}"); stringBuilder.AppendLine(); foreach (KeyValuePair<string, string> item in artifacts.ModList.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> k) => k.Key, StringComparer.OrdinalIgnoreCase)) { stringBuilder.Append(item.Key).Append('=').AppendLine(item.Value); } return stringBuilder.ToString(); } private static List<ModIssueHit> FindModsWithIssues(ClientLogArtifacts artifacts) { if (artifacts.SourceBreakdown == null || artifacts.SourceBreakdown.Count == 0) { return new List<ModIssueHit>(); } List<ModIssueHit> list = new List<ModIssueHit>(artifacts.SourceBreakdown.Count); foreach (LogErrorWarningExtractor.SourceIssueCount item in artifacts.SourceBreakdown) { if ((item.ErrorCount != 0 || item.WarningCount != 0) && !NonModSourceTags.Contains(item.SourceTag)) { list.Add(new ModIssueHit { DisplayName = ResolveModDisplayName(item.SourceTag, artifacts.ModList), ErrorCount = item.ErrorCount, WarningCount = item.WarningCount }); } } list.Sort(delegate(ModIssueHit a, ModIssueHit b) { int num = b.ErrorCount.CompareTo(a.ErrorCount); return (num != 0) ? num : b.WarningCount.CompareTo(a.WarningCount); }); return list; } private static string ResolveModDisplayName(string sourceTag, IReadOnlyDictionary<string, string> modList) { if (modList == null || modList.Count == 0) { return sourceTag; } string text = StripSeparatorsAndLower(sourceTag); if (text.Length == 0) { return sourceTag; } foreach (KeyValuePair<string, string> mod in modList) { string key = mod.Key; if (!string.IsNullOrEmpty(key)) { int num = key.LastIndexOf('.'); string text2 = ((num >= 0) ? key.Substring(num + 1) : key); if (StripSeparatorsAndLower(text2) == text) { return text2; } } } return sourceTag; } private static string StripSeparatorsAndLower(string s) { if (string.IsNullOrEmpty(s)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { if (c != '_' && c != '-' && c != '.' && c != ' ') { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } private static string FormatProblemModLine(ModIssueHit hit) { if (hit.ErrorCount > 0 && hit.WarningCount > 0) { return $"• {hit.DisplayName} ({hit.ErrorCount} error{Plural(hit.ErrorCount)}, {hit.WarningCount} warning{Plural(hit.WarningCount)})"; } if (hit.ErrorCount > 0) { return $"• {hit.DisplayName} ({hit.ErrorCount} error{Plural(hit.ErrorCount)})"; } return $"• {hit.DisplayName} ({hit.WarningCount} warning{Plural(hit.WarningCount)})"; } private static string Plural(int count) { return (count == 1) ? "" : "s"; } private static string FormatBytes(long bytes) { if (bytes <= 0) { return "0 B"; } string[] array = new string[4] { "B", "KB", "MB", "GB" }; double num = bytes; int num2 = 0; while (num >= 1024.0 && num2 < array.Length - 1) { num /= 1024.0; num2++; } return $"{num:0.##} {array[num2]}"; } private static int CompareVersions(string a, string b) { if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) { return 0; } string[] array = a.Split(new char[1] { '.' }); string[] array2 = b.Split(new char[1] { '.' }); int num = Math.Max(array.Length, array2.Length); for (int i = 0; i < num; i++) { int result = 0; int result2 = 0; if (i < array.Length) { int.TryParse(array[i], out result); } if (i < array2.Length) { int.TryParse(array2[i], out result2); } if (result != result2) { return result.CompareTo(result2); } } return 0; } } public sealed class DiskConsumer : IClientLogConsumer { private readonly Func<string> _rootDirResolver; private readonly Func<bool> _enabledGate; public string ConsumerId { get; } public DiskConsumer(string consumerId, Func<string> rootDirResolver, Func<bool> enabledGate = null) { ConsumerId = consumerId ?? throw new ArgumentNullException("consumerId"); _rootDirResolver = rootDirResolver ?? throw new ArgumentNullException("rootDirResolver"); _enabledGate = enabledGate; } public void OnClientArtifacts(ClientLogArtifacts artifacts) { if (_enabledGate != null && !_enabledGate()) { return; } string text; try { text = _rootDirResolver(); } catch (Exception ex) { Debug.LogWarning((object)("[ClientLogRelay:" + ConsumerId + "] Root resolver threw: " + ex.Message)); return; } if (string.IsNullOrEmpty(text)) { Debug.LogWarning((object)("[ClientLogRelay:" + ConsumerId + "] Root directory is empty; skipping")); return; } string text2 = ClientLogArtifactWriter.Write(text, artifacts); if (text2 != null) { Debug.Log((object)("[ClientLogRelay:" + ConsumerId + "] Wrote artifacts for " + artifacts.PlatformId + " ? " + text2)); } } } } namespace FiresDiscordIntegration.Discord { public static class DiscordBotIdentity { [CompilerGenerated] private sealed class <FetchIdentityCoroutine>d__10 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; private string <url>5__1; private UnityWebRequest <req>5__2; private JObject <json>5__3; private string <id>5__4; private string <username>5__5; private string <avatarHash>5__6; private Exception <ex>5__7; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <FetchIdentityCoroutine>d__10(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <req>5__2 = null; <json>5__3 = null; <id>5__4 = null; <username>5__5 = null; <avatarHash>5__6 = null; <ex>5__7 = null; <>1__state = -2; } private bool MoveNext() { //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Invalid comparison between Unknown and I4 try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/users/@me"; <req>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <req>5__2.SetRequestHeader("Authorization", "Bot " + token); <req>5__2.timeout = 10; <>2__current = <req>5__2.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if ((int)<req>5__2.result == 1) { try { DownloadHandler downloadHandler = <req>5__2.downloadHandler; <json>5__3 = JObject.Parse(((downloadHandler != null) ? downloadHandler.text : null) ?? "{}"); <id>5__4 = ((JToken)<json>5__3).Value<string>((object)"id"); <username>5__5 = ((JToken)<json>5__3).Value<string>((object)"username"); <avatarHash>5__6 = ((JToken)<json>5__3).Value<string>((object)"avatar"); _cachedUsername = (string.IsNullOrEmpty(<username>5__5) ? null : <username>5__5); if (!string.IsNullOrEmpty(<id>5__4) && !string.IsNullOrEmpty(<avatarHash>5__6)) { _cachedAvatarUrl = "https://cdn.discordapp.com/avatars/" + <id>5__4 + "/" + <avatarHash>5__6 + ".png?size=128"; } _resolved = true; Debug.Log((object)("[DiscordBotIdentity] Resolved bot identity: username='" + _cachedUsername + "', avatar=" + (string.IsNullOrEmpty(_cachedAvatarUrl) ? "<none>" : "<set>"))); <json>5__3 = null; <id>5__4 = null; <username>5__5 = null; <avatarHash>5__6 = null; } catch (Exception ex) { <ex>5__7 = ex; Debug.LogWarning((object)("[DiscordBotIdentity] Failed to parse @me response: " + <ex>5__7.Message)); } } else { Debug.LogWarning((object)$"[DiscordBotIdentity] /users/@me failed ({<req>5__2.responseCode}): {<req>5__2.error}"); } <>m__Finally1(); <req>5__2 = null; _resolveInFlight = false; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__2 != null) { ((IDisposable)<req>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string DiscordApiBase = "https://discord.com/api/v10"; private static string _cachedUsername; private static string _cachedAvatarUrl; private static bool _resolved; private static bool _resolveInFlight; private static string _cachedForToken; public static string ResolveUsername(string fallback) { EnsureResolved(); if (_resolved && !string.IsNullOrEmpty(_cachedUsername)) { return _cachedUsername; } return fallback; } public static string ResolveAvatarUrl(string fallback) { EnsureResolved(); if (_resolved && !string.IsNullOrEmpty(_cachedAvatarUrl)) { return _cachedAvatarUrl; } return fallback; } public static void Invalidate() { _resolved = false; _resolveInFlight = false; _cachedUsername = null; _cachedAvatarUrl = null; _cachedForToken = null; } private static void EnsureResolved() { string token = BotTokenFile.Token; if (string.IsNullOrEmpty(token)) { return; } if (_resolved && _cachedForToken != token) { Invalidate(); } if (!_resolved && !_resolveInFlight) { _resolveInFlight = true; _cachedForToken = token; MonoBehaviour val = (MonoBehaviour)(((object)FiresDiscordIntegrationPlugin.Instance) ?? ((object)ZNet.instance)); if ((Object)(object)val == (Object)null) { _resolveInFlight = false; } else { val.StartCoroutine(FetchIdentityCoroutine(token)); } } } [IteratorStateMachine(typeof(<FetchIdentityCoroutine>d__10))] private static IEnumerator FetchIdentityCoroutine(string token) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <FetchIdentityCoroutine>d__10(0) { token = token }; } } public static class DiscordBotListener { private struct PendingRelay { public string AuthorId; public string AuthorName; public string Content; } private class CoroutineHost : MonoBehaviour { } [CompilerGenerated] private sealed class <CleanupOldMessages>d__36 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string channelId; public TimeSpan maxAge; private DateTime <cutoffTime>5__1; private string <url>5__2; private UnityWebRequest <request>5__3; private JArray <messages>5__4; private int <deletedCount>5__5; private Exception <ex>5__6; private IEnumerator<JToken> <>s__7; private JToken <msg>5__8; private JObject <msgObj>5__9; private string <messageId>5__10; private string <timestampStr>5__11; private DateTime <messageTime>5__12; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <CleanupOldMessages>d__36(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if ((uint)(num - -4) <= 1u || (uint)(num - 1) <= 2u) { try { if (num == -4 || (uint)(num - 2) <= 1u) { try { } finally { <>m__Finally2(); } } } finally { <>m__Finally1(); } } <url>5__2 = null; <request>5__3 = null; <messages>5__4 = null; <ex>5__6 = null; <>s__7 = null; <msg>5__8 = null; <msgObj>5__9 = null; <messageId>5__10 = null; <timestampStr>5__11 = null; <>1__state = -2; } private bool MoveNext() { //IL_02b9: Unknown result type (might be due to invalid IL or missing references) //IL_02c3: Expected O, but got Unknown bool result; try { switch (<>1__state) { default: result = false; goto end_IL_0000; case 0: <>1__state = -1; <cutoffTime>5__1 = DateTime.UtcNow - maxAge; <url>5__2 = "https://discord.com/api/v10/channels/" + channelId + "/messages?limit=100"; <request>5__3 = UnityWebRequest.Get(<url>5__2); <>1__state = -3; <request>5__3.SetRequestHeader("Authorization", "Bot " + token); <request>5__3.timeout = 15; <>2__current = <request>5__3.SendWebRequest(); <>1__state = 1; result = true; goto end_IL_0000; case 1: <>1__state = -3; if (<request>5__3.responseCode != 200) { LogVerbose($"Failed to fetch messages for cleanup ({<request>5__3.responseCode}): {<request>5__3.error}"); result = false; break; } <messages>5__4 = null; try { <messages>5__4 = JArray.Parse(<request>5__3.downloadHandler.text); } catch (Exception ex) { <ex>5__6 = ex; Debug.LogWarning((object)("[Discord] Message cleanup parse failed: " + <ex>5__6.Message)); result = false; break; } <deletedCount>5__5 = 0; <>s__7 = <messages>5__4.GetEnumerator(); <>1__state = -4; goto IL_02f7; case 2: <>1__state = -4; <deletedCount>5__5++; <>2__current = (object)new WaitForSecondsRealtime(0.5f); <>1__state = 3; result = true; goto end_IL_0000; case 3: { <>1__state = -4; goto IL_02da; } IL_02da: <msgObj>5__9 = null; <messageId>5__10 = null; <timestampStr>5__11 = null; <msg>5__8 = null; goto IL_02f7; IL_02f7: while (true) { if (<>s__7.MoveNext()) { <msg>5__8 = <>s__7.Current; ref JObject reference = ref <msgObj>5__9; JToken obj = <msg>5__8; reference = (JObject)(object)((obj is JObject) ? obj : null); if (<msgObj>5__9 == null) { continue; } <messageId>5__10 = ((JToken)<msgObj>5__9).Value<string>((object)"id"); <timestampStr>5__11 = ((JToken)<msgObj>5__9).Value<string>((object)"timestamp"); if (string.IsNullOrEmpty(<messageId>5__10) || string.IsNullOrEmpty(<timestampStr>5__11) || !DateTime.TryParse(<timestampStr>5__11, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out <messageTime>5__12)) { continue; } if (<messageTime>5__12 < <cutoffTime>5__1) { <>2__current = DeleteMessage(token, channelId, <messageId>5__10); <>1__state = 2; result = true; break; } goto IL_02da; } <>m__Finally2(); <>s__7 = null; if (<deletedCount>5__5 > 0) { LogVerbose($"Cleaned up {<deletedCount>5__5} old message(s) from status channel"); } <messages>5__4 = null; <>m__Finally1(); <request>5__3 = null; result = false; break; } goto end_IL_0000; } <>m__Finally1(); end_IL_0000:; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__3 != null) { ((IDisposable)<request>5__3).Dispose(); } } private void <>m__Finally2() { <>1__state = -3; if (<>s__7 != null) { <>s__7.Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <DeleteMessage>d__37 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string channelId; public string messageId; private string <url>5__1; private UnityWebRequest <request>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <DeleteMessage>d__37(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <request>5__2 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId; <request>5__2 = UnityWebRequest.Delete(<url>5__1); <>1__state = -3; <request>5__2.SetRequestHeader("Authorization", "Bot " + token); <request>5__2.timeout = 10; <>2__current = <request>5__2.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<request>5__2.responseCode != 204 && <request>5__2.responseCode != 404) { LogVerbose($"Failed to delete message {messageId} ({<request>5__2.responseCode}): {<request>5__2.error}"); } <>m__Finally1(); <request>5__2 = null; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__2 != null) { ((IDisposable)<request>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <FetchNewMessages>d__17 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string channelId; private string <url>5__1; private UnityWebRequest <request>5__2; private List<PendingRelay> <pendingRelay>5__3; private string <newestMessageId>5__4; private bool <parseOk>5__5; private string <responseText>5__6; private JArray <messages>5__7; private JObject <newest>5__8; private int <i>5__9; private Exception <ex>5__10; private List<PendingRelay>.Enumerator <>s__11; private PendingRelay <entry>5__12; private string <colorHex>5__13; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <FetchNewMessages>d__17(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if ((uint)(num - -4) <= 1u || (uint)(num - 1) <= 2u) { try { if (num == -4 || num == 2) { try { } finally { <>m__Finally2(); } } } finally { <>m__Finally1(); } } <url>5__1 = null; <request>5__2 = null; <pendingRelay>5__3 = null; <newestMessageId>5__4 = null; <responseText>5__6 = null; <messages>5__7 = null; <newest>5__8 = null; <ex>5__10 = null; <>s__11 = default(List<PendingRelay>.Enumerator); <entry>5__12 = default(PendingRelay); <colorHex>5__13 = null; <>1__state = -2; } private bool MoveNext() { //IL_03ef: Unknown result type (might be due to invalid IL or missing references) //IL_03f9: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (!string.IsNullOrEmpty(_lastMessageId)) { <url>5__1 = "https://discord.com/api/v10/channels/" + channelId + "/messages?after=" + _lastMessageId + "&limit=10"; } else { <url>5__1 = "https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1"; } <request>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <request>5__2.SetRequestHeader("Authorization", "Bot " + token); <request>5__2.timeout = 10; <>2__current = <request>5__2.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<request>5__2.responseCode == 200) { <pendingRelay>5__3 = new List<PendingRelay>(); <newestMessageId>5__4 = null; <parseOk>5__5 = true; try { DownloadHandler downloadHandler = <request>5__2.downloadHandler; <responseText>5__6 = ((downloadHandler != null) ? downloadHandler.text : null); if (string.IsNullOrEmpty(<responseText>5__6)) { LogVerbose("Bot poll returned empty response body"); <parseOk>5__5 = false; } else { <messages>5__7 = JArray.Parse(<responseText>5__6); if (<messages>5__7 != null && ((JContainer)<messages>5__7).Count > 0) { <i>5__9 = ((JContainer)<messages>5__7).Count - 1; while (<i>5__9 >= 0) { JToken obj = <messages>5__7[<i>5__9]; TryQueueRelay((JObject)(object)((obj is JObject) ? obj : null), <pendingRelay>5__3); <i>5__9--; } ref JObject reference = ref <newest>5__8; JToken obj2 = <messages>5__7[0]; reference = (JObject)(object)((obj2 is JObject) ? obj2 : null); JObject obj3 = <newest>5__8; <newestMessageId>5__4 = ((obj3 != null) ? ((JToken)obj3).Value<string>((object)"id") : null); <newest>5__8 = null; } <messages>5__7 = null; } <responseText>5__6 = null; } catch (Exception ex) { <ex>5__10 = ex; Debug.LogWarning((object)("[Discord] Failed to parse messages: " + <ex>5__10.Message + "\n" + <ex>5__10.StackTrace)); <parseOk>5__5 = false; } if (<parseOk>5__5) { <>s__11 = <pendingRelay>5__3.GetEnumerator(); <>1__state = -4; goto IL_036d; } goto IL_03b0; } if (<request>5__2.responseCode == 429) { LogVerbose("Bot listener rate limited, backing off"); <>2__current = (object)new WaitForSecondsRealtime(5f); <>1__state = 3; return true; } if (<request>5__2.responseCode > 0) { string text = $"[Discord] Bot poll failed ({<request>5__2.responseCode}): "; DownloadHandler downloadHandler2 = <request>5__2.downloadHandler; Debug.LogWarning((object)(text + (((downloadHandler2 != null) ? downloadHandler2.text : null) ?? <request>5__2.error))); } break; case 2: <>1__state = -4; DiscordRoleColorResolver.TryGetUserColorHex(<entry>5__12.AuthorId, out <colorHex>5__13); RelayToGameChat(<entry>5__12.AuthorName, <entry>5__12.Content, <colorHex>5__13); <colorHex>5__13 = null; <entry>5__12 = default(PendingRelay); goto IL_036d; case 3: { <>1__state = -3; break; } IL_03b0: <pendingRelay>5__3 = null; <newestMessageId>5__4 = null; break; IL_036d: if (<>s__11.MoveNext()) { <entry>5__12 = <>s__11.Current; <>2__current = DiscordRoleColorResolver.EnsureUserFetched(<entry>5__12.AuthorId); <>1__state = 2; return true; } <>m__Finally2(); <>s__11 = default(List<PendingRelay>.Enumerator); if (!string.IsNullOrEmpty(<newestMessageId>5__4)) { _lastMessageId = <newestMessageId>5__4; } goto IL_03b0; } <>m__Finally1(); <request>5__2 = null; return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__2 != null) { ((IDisposable)<request>5__2).Dispose(); } } private void <>m__Finally2() { <>1__state = -3; ((IDisposable)<>s__11).Dispose(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PollLogRequestReactions>d__14 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string channelId; private LogRequestContext[] <snapshot>5__1; private string <emojiLiteral>5__2; private string <emojiEncoded>5__3; private int <s>5__4; private LogRequestContext <ctx>5__5; private int <i>5__6; private LogRequestContext <ctx>5__7; private string <url>5__8; private UnityWebRequest <req>5__9; private string <body>5__10; private JArray <users>5__11; private Exception <ex>5__12; private int <u>5__13; private JObject <user>5__14; private string <userId>5__15; private string <pairKey>5__16; private bool <dispatched>5__17; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PollLogRequestReactions>d__14(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 2) { try { } finally { <>m__Finally1(); } } <snapshot>5__1 = null; <emojiLiteral>5__2 = null; <emojiEncoded>5__3 = null; <ctx>5__5 = null; <ctx>5__7 = null; <url>5__8 = null; <req>5__9 = null; <body>5__10 = null; <users>5__11 = null; <ex>5__12 = null; <user>5__14 = null; <userId>5__15 = null; <pairKey>5__16 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (!FiresDiscordIntegration.ClientLogRelay.ClientLogRelay.HasLogRequestHandler) { return false; } <snapshot>5__1 = LogRequestRegistry.Snapshot(); if (<snapshot>5__1 == null || <snapshot>5__1.Length == 0) { return false; } <emojiLiteral>5__2 = DiscordIntegrationConfig.LogRequestReactionEmoji?.Value; if (string.IsNullOrEmpty(<emojiLiteral>5__2)) { <emojiLiteral>5__2 = "\ud83d\udce5"; } try { <emojiEncoded>5__3 = UnityWebRequest.EscapeURL(<emojiLiteral>5__2); } catch { <emojiEncoded>5__3 = <emojiLiteral>5__2; } <s>5__4 = 0; goto IL_019b; case 1: <>1__state = -1; _seededMessageIds.Add(<ctx>5__5.MessageId); <ctx>5__5 = null; goto IL_0189; case 2: { <>1__state = -3; if (<req>5__9.responseCode != 404) { if (<req>5__9.responseCode != 200) { LogVerbose($"Reaction poll {<ctx>5__7.MessageId}: HTTP {<req>5__9.responseCode}"); } else { DownloadHandler downloadHandler = <req>5__9.downloadHandler; <body>5__10 = ((downloadHandler != null) ? downloadHandler.text : null); if (!string.IsNullOrEmpty(<body>5__10)) { <users>5__11 = null; try { <users>5__11 = JArray.Parse(<body>5__10); } catch (Exception ex) { <ex>5__12 = ex; LogVerbose("Reaction poll parse failed: " + <ex>5__12.Message); goto IL_0558; } if (<users>5__11 != null && ((JContainer)<users>5__11).Count != 0) { <u>5__13 = 0; while (<u>5__13 < ((JContainer)<users>5__11).Count) { ref JObject reference = ref <user>5__14; JToken obj = <users>5__11[<u>5__13]; reference = (JObject)(object)((obj is JObject) ? obj : null); JObject obj2 = <user>5__14; <userId>5__15 = ((obj2 != null) ? ((JToken)obj2).Value<string>((object)"id") : null); if (!string.IsNullOrEmpty(<userId>5__15) && (string.IsNullOrEmpty(_selfBotUserId) || !(<userId>5__15 == _selfBotUserId))) { <pairKey>5__16 = <ctx>5__7.MessageId + "|" + <userId>5__15; if (_processedReactionPairs.Add(<pairKey>5__16)) { <dispatched>5__17 = FiresDiscordIntegration.ClientLogRelay.ClientLogRelay.TryDispatchLogRequest(<ctx>5__7.MessageId, <userId>5__15, <emojiLiteral>5__2); if (!<dispatched>5__17) { LogVerbose("Reaction from " + <userId>5__15 + " on " + <ctx>5__7.MessageId + " not dispatched"); } <user>5__14 = null; <userId>5__15 = null; <pairKey>5__16 = null; } } <u>5__13++; } <body>5__10 = null; <users>5__11 = null; <>m__Finally1(); <req>5__9 = null; <ctx>5__7 = null; <url>5__8 = null; goto IL_0577; } } } } goto IL_0558; } IL_019b: if (<s>5__4 < <snapshot>5__1.Length) { <ctx>5__5 = <snapshot>5__1[<s>5__4]; if (<ctx>5__5 == null || string.IsNullOrEmpty(<ctx>5__5.MessageId) || _seededMessageIds.Contains(<ctx>5__5.MessageId)) { goto IL_0189; } <>2__current = SeedReaction(token, channelId, <ctx>5__5.MessageId, <emojiEncoded>5__3); <>1__state = 1; return true; } <i>5__6 = 0; goto IL_0589; IL_0577: <i>5__6++; goto IL_0589; IL_0189: <s>5__4++; goto IL_019b; IL_0558: <>m__Finally1(); goto IL_0577; IL_0589: if (<i>5__6 < <snapshot>5__1.Length) { <ctx>5__7 = <snapshot>5__1[<i>5__6]; if (<ctx>5__7 == null || string.IsNullOrEmpty(<ctx>5__7.MessageId)) { goto IL_0577; } <url>5__8 = "https://discord.com/api/v10/channels/" + channelId + "/messages/" + <ctx>5__7.MessageId + "/reactions/" + <emojiEncoded>5__3 + "?limit=100"; <req>5__9 = UnityWebRequest.Get(<url>5__8); <>1__state = -3; <req>5__9.SetRequestHeader("Authorization", "Bot " + token); <req>5__9.timeout = 10; <>2__current = <req>5__9.SendWebRequest(); <>1__state = 2; return true; } return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__9 != null) { ((IDisposable)<req>5__9).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PollLoop>d__11 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string channelId; private float <interval>5__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PollLoop>d__11(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = SeedLastMessageId(token, channelId); <>1__state = 1; return true; case 1: <>1__state = -1; <interval>5__1 = Mathf.Clamp(DiscordIntegrationConfig.BotPollIntervalSeconds?.Value ?? 3f, 1f, 30f); goto IL_0123; case 2: <>1__state = -1; if (!_running) { break; } if (!DiscordIntegrationCore.IsActive()) { goto IL_0123; } <>2__current = FetchNewMessages(token, channelId); <>1__state = 3; return true; case 3: <>1__state = -1; <>2__current = PollLogRequestReactions(token, channelId); <>1__state = 4; return true; case 4: { <>1__state = -1; goto IL_0123; } IL_0123: if (_running) { <>2__current = (object)new WaitForSecondsRealtime(<interval>5__1); <>1__state = 2; return true; } break; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <ResolveSelfUserId>d__10 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; private string <url>5__1; private UnityWebRequest <request>5__2; private JObject <json>5__3; private Exception <ex>5__4; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ResolveSelfUserId>d__10(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <request>5__2 = null; <json>5__3 = null; <ex>5__4 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/users/@me"; <request>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <request>5__2.SetRequestHeader("Authorization", "Bot " + token); <request>5__2.timeout = 10; <>2__current = <request>5__2.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<request>5__2.responseCode == 200) { try { <json>5__3 = JObject.Parse(<request>5__2.downloadHandler.text); _selfBotUserId = ((object)<json>5__3["id"])?.ToString(); _selfIdResolved = true; LogVerbose("Bot user ID resolved: " + _selfBotUserId); <json>5__3 = null; } catch (Exception ex) { <ex>5__4 = ex; Debug.LogWarning((object)("[Discord] Failed to parse bot user info: " + <ex>5__4.Message)); _selfIdResolved = true; } } else { Debug.LogWarning((object)$"[Discord] Failed to resolve bot user ID ({<request>5__2.responseCode}): {<request>5__2.error}"); _selfIdResolved = true; } <>m__Finally1(); <request>5__2 = null; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__2 != null) { ((IDisposable)<request>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <SeedLastMessageId>d__16 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string channelId; private string <url>5__1; private UnityWebRequest <request>5__2; private string <responseText>5__3; private JArray <messages>5__4; private JObject <first>5__5; private Exception <ex>5__6; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SeedLastMessageId>d__16(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <request>5__2 = null; <responseText>5__3 = null; <messages>5__4 = null; <first>5__5 = null; <ex>5__6 = null; <>1__state = -2; } private bool MoveNext() { bool result; try { switch (<>1__state) { default: result = false; break; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1"; <request>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <request>5__2.SetRequestHeader("Authorization", "Bot " + token); <request>5__2.timeout = 10; <>2__current = <request>5__2.SendWebRequest(); <>1__state = 1; result = true; break; case 1: { <>1__state = -3; if (<request>5__2.responseCode == 200) { try { DownloadHandler downloadHandler = <request>5__2.downloadHandler; <responseText>5__3 = ((downloadHandler != null) ? downloadHandler.text : null); if (!string.IsNullOrEmpty(<responseText>5__3)) { <messages>5__4 = JArray.Parse(<responseText>5__3); if (<messages>5__4 != null && ((JContainer)<messages>5__4).Count > 0) { ref JObject reference = ref <first>5__5; JToken obj = <messages>5__4[0]; reference = (JObject)(object)((obj is JObject) ? obj : null); JObject obj2 = <first>5__5; _lastMessageId = ((obj2 != null) ? ((JToken)obj2).Value<string>((object)"id") : null); LogVerbose("Seeded last message ID: " + _lastMessageId); <first>5__5 = null; } <responseText>5__3 = null; <messages>5__4 = null; goto IL_021d; } result = false; } catch (Exception ex) { <ex>5__6 = ex; Debug.LogWarning((object)("[Discord] Failed to seed message ID: " + <ex>5__6.Message + "\n" + <ex>5__6.StackTrace)); goto IL_021d; } <>m__Finally1(); break; } string text = $"[Discord] Failed to seed messages ({<request>5__2.responseCode}): "; DownloadHandler downloadHandler2 = <request>5__2.downloadHandler; Debug.LogWarning((object)(text + (((downloadHandler2 != null) ? downloadHandler2.text : null) ?? <request>5__2.error))); goto IL_021d; } IL_021d: <>m__Finally1(); <request>5__2 = null; result = false; break; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__2 != null) { ((IDisposable)<request>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <SeedReaction>d__15 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string channelId; public string messageId; public string emojiEncoded; private string <url>5__1; private UnityWebRequest <req>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SeedReaction>d__15(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <req>5__2 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId + "/reactions/" + emojiEncoded + "/@me"; <req>5__2 = UnityWebRequest.Put(<url>5__1, Array.Empty<byte>()); <>1__state = -3; <req>5__2.method = "PUT"; <req>5__2.SetRequestHeader("Authorization", "Bot " + token); <req>5__2.SetRequestHeader("Content-Length", "0"); <req>5__2.timeout = 10; <>2__current = <req>5__2.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<req>5__2.responseCode == 204) { LogVerbose("Seeded reaction on message " + messageId); } else if (<req>5__2.responseCode == 403) { LogVerbose("Cannot seed reaction on " + messageId + ": missing ADD_REACTIONS permission in this channel (users can still add their own reaction)"); } else { string text = $"Seed reaction PUT {messageId} ? HTTP {<req>5__2.responseCode}: "; DownloadHandler downloadHandler = <req>5__2.downloadHandler; LogVerbose(text + (((downloadHandler != null) ? downloadHandler.text : null) ?? <req>5__2.error)); } <>m__Finally1(); <req>5__2 = null; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__2 != null) { ((IDisposable)<req>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string DiscordApiBase = "https://discord.com/api/v10"; private static MonoBehaviour _host; private static Coroutine _pollCoroutine; private static string _lastMessageId; private static bool _running; private static string _selfBotUserId; private static bool _selfIdResolved; private static string _webhookId; private static readonly HashSet<string> _processedReactionPairs = new HashSet<string>(StringComparer.Ordinal); private static readonly HashSet<string> _seededMessageIds = new HashSet<string>(StringComparer.Ordinal); private const int MaxChatChunkLength = 400; internal const char DiscordUserInfoNameDelimiter = '|'; internal const string DiscordUserInfoNamePrefix = "[Discord]"; private static string _cachedServerSteamId; public static void Start() { if (_running || !DiscordIntegrationCore.IsActive()) { return; } ConfigEntry<bool> enableBotListener = DiscordIntegrationConfig.EnableBotListener; if (enableBotListener == null || !enableBotListener.Value) { return; } string token = BotTokenFile.Token; string text = DiscordIntegrationConfig.ChatChannelId?.Value; if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(text)) { Debug.LogWarning((object)"[Discord] Bot listener enabled but BotToken or ChatChannelId is empty"); return; } _webhookId = ExtractWebhookId(DiscordIntegrationConfig.ChatWebhookUrl?.Value); EnsureHost(); if (!((Object)(object)_host == (Object)null)) { _running = true; _selfIdResolved = false; _selfBotUserId = null; _host.StartCoroutine(ResolveSelfUserId(token)); DiscordMentionResolver.FetchRoles(); _pollCoroutine = _host.StartCoroutine(PollLoop(token, text)); Debug.Log((object)("[Discord] Bot listener started - polling channel " + text)); } } public static void Stop() { _running = false; if (_pollCoroutine != null && (Object)(object)_host != (Object)null) { _host.StopCoroutine(_pollCoroutine); _pollCoroutine = null; } _lastMessageId = null; _selfIdResolved = false; _selfBotUserId = null; _processedReactionPairs.Clear(); _seededMessageIds.Clear(); DiscordMentionResolver.ClearCache(); DiscordRoleColorResolver.ClearCache(); Debug.Log((object)"[Discord] Bot listener stopped"); } [IteratorStateMachine(typeof(<ResolveSelfUserId>d__10))] private static IEnumerator ResolveSelfUserId(string token) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ResolveSelfUserId>d__10(0) { token = token }; } [IteratorStateMachine(typeof(<PollLoop>d__11))] private static IEnumerator PollLoop(string token, string channelId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PollLoop>d__11(0) { token = token, channelId = channelId }; } [IteratorStateMachine(typeof(<PollLogRequestReactions>d__14))] private static IEnumerator PollLogRequestReactions(string token, string channelId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PollLogRequestReactions>d__14(0) { token = token, channelId = channelId }; } [IteratorStateMachine(typeof(<SeedReaction>d__15))] private static IEnumerator SeedReaction(string token, string channelId, string messageId, string emojiEncoded) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SeedReaction>d__15(0) { token = token, channelId = channelId, messageId = messageId, emojiEncoded = emojiEncoded }; } [IteratorStateMachine(typeof(<SeedLastMessageId>d__16))] private static IEnumerator SeedLastMessageId(string token, string channelId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SeedLastMessageId>d__16(0) { token = token, channelId = channelId }; } [IteratorStateMachine(typeof(<FetchNewMessages>d__17))] private static IEnumerator FetchNewMessages(string token, string channelId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <FetchNewMessages>d__17(0) { token = token, channelId = channelId }; } private static string AppendEmbeddedMediaUrls(string content, JArray embeds) { if (embeds == null || ((JContainer)embeds).Count == 0) { return content; } if (ContentHasResolvableShareUrl(content)) { return content; } foreach (JToken embed in embeds) { JObject val = (JObject)(object)((embed is JObject) ? embed : null); if (val != null) { string text = ExtractPlayableImageUrl(val); if (!string.IsNullOrEmpty(text) && LooksLikeImageUrl(text)) { content = (string.IsNullOrEmpty(content) ? text : (content + " " + text)); } } } return content; } private static bool ContentHasResolvableShareUrl(string content) { if (string.IsNullOrEmpty(content)) { return false; } return content.IndexOf("tenor.com/view/", StringComparison.OrdinalIgnoreCase) >= 0 || content.IndexOf("giphy.com/gifs/", StringComparison.OrdinalIgnoreCase) >= 0 || content.IndexOf("klipy.com/gifs/", StringComparison.OrdinalIgnoreCase) >= 0; } private static string ExtractPlayableImageUrl(JObject embed) { JToken obj = embed["image"]; string text = ReadUrl((JObject)(object)((obj is JObject) ? obj : null), preferProxy: true); if (!string.IsNullOrEmpty(text)) { return text; } JToken obj2 = embed["thumbnail"]; return ReadUrl((JObject)(object)((obj2 is JObject) ? obj2 : null), preferProxy: true); } private static string ReadUrl(JObject node, bool preferProxy) { if (node == null) { return null; } if (preferProxy) { string text = ((JToken)node).Value<string>((object)"proxy_url"); if (!string.IsNullOrEmpty(text)) { return text; } } return ((JToken)node).Value<string>((object)"url"); } private static bool LooksLikeImageUrl(string url) { if (string.IsNullOrEmpty(url)) { return false; } int num = url.IndexOf('?'); string text = ((num >= 0) ? url.Substring(0, num) : url); return text.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) || text.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || text.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || text.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || text.EndsWith(".webp", StringComparison.OrdinalIgnoreCase); } private static void TryQueueRelay(JObject msg, List<PendingRelay> pending) { if (msg == null) { return; } string text = ((JToken)msg).Value<string>((object)"content"); string value = ((JToken)msg).Value<string>((object)"webhook_id"); JToken obj = msg["author"]; JObject val = (JObject)(object)((obj is JObject) ? obj : null); string text2 = ((val != null) ? ((JToken)val).Value<string>((object)"id") : null); bool flag = val != null && val["bot"] != null && ((JToken)val).Value<bool>((object)"bot"); JToken obj2 = msg["attachments"]; JArray val2 = (JArray)(object)((obj2 is JArray) ? obj2 : null); bool flag2 = val2 != null && ((JContainer)val2).Count > 0; if ((string.IsNullOrEmpty(text) && !flag2) || (!string.IsNullOrEmpty(_selfBotUserId) && text2 == _selfBotUserId) || !string.IsNullOrEmpty(value) || flag) { return; } string text3 = ((val != null) ? ((JToken)val).Value<string>((object)"global_name") : null); if (string.IsNullOrEmpty(text3)) { text3 = ((val != null) ? ((JToken)val).Value<string>((object)"username") : null) ?? "Unknown"; text3 = StripDiscriminator(text3); } if (flag2) { foreach (JToken item in val2) { string text4 = ((item != null) ? item.Value<string>((object)"url") : null); if (!string.IsNullOrEmpty(text4)) { text = (string.IsNullOrEmpty(text) ? text4 : (text + " " + text4)); } } } string content = text; JToken obj3 = msg["embeds"]; text = AppendEmbeddedMediaUrls(content, (JArray)(object)((obj3 is JArray) ? obj3 : null)); if (!string.IsNullOrEmpty(text)) { text = DiscordEmojiConverter.ConvertEmoji(text); pending.Add(new PendingRelay { AuthorId = text2, AuthorName = text3, Content = text }); } } private static void RelayToGameChat(string discordUsername, string message, string nameColorHex) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Expected O, but got Unknown //IL_0127: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZRoutedRpc.instance == null) { return; } string serverSteamId = GetServerSteamId(); if (string.IsNullOrEmpty(serverSteamId)) { LogVerbose("Could not resolve server Steam ID for chat relay"); return; } Platform val = default(Platform); ((Platform)(ref val))..ctor("Steam"); PlatformUserID userId = default(PlatformUserID); ((PlatformUserID)(ref userId))..ctor(val, serverSteamId); string name = (string.IsNullOrEmpty(nameColorHex) ? "[Discord]" : ("[Discord]|" + nameColorHex)); UserInfo val2 = new UserInfo { Name = name, UserId = userId }; string text = "[Discord] " + discordUsername + ": "; List<string> list = SplitChatMessage(message, 400 - text.Length); foreach (string item in list) { string text2 = text + item; LogVerbose("Relaying to game chat: " + text2); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[5] { (object)new Vector3(0f, 1000f, 0f), 2, val2, text2, ((object)(PlatformUserID)(ref userId)).ToString() }); } } private static List<string> SplitChatMessage(string message, int maxLen) { if (maxLen < 50) { maxLen = 50; } List<string> list = new List<string>(); if (string.IsNullOrEmpty(message)) { list.Add(string.Empty); return list; } if (message.Length <= maxLen) { list.Add(message); return list; } int num = 0; while (num < message.Length) { int num2 = message.Length - num; if (num2 <= maxLen) { list.Add(message.Substring(num)); break; } int num3 = num + maxLen; int num4 = -1; int num5 = message.LastIndexOf('\n', num3 - 1, maxLen); if (num5 > num) { num4 = num5 + 1; } if (num4 < 0) { int num6 = message.LastIndexOf(' ', num3 - 1, maxLen); if (num6 > num) { num4 = num6 + 1; } } if (num4 <= num) { num4 = num3; } list.Add(message.Substring(num, num4 - num)); num = num4; } return list; } private static string GetServerSteamId() { if (_cachedServerSteamId != null) { return _cachedServerSteamId; } try { FieldInfo field = typeof(ZNet).GetField("m_serverSteamID", BindingFlags.Static | BindingFlags.NonPublic); if (field != null) { ulong num = (ulong)field.GetValue(null); if (num != 0) { _cachedServerSteamId = num.ToString(); LogVerbose("Resolved server Steam ID: " + _cachedServerSteamId); return _cachedServerSteamId; } } } catch (Exception ex) { Debug.LogWarning((object)("[Discord] Failed to get server Steam ID via reflection: " + ex.Message)); } try { FieldInfo field2 = typeof(ZNet).GetField("m_hostSocket", BindingFlags.Instance | BindingFlags.NonPublic); if (field2 != null && (Object)(object)ZNet.instance != (Object)null) { object? value = field2.GetValue(ZNet.instance); ISocket val = (ISocket)((value is ISocket) ? value : null); if (val != null) { string hostName = val.GetHostName(); if (!string.IsNullOrEmpty(hostName)) { _cachedServerSteamId = hostName; LogVerbose("Resolved server Steam ID from host socket: " + _cachedServerSteamId); return _cachedServerSteamId; } } } } catch (Exception ex2) { Debug.LogWarning((object)("[Discord] Failed to get server host socket: " + ex2.Message)); } return null; } private static string StripDiscriminator(string username) { if (string.IsNullOrEmpty(username)) { return username; } int num = username.LastIndexOf('#'); int result; if (num > 0 && num < username.Length - 1) { string text = username.Substring(num + 1); if (text.Length <= 4 && int.TryParse(text, out result)) { return username.Substring(0, num); } } int num2 = username.LastIndexOf('_'); if (num2 > 0 && num2 < username.Length - 1) { string text2 = username.Substring(num2 + 1); if (text2.Length <= 4 && int.TryParse(text2, out result)) { return username.Substring(0, num2); } } return username; } private static string ExtractWebhookId(string webhookUrl) { if (string.IsNullOrEmpty(webhookUrl)) { return null; } try { Uri uri = new Uri(webhookUrl); string[] array = uri.AbsolutePath.Split(new char[1] { '/' }); for (int i = 0; i < array.Length - 1; i++) { if (array[i] == "webhooks" && i + 1 < array.Length) { return array[i + 1]; } } } catch { } return null; } private static void EnsureHost() { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown if ((Object)(object)_host != (Object)null) { return; } GameObject val = GameObject.Find("DiscordWebhookHost"); if ((Object)(object)val != (Object)null) { _host = val.GetComponent<MonoBehaviour>(); if ((Object)(object)_host != (Object)null) { return; } } GameObject val2 = new GameObject("DiscordBotListenerHost"); Object.DontDestroyOnLoad((Object)(object)val2); ((Object)val2).hideFlags = (HideFlags)61; _host = (MonoBehaviour)(object)val2.AddComponent<CoroutineHost>(); } public static void TriggerMessageCleanup(string channelId, TimeSpan maxAge) { if (string.IsNullOrEmpty(channelId)) { return; } string token = BotTokenFile.Token; if (!string.IsNullOrEmpty(token)) { EnsureHost(); if (!((Object)(object)_host == (Object)null)) { _host.StartCoroutine(CleanupOldMessages(token, channelId, maxAge)); } } } [IteratorStateMachine(typeof(<CleanupOldMessages>d__36))] private static IEnumerator CleanupOldMessages(string token, string channelId, TimeSpan maxAge) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <CleanupOldMessages>d__36(0) { token = token, channelId = channelId, maxAge = maxAge }; } [IteratorStateMachine(typeof(<DeleteMessage>d__37))] private static IEnumerator DeleteMessage(string token, string channelId, string messageId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <DeleteMessage>d__37(0) { token = token, channelId = channelId, messageId = messageId }; } private static void LogVerbose(string msg) { if (DiscordIntegrationConfig.VerboseLogging != null && DiscordIntegrationConfig.VerboseLogging.Value) { Debug.Log((object)("[Discord] " + msg)); } } } public static class DiscordCrashDetector { private class CoroutineHost : MonoBehaviour { } [CompilerGenerated] private sealed class <HeartbeatLoop>d__14 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private Dictionary<string, object> <embed>5__1; private string <json>5__2; private string <url>5__3; private UnityWebRequest <req>5__4; private byte[] <body>5__5; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <HeartbeatLoop>d__14(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 2) { try { } finally { <>m__Finally1(); } } <embed>5__1 = null; <json>5__2 = null; <url>5__3 = null; <req>5__4 = null; <body>5__5 = null; <>1__state = -2; } private bool MoveNext() { //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Expected O, but got Unknown //IL_0123: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Expected O, but got Unknown //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; break; case 1: <>1__state = -1; if (_cleanShutdown) { return false; } try { WriteSentinel("running"); } catch { } if (string.IsNullOrEmpty(_statusMessageId)) { break; } <embed>5__1 = BuildOnlineEmbed(); <json>5__2 = BuildEmbedPayload(<embed>5__1); <url>5__3 = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + _statusMessageId; <req>5__4 = new UnityWebRequest(<url>5__3, "PATCH"); <>1__state = -3; <body>5__5 = Encoding.UTF8.GetBytes(<json>5__2); <req>5__4.uploadHandler = (UploadHandler)new UploadHandlerRaw(<body>5__5); <req>5__4.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); <req>5__4.SetRequestHeader("Authorization", "Bot " + _botToken); <req>5__4.SetRequestHeader("Content-Type", "application/json"); <req>5__4.timeout = 10; <>2__current = <req>5__4.SendWebRequest(); <>1__state = 2; return true; case 2: <>1__state = -3; if (<req>5__4.responseCode == 429) { LogVerbose("Status message edit rate limited"); } else if (<req>5__4.responseCode != 200) { object arg = <req>5__4.responseCode; DownloadHandler downloadHandler = <req>5__4.downloadHandler; LogVerbose($"Status message edit failed ({arg}): {((downloadHandler != null) ? downloadHandler.text : null) ?? <req>5__4.error}"); } <body>5__5 = null; <>m__Finally1(); <req>5__4 = null; <embed>5__1 = null; <json>5__2 = null; <url>5__3 = null; break; } if (!_cleanShutdown) { <>2__current = (object)new WaitForSecondsRealtime(60f); <>1__state = 1; return true; } return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__4 != null) { ((IDisposable)<req>5__4).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PostStatusMessage>d__13 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private Dictionary<string, object> <embed>5__1; private string <json>5__2; private string <url>5__3; private UnityWebRequest <req>5__4; private byte[] <body>5__5; private string <responseText>5__6; private JObject <resp>5__7; private Exception <ex>5__8; private string <errBody>5__9; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PostStatusMessage>d__13(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <embed>5__1 = null; <json>5__2 = null; <url>5__3 = null; <req>5__4 = null; <body>5__5 = null; <responseText>5__6 = null; <resp>5__7 = null; <ex>5__8 = null; <errBody>5__9 = null; <>1__state = -2; } private bool MoveNext() { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Expected O, but got Unknown //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Expected O, but got Unknown //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <embed>5__1 = BuildOnlineEmbed(); <json>5__2 = BuildEmbedPayload(<embed>5__1); <url>5__3 = "https://discord.com/api/v10/channels/" + _channelId + "/messages"; <req>5__4 = new UnityWebRequest(<url>5__3, "POST"); <>1__state = -3; <body>5__5 = Encoding.UTF8.GetBytes(<json>5__2); <req>5__4.uploadHandler = (UploadHandler)new UploadHandlerRaw(<body>5__5); <req>5__4.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); <req>5__4.SetRequestHeader("Authorization", "Bot " + _botToken); <req>5__4.SetRequestHeader("Content-Type", "application/json"); <req>5__4.timeout = 15; <>2__current = <req>5__4.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<req>5__4.responseCode == 200 || <req>5__4.responseCode == 201) { try { DownloadHandler downloadHandler = <req>5__4.downloadHandler; <responseText>5__6 = ((downloadHandler != null) ? downloadHandler.text : null); if (!string.IsNullOrEmpty(<responseText>5__6)) { <resp>5__7 = JObject.Parse(<responseText>5__6); _statusMessageId = ((JToken)<resp>5__7).Value<string>((object)"id"); Debug.Log((object)("[Discord] CrashDetector: status message posted to channel " + _channelId + " (msg ID: " + _statusMessageId + ")")); <resp>5__7 = null; } <responseText>5__6 = null; } catch (Exception ex) { <ex>5__8 = ex; Debug.LogWarning((object)("[Discord] CrashDetector: posted but failed to parse message ID: " + <ex>5__8.Message)); } } else { DownloadHandler downloadHandler2 = <req>5__4.downloadHandler; <errBody>5__9 = ((downloadHandler2 != null) ? downloadHandler2.text : null) ?? <req>5__4.error; Debug.LogWarning((object)$"[Discord] CrashDetector: failed to post status message ({<req>5__4.responseCode}): {<errBody>5__9}"); if (<req>5__4.responseCode == 403) { Debug.LogWarning((object)("[Discord] CrashDetector: 403 Forbidden \ufffd the bot does not have Send Messages permission in channel " + _channelId + ". Add the bot to that channel in your Discord server settings ? Channels ? Permissions.")); } else if (<req>5__4.responseCode == 404) { Debug.LogWarning((object)("[Discord] CrashDetector: 404 \ufffd channel " + _channelId + " not found. Check that StatusChannelId (or ChatChannelId) is correct.")); } <errBody>5__9 = null; } <body>5__5 = null; <>m__Finally1(); <req>5__4 = null; _heartbeatCoroutine = _host.StartCoroutine(HeartbeatLoop()); return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__4 != null) { ((IDisposable)<req>5__4).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string DiscordApiBase = "https://discord.com/api/v10"; private const float HeartbeatIntervalSeconds = 60f; private const string SentinelFileName = "server_heartbeat.json"; private static string _sentinelPath; private static string _statusMessageId; private static string _botToken; private static string _channelId; private static Coroutine _heartbeatCoroutine; private static MonoBehaviour _host; private static DateTime _serverStartUtc; private static bool _cleanShutdown; public static void OnServerStart() { if (DiscordIntegrationCore.IsActive()) { InitSentinel(); SpawnCrashNotifier(); } } public static void OnServerStop() { _cleanShutdown = true; try { WriteSentinel("stopped"); } catch { } if (_heartbeatCoroutine != null && (Object)(object)_host != (Object)null) { _host.StopCoroutine(_heartbeatCoroutine); _heartbeatCoroutine = null; } if (!string.IsNullOrEmpty(_statusMessageId) && !string.IsNullOrEmpty(_botToken) && !string.IsNullOrEmpty(_channelId)) { try { EditStatusMessageSync(_statusMessageId, BuildOfflineEmbed()); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] CrashDetector: failed to edit status on shutdown: " + ex.Message)); } } } [IteratorStateMachine(typeof(<PostStatusMessage>d__13))] private static IEnumerator PostStatusMessage() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PostStatusMessage>d__13(0); } [IteratorStateMachine(typeof(<HeartbeatLoop>d__14))] private static IEnumerator HeartbeatLoop() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <HeartbeatLoop>d__14(0); } private static Dictionary<string, object> BuildOnlineEmbed() { string serverName = GetServerName(); int num = 0; try { ZNet instance = ZNet.instance; num = ((instance == null) ? null : instance.GetPeers()?.Count).GetValueOrDefault(); } catch { } TimeSpan timeSpan = DateTime.UtcNow - _serverStartUtc; string value = ((timeSpan.TotalHours >= 1.0) ? $"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m" : $"{(int)timeSpan.TotalMinutes}m"); return new Dictionary<string, object> { { "title", "\ud83d\udfe2 Server Online" }, { "description", "**" + serverName + "** is running." }, { "color", 5763719 }, { "fields", new List<object> { new Dictionary<string, object> { { "name", "Players" }, { "value", num.ToString() }, { "inline", true } }, new Dictionary<string, object> { { "name", "Uptime" }, { "value", value }, { "inline", true } }, new Dictionary<string, object> { { "name", "Last Heartbeat" }, { "value", DateTime.UtcNow.ToString("HH:mm:ss") + " UTC" }, { "inline", true } } } }, { "footer", new Dictionary<string, object> { { "text", "Updates every 60s · " + serverName + " CrashDetector" } } } }; } private static Dictionary<string, object> BuildOfflineEmbed() { string serverName = GetServerName(); TimeSpan timeSpan = DateTime.UtcNow - _serverStartUtc; string value = ((timeSpan.TotalHours >= 1.0) ? $"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m" : $"{(int)timeSpan.TotalMinutes}m"); return new Dictionary<string, object> { { "title", "\ud83d\udd34 Server Offline" }, { "description", "**" + serverName + "** shut down cleanly." }, { "color", 15548997 }, { "fields", new List<object> { new Dictionary<string, object> { { "name", "Session Duration" }, { "value", value }, { "inline", true } }, new Dictionary<string, object> { { "name", "Stopped At" }, { "value", DateTime.UtcNow.ToString("HH:mm:ss") + " UTC" }, { "inline", true } } } }, { "footer", new Dictionary<string, object> { { "text", serverName + " CrashDetector" } } } }; } private static string BuildEmbedPayload(Dictionary<string, object> embed) { Dictionary<string, object> dictionary = new Dictionary<string, object> { { "embeds", new List<object> { embed } } }; return JsonConvert.SerializeObject((object)dictionary); } private static void EditStatusMessageSync(string messageId, Dictionary<string, object> embed) { string data = BuildEmbedPayload(embed); string address = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + messageId; try { using WebClient webClient = new WebClient(); webClient.Headers.Add("Authorization", "Bot " + _botToken); webClient.Headers.Add("Content-Type", "application/json"); webClient.UploadString(address, "PATCH", data); } catch { } } private static void InitSentinel() { try { string text = Path.Combine(Paths.ConfigPath, "FiresRPGmaker"); Directory.CreateDirectory(text); _sentinelPath = Path.Combine(text, "server_heartbeat.json"); WriteSentinel("running"); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] CrashDetector sentinel write failed: " + ex.Message)); } } private static void WriteSentinel(string status) { if (!string.IsNullOrEmpty(_sentinelPath)) { int id = Process.GetCurrentProcess().Id; string contents = $"{{\"status\":\"{status}\",\"heartbeat\":\"{DateTime.UtcNow:O}\",\"pid\":{id}}}"; File.WriteAllText(_sentinelPath, contents); } } private static void SpawnCrashNotifier() { try { string text = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); if (DiscordWebhookService.IsValidWebhookUrl(text) && !string.IsNullOrEmpty(_sentinelPath)) { int id = Process.GetCurrentProcess().Id; string serverName = GetServerName(); string text2 = _sentinelPath.Replace("'", "''"); string text3 = text.Replace("'", "''"); string text4 = serverName.Replace("'", "''").Replace("\"", "\\\""); string text5 = (DiscordIntegrationConfig.WebhookDisplayName?.Value ?? "Valheim Server").Replace("'", "''"); string value = "try{ (Get-Process -Id " + id + " -EA SilentlyContinue).WaitForExit() }catch{}\nStart-Sleep 3\n$s=Get-Content '" + text2 + "' -Raw -EA SilentlyContinue\nif($s -notmatch '\"running\"'){exit}\n$t=(Get-Date).ToUniversalTime().ToString('HH:mm:ss')\n$emoji=[char]::ConvertFromUtf32(0x1F4A5)\n$b=@{username='" + text5 + "';embeds=@(@{title=\"\"$emoji Server Crash Detected\"\";description=\"\"**" + text4 + "** crashed or was killed.\"\";color=15105570;fields=@(@{name='Detected At';value=\"\"$t UTC\"\";inline=$true});footer=@{text='" + text4 + " CrashDetector'}})}|ConvertTo-Json -Depth 5 -Compress\ntry{(New-Object Net.WebClient).UploadString('" + text3 + "','POST',$b)|Out-Null}catch{}\ntry{Set-Content '" + text2 + "' '{\"status\":\"crashed\"}' -EA SilentlyContinue}catch{}"; ProcessStartInfo startInfo = new ProcessStartInfo { FileName = "powershell.exe", Arguments = "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command -", UseShellExecute = false, RedirectStandardInput = true, CreateNoWindow = true }; Process process = Process.Start(startInfo); if (process != null) { process.StandardInput.Write(value); process.StandardInput.Close(); Debug.Log((object)$"[Discord] CrashDetector: background notifier spawned (watchdog PID {process.Id}, monitoring server PID {id})"); } } } catch (Exception ex) { Debug.LogWarning((object)("[Discord] CrashDetector: failed to spawn crash notifier: " + ex.Message)); } } private static string GetServerName() { return ServerNameResolver.GetOperatorServerName(); } private static void EnsureHost() { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Expected O, but got Unknown if ((Object)(object)_host != (Object)null) { return; } GameObject val = GameObject.Find("DiscordBotListenerHost"); if ((Object)(object)val != (Object)null) { _host = val.GetComponent<MonoBehaviour>(); if ((Object)(object)_host != (Object)null) { return; } } val = GameObject.Find("DiscordWebhookHost"); if ((Object)(object)val != (Object)null) { _host = val.GetComponent<MonoBehaviour>(); if ((Object)(object)_host != (Object)null) { return; } } GameObject val2 = new GameObject("DiscordCrashDetectorHost"); Object.DontDestroyOnLoad((Object)(object)val2); ((Object)val2).hideFlags = (HideFlags)61; _host = (MonoBehaviour)(object)val2.AddComponent<CoroutineHost>(); } private static void LogVerbose(string msg) { ConfigEntry<bool> verboseLogging = DiscordIntegrationConfig.VerboseLogging; if (verboseLogging != null && verboseLogging.Value) { Debug.Log((object)("[Discord] CrashDetector: " + msg)); } } } public static class DiscordEmojiConverter { private static readonly Regex CustomEmojiRegex = new Regex("<a?:(\\w+):\\d+>", RegexOptions.Compiled); private static readonly Regex ShortcodeRegex = new Regex(":([a-zA-Z0-9_+-]+):", RegexOptions.Compiled); private static readonly Dictionary<string, string> ShortcodeMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "grinning", "\ud83d\ude00" }, { "smile", "\ud83d\ude04" }, { "smiley", "\ud83d\ude03" }, { "grin", "\ud83d\ude01" }, { "laughing", "\ud83d\ude06" }, { "satisfied", "\ud83d\ude06" }, { "sweat_smile", "\ud83d\ude05" }, { "rofl", "\ud83e\udd23" }, { "joy", "\ud83d\ude02" }, { "slightly_smiling_face", "\ud83d\ude42" }, { "wink", "\ud83d\ude09" }, { "blush", "\ud83d\ude0a" }, { "innocent", "\ud83d\ude07" }, { "heart_eyes", "\ud83d\ude0d" }, { "kissing_heart", "\ud83d\ude18" }, { "stuck_out_tongue_winking_eye", "\ud83d\ude1c" }, { "stuck_out_tongue", "\ud83d\ude1b" }, { "thinking", "\ud83e\udd14" }, { "thinking_face", "\ud83e\udd14" }, { "shushing_face", "\ud83e\udd2b" }, { "sunglasses", "\ud83d\ude0e" }, { "nerd", "\ud83e\udd13" }, { "nerd_face", "\ud83e\udd13" }, { "clown", "\ud83e\udd21" }, { "clown_face", "\ud83e\udd21" }, { "sob", "\ud83d\ude2d" }, { "cry", "\ud83d\ude22" }, { "angry", "\ud83d\ude20" }, { "rage", "\ud83d\ude21" }, { "scream", "\ud83d\ude31" }, { "skull", "\ud83d\udc80" }, { "skull_crossbones", "☠\ufe0f" }, { "ghost", "\ud83d\udc7b" }, { "alien", "\ud83d\udc7d" }, { "robot", "\ud83e\udd16" }, { "poop", "\ud83d\udca9" }, { "hankey", "\ud83d\udca9" }, { "wave", "\ud83d\udc4b" }, { "raised_hands", "\ud83d\ude4c" }, { "clap", "\ud83d\udc4f" }, { "thumbsup", "\ud83d\udc4d" }, { "+1", "\ud83d\udc4d" }, { "thumbup", "\ud83d\udc4d" }, { "thumbsdown", "\ud83d\udc4e" }, { "-1", "\ud83d\udc4e" }, { "punch", "\ud83d\udc4a" }, { "fist", "✊" }, { "muscle", "\ud83d\udcaa" }, { "pray", "\ud83d\ude4f" }, { "point_up", "☝\ufe0f" }, { "point_down", "\ud83d\udc47" }, { "point_left", "\ud83d\udc48" }, { "point_right", "\ud83d\udc49" }, { "middle_finger", "\ud83d\udd95" }, { "ok_hand", "\ud83d\udc4c" }, { "eyes", "\ud83d\udc40" }, { "eye", "\ud83d\udc41\ufe0f" }, { "tongue", "\ud83d\udc45" }, { "lips", "\ud83d\udc44" }, { "100", "\ud83d\udcaf" }, { "salute", "\ud83e\udee1" }, { "saluting_face", "\ud83e\udee1" }, { "heart", "❤\ufe0f" }, { "red_heart", "❤\ufe0f" }, { "orange_heart", "\ud83e\udde1" }, { "yellow_heart", "\ud83d\udc9b" }, { "green_heart", "\ud83d\udc9a" }, { "blue_heart", "\ud83d\udc99" }, { "purple_heart", "\ud83d\udc9c" }, { "black_heart", "\ud83d\udda4" }, { "white_heart", "\ud83e\udd0d" }, { "broken_heart", "\ud83d\udc94" }, { "heartbeat", "\ud83d\udc93" }, { "sparkling_heart", "\ud83d\udc96" }, { "two_hearts", "\ud83d\udc95" }, { "revolving_hearts", "\ud83d\udc9e" }, { "cupid", "\ud83d\udc98" }, { "gift_heart", "\ud83d\udc9d" }, { "heartpulse", "\ud83d\udc97" }, { "star", "⭐" }, { "star2", "\ud83c\udf1f" }, { "sparkles", "✨" }, { "dizzy", "\ud83d\udcab" }, { "boom", "\ud83d\udca5" }, { "collision", "\ud83d\udca5" }, { "anger", "\ud83d\udca2" }, { "exclamation", "❗" }, { "question", "❓" }, { "x", "❌" }, { "o", "⭕" }, { "check", "✅" }, { "white_check_mark", "✅" }, { "heavy_check_mark", "✔\ufe0f" }, { "ballot_box_with_check", "☑\ufe0f" }, { "warning", "⚠\ufe0f" }, { "no_entry", "⛔" }, { "no_entry_sign", "\ud83d\udeab" }, { "zzz", "\ud83d\udca4" }, { "fire", "\ud83d\udd25" }, { "flame", "\ud83d\udd25" }, { "droplet", "\ud83d\udca7" }, { "sweat_drops", "\ud83d\udca6" }, { "snowflake", "❄\ufe0f" }, { "cloud", "☁\ufe0f" }, { "sun", "☀\ufe0f" }, { "sunny", "☀\ufe0f" }, { "rainbow", "\ud83c\udf08" }, { "zap", "⚡" }, { "lightning", "⚡" }, { "thunder", "⚡" }, { "tornado", "\ud83c\udf2a\ufe0f" }, { "ocean", "\ud83c\udf0a" }, { "earth_americas", "\ud83c\udf0e" }, { "globe_with_meridians", "\ud83c\udf10" }, { "crescent_moon", "\ud83c\udf19" }, { "full_moon", "\ud83c\udf15" }, { "comet", "☄\ufe0f" }, { "volcano", "\ud83c\udf0b" }, { "mountain", "⛰\ufe0f" }, { "mount_fuji", "\ud83d\uddfb" }, { "evergreen_tree", "\ud83c\udf32" }, { "deciduous_tree", "\ud83c\udf33" }, { "fallen_leaf", "\ud83c\udf42" }, { "maple_leaf", "\ud83c\udf41" }, { "mushroom", "\ud83c\udf44" }, { "herb", "\ud83c\udf3f" }, { "seedling", "\ud83c\udf31" }, { "blossom", "\ud83c\udf3c" }, { "rose", "\ud83c\udf39" }, { "sunflower", "\ud83c\udf3b" }, { "tulip", "\ud83c\udf37" }, { "wolf", "\ud83d\udc3a" }, { "bear", "\ud83d\udc3b" }, { "boar", "\ud83d\udc17" }, { "pig", "\ud83d\udc37" }, { "deer", "\ud83e\udd8c" }, { "eagle", "\ud83e\udd85" }, { "snake", "\ud83d\udc0d" }, { "dragon", "\ud83d\udc09" }, { "dragon_face", "\ud83d\udc32" }, { "bat", "\ud83e\udd87" }, { "spider", "\ud83d\udd77\ufe0f" }, { "bug", "\ud83d\udc1b" }, { "bee", "\ud83d\udc1d" }, { "fish", "\ud83d\udc1f" }, { "chicken", "\ud83d\udc14" }, { "rooster", "\ud83d\udc13" }, { "dog", "\ud83d\udc36" }, { "cat", "\ud83d\udc31" }, { "horse", "\ud83d\udc34" }, { "unicorn", "\ud83e\udd84" }, { "crab", "\ud83e\udd80" }, { "octopus", "\ud83d\udc19" }, { "whale", "\ud83d\udc0b" }, { "turtle", "\ud83d\udc22" }, { "frog", "\ud83d\udc38" }, { "monkey_face", "\ud83d\udc35" }, { "gorilla", "\ud83e\udd8d" }, { "lion", "\ud83e\udd81" }, { "lion_face", "\ud83e\udd81" }, { "fox", "\ud83e\udd8a" }, { "fox_face", "\ud83e\udd8a" }, { "crow", "\ud83d\udc26\u200d⬛" }, { "bird", "\ud83d\udc26" }, { "beer", "\ud83c\udf7a" }, { "beers", "\ud83c\udf7b" }, { "wine_glass", "\ud83c\udf77" }, { "cocktail", "\ud83c\udf78" }, { "mead", "\ud83c\udf7a" }, { "meat_on_bone", "\ud83c\udf56" }, { "poultry_leg", "\ud83c\udf57" }, { "steak", "\ud83e\udd69" }, { "cut_of_meat", "\ud83e\udd69" }, { "bread", "\ud83c\udf5e" }, { "cheese", "\ud83e\uddc0" }, { "apple", "\ud83c\udf4e" }, { "mushroom_food", "\ud83c\udf44" }, { "pie", "\ud83e\udd67" }, { "honey_pot", "\ud83c\udf6f" }, { "egg", "\ud83e\udd5a" }, { "fish_food", "\ud83d\udc1f" }, { "cooking", "\ud83c\udf73" }, { "fork_and_knife", "\ud83c\udf74" }, { "pizza", "\ud83c\udf55" }, { "hamburger", "\ud83c\udf54" }, { "taco", "\ud83c\udf2e" }, { "cake", "\ud83c\udf70" }, { "coffee", "☕" }, { "crossed_swords", "⚔\ufe0f" }, { "swords", "⚔\ufe0f" }, { "dagger", "\ud83d\udde1\ufe0f" }, { "dagger_knife", "\ud83d\udde1\ufe0f" }, { "shield", "\ud83d\udee1\ufe0f" }, { "bow_and_arrow", "\ud83c\udff9" }, { "axe", "\ud83e\ude93" }, { "hammer", "\ud83d\udd28" }, { "pick", "⛏\ufe0f" }, { "wrench", "\ud83d\udd27" }, { "gear", "⚙\ufe0f" }, { "bomb", "\ud83d\udca3" }, { "trident", "\ud83d\udd31" }, { "crown", "\ud83d\udc51" }, { "gem", "\ud83d\udc8e" }, { "ring", "\ud83d\udc8d" }, { "trophy", "\ud83c\udfc6" }, { "medal", "\ud83c\udfc5" }, { "military_medal", "\ud83c\udf96\ufe0f" }, { "scroll", "\ud83d\udcdc" }, { "map", "\ud83d\uddfa\ufe0f" }, { "world_map", "\ud83d\uddfa\ufe0f" }, { "compass", "\ud83e\udded" }, { "anchor", "⚓" }, { "boat", "⛵" }, { "sailboat", "⛵" }, { "canoe", "\ud83d\udef6" }, { "ship", "\ud83d\udea2" }, { "skull_and_crossbones", "☠\ufe0f" }, { "coffin", "⚰\ufe0f" }, { "key", "\ud83d\udd11" }, { "lock", "\ud83d\udd12" }, { "chains", "⛓\ufe0f" }, { "magic_wand", "\ud83e\ude84" }, { "crystal_ball", "\ud83d\udd2e" }, { "hourglass", "⌛" }, { "shield2", "\ud83d\udee1\ufe0f" }, { "house", "\ud83c\udfe0" }, { "castle", "\ud83c\udff0" }, { "european_castle", "\ud83c\udff0" }, { "tent", "⛺" }, { "camping", "\ud83c\udfd5\ufe0f" }, { "house_with_garden", "\ud83c\udfe1" }, { "church", "⛪" }, { "tada", "\ud83c\udf89" }, { "confetti_ball", "\ud83c\udf8a" }, { "party_popper", "\ud83c\udf89" }, { "ribbon", "\ud83c\udf80" }, { "gift", "\ud83c\udf81" }, { "balloon", "\ud83c\udf88" }, { "fireworks", "\ud83c\udf86" }, { "sparkler", "\ud83c\udf87" }, { "video_game", "\ud83c\udfae" }, { "joystick", "\ud83d\udd79\ufe0f" }, { "game_die", "\ud83c\udfb2" }, { "dart", "\ud83c\udfaf" }, { "bullseye", "\ud83c\udfaf" }, { "musical_note", "\ud83c\udfb5" }, { "notes", "\ud83c\udfb6" }, { "headphones", "\ud83c\udfa7" }, { "gg", "\ud83c\udfae" }, { "lol", "\ud83d\ude02" }, { "rip", "\ud83e\udea6" }, { "headstone", "\ud83e\udea6" }, { "coffin2", "⚰\ufe0f" }, { "flag_white", "\ud83c\udff3\ufe0f" }, { "pirate_flag", "\ud83c\udff4\u200d☠\ufe0f" }, { "triangular_flag_on_post", "\ud83d\udea9" }, { "crossed_flags", "\ud83c\udf8c" }, { "checkered_flag", "\ud83c\udfc1" }, { "mega", "\ud83d\udce3" }, { "loudspeaker", "\ud83d\udce2" }, { "bell", "\ud83d\udd14" }, { "speech_balloon", "\ud83d\udcac" }, { "thought_balloon", "\ud83d\udcad" }, { "up", "⬆\ufe0f" }, { "down", "⬇\ufe0f" }, { "left", "⬅\ufe0f" }, { "right", "➡\ufe0f" }, { "arrow_up", "⬆\ufe0f" }, { "arrow_down", "⬇\ufe0f" }, { "arrow_left", "⬅\ufe0f" }, { "arrow_right", "➡\ufe0f" }, { "arrows_counterclockwise", "\ud83d\udd04" }, { "arrows_clockwise", "\ud83d\udd03" }, { "recycle", "♻\ufe0f" } }; public static string ConvertEmoji(string message) { if (string.IsNullOrEmpty(message)) { return message; } message = CustomEmojiRegex.Replace(message, delegate(Match match) { string value3 = match.Groups[1].Value; string value4; return ShortcodeMap.TryGetValue(value3, out value4) ? value4 : value3; }); message = ShortcodeRegex.Replace(message, delegate(Match match) { string value = match.Groups[1].Value; string value2; return ShortcodeMap.TryGetValue(value, out value2) ? value2 : match.Value; }); return message; } } public static class DiscordIntegrationConfig { public static ConfigEntry<bool> Enabled; public static ConfigEntry<bool> VerboseLogging; public static ConfigEntry<string> GeneralWebhookUrl; public static ConfigEntry<string> AntiCheatWebhookUrl; public static ConfigEntry<string> ChatWebhookUrl; public static ConfigEntry<string> ClientLogWebhookUrl; public static ConfigEntry<bool> NotifyServerStart; public static ConfigEntry<bool> NotifyServerStop; public static ConfigEntry<bool> NotifyServerSave; public static ConfigEntry<bool> NotifyPlayerJoin; public static ConfigEntry<bool> NotifyPlayerLeave; public static ConfigEntry<bool> NotifyPlayerDeath; public static ConfigEntry<bool> NotifyPlayerShout; public static ConfigEntry<bool> NotifyPlayerSay; public static ConfigEntry<bool> NotifyPlayerWhisper; public static ConfigEntry<bool> NotifyRandomEvent; public static ConfigEntry<bool> NotifyNewDay; public static ConfigEntry<bool> NotifyAntiCheatKick; public static ConfigEntry<bool> NotifyAntiCheatViolation; public static ConfigEntry<bool> SendModListOnKick; public static ConfigEntry<bool> SendBepInExLogOnKick; public static ConfigEntry<bool> NotifyHourlySummary; public static ConfigEntry<bool> NotifyDailySummary; public static ConfigEntry<bool> EnableStatusHeartbeat; public static ConfigEntry<float> StatusHeartbeatIntervalMinutes; public static ConfigEntry<bool> NotifyClientLoginArtifacts; public static ConfigEntry<bool> AttachFullClientLogOnLogin; public static ConfigEntry<bool> AttachModListOnLogin; public static ConfigEntry<bool> AttachErrorsWarningsOnLogin; public static ConfigEntry<bool> OnlyNotifyLoginIfErrorsOrWarnings; public static ConfigEntry<bool> EnableLogRequestReaction; public static ConfigEntry<string> LogRequestReactionEmoji; public static ConfigEntry<string> BotAdminDiscordIds; public static ConfigEntry<string> ServerManagerWebhookUrl; public static ConfigEntry<string> ServerManagerChannelId; public static ConfigEntry<string> WebhookDisplayName; public static ConfigEntry<string> WebhookAvatarUrl; public static ConfigEntry<bool> EnableBotListener; public static ConfigEntry<string> ChatChannelId; public static ConfigEntry<string> StatusChannelId; public static ConfigEntry<float> BotPollIntervalSeconds; public static ConfigEntry<string> DiscordGuildId; public static void Initialize(ConfigFile config) { Enabled = config.Bind<bool>("Discord", "Enabled", false, "Enable Discord webhook integration. Only active on server/host."); VerboseLogging = config.Bind<bool>("Discord", "VerboseLogging", false, "Enable detailed Discord integration debug logging."); GeneralWebhookUrl = config.Bind<string>("Discord.Webhooks", "GeneralWebhookUrl", "", "Discord webhook URL for general events (join, leave, death, server status). Must be a valid Discord webhook URL (https://discord.com/api/webhooks/...)."); AntiCheatWebhookUrl = config.Bind<string>("Discord.Webhooks", "AntiCheatWebhookUrl", "", "Discord webhook URL for anti-cheat notifications (kicks, violations). Falls back to GeneralWebhookUrl if empty."); ChatWebhookUrl = config.Bind<string>("Discord.Webhooks", "ChatWebhookUrl", "", "Discord webhook URL for in-game chat relay (shout messages). Falls back to GeneralWebhookUrl if empty."); ClientLogWebhookUrl = config.Bind<string>("Discord.Webhooks", "ClientLogWebhookUrl", "", "Discord webhook URL for per-login client log forwarding (full BepInEx log, mod list, errors+warnings report). Fires on every successful client login so admins can audit what mods the client is running and whether their session is clean. Falls back to GeneralWebhookUrl if empty."); NotifyServerStart = config.Bind<bool>("Discord.Events", "NotifyServerStart", true, "Send webhook when server starts."); NotifyServerStop = config.Bind<bool>("Discord.Events", "NotifyServerStop", true, "Send webhook when server stops."); NotifyServerSave = config.Bind<bool>("Discord.Events", "NotifyServerSave", false, "Send webhook when world is saved. Disabled by default (frequent)."); NotifyPlayerJoin = config.Bind<bool>("Discord.Events", "NotifyPlayerJoin", true, "Send webhook when a player joins."); NotifyPlayerLeave = config.Bind<bool>("Discord.Events", "NotifyPlayerLeave", true, "Send webhook when a player leaves."); NotifyPlayerDeath = config.Bind<bool>("Discord.Events", "NotifyPlayerDeath", true, "Send webhook when a player dies."); NotifyPlayerShout = config.Bind<bool>("Discord.Events", "NotifyPlayerShout", true, "Send webhook when a player shouts in chat (/s, global range)."); NotifyPlayerSay = config.Bind<bool>("Discord.Events", "NotifyPlayerSay", true, "Send webhook when a player uses normal local-range chat (/say, the default Enter-key chat)."); NotifyPlayerWhisper = config.Bind<bool>("Discord.Events", "NotifyPlayerWhisper", true, "Send webhook when a player whispers (/w, private to nearest player). Turn off if your server treats whispers as private and shouldn't surface them in Discord."); NotifyRandomEvent = config.Bind<bool>("Discord.Events", "NotifyRandomEvent", true, "Send webhook when a random event (raid) starts/stops."); NotifyNewDay = config.Bind<bool>("Discord.Events", "NotifyNewDay", false, "Send webhook on each new in-game day. Disabled by default (frequent)."); NotifyAntiCheatKick = config.Bind<bool>("Discord.Events", "NotifyAntiCheatKick", true, "Send webhook when VAngarde kicks a player."); NotifyAntiCheatViolation = config.Bind<bool>("Discord.Events", "NotifyAntiCheatViolation", true, "Send webhook when VAngarde detects a violation (even in log-only mode)."); SendModListOnKick = config.Bind<bool>("Discord.Events", "SendModListOnKick", true, "Attach the kicked player's mod list as a text file to the kick webhook. Useful for debugging which mods caused the violation."); SendBepInExLogOnKick = config.Bind<bool>("Discord.Events", "SendBepInExLogOnKick", true, "Attach the kicked client's BepInEx output log to the kick webhook. The client sends the tail of their log with their challenge response. Falls back to the server's log if the client's log wasn't received."); NotifyHourlySummary = config.Bind<bool>("Discord.Events", "NotifyHourlySummary", true, "Send a rich status embed every hour with player activity, uptime, and session stats."); NotifyDailySummary = config.Bind<bool>("Discord.Events", "NotifyDailySummary", true, "Send a daily recap embed at midnight Eastern Time (EST/EDT) with a full 24-hour summary."); EnableStatusHeartbeat = config.Bind<bool>("Discord.Events", "EnableStatusHeartbeat", false, "Send periodic status heartbeat messages to the status channel showing server uptime and online players. Automatically cleans up messages older than 24 hours. Independent from hourly summaries."); StatusHeartbeatIntervalMinutes = config.Bind<float>("Discord.Events", "StatusHeartbeatIntervalMinutes", 15f, "How often (in minutes) to send status heartbeat updates. Range: 1-60. Default: 15 minutes."); NotifyClientLoginArtifacts = config.Bind<bool>("Discord.ClientLogs", "NotifyClientLoginArtifacts", false, "Send a Discord embed with the client's BepInEx log, mod list, and errors/warnings summary on every successful client login. Routes to ClientLogWebhookUrl (or GeneralWebhookUrl)."); AttachFullClientLogOnLogin = config.Bind<bool>("Discord.ClientLogs", "AttachFullClientLogOnLogin", false, "Attach the client's full BepInEx LogOutput.log as a .log file. Can be large (1-10 MB). Default OFF: enable EnableLogRequestReaction below to fetch the log on demand by reacting to the login snapshot in Discord instead of auto-attaching it every time."); AttachModListOnLogin = config.Bind<bool>("Discord.ClientLogs", "AttachModListOnLogin", true, "Attach the client's mod list (GUID=Version) as a .txt file."); AttachErrorsWarningsOnLogin = config.Bind<bool>("Discord.ClientLogs", "AttachErrorsWarningsOnLogin", true, "Attach only the Error/Warning/Exception lines extracted from the client log as a .txt file."); OnlyNotifyLoginIfErrorsOrWarnings = config.Bind<bool>("Discord.ClientLogs", "OnlyNotifyLoginIfErrorsOrWarnings", false, "If true, only fire the login webhook when the client's log contains at least one error or warning. Recommended for busy servers to reduce Discord noise."); EnableLogRequestReaction = config.Bind<bool>("Discord.ClientLogs", "EnableLogRequestReaction", true, "Show a \"React with {emoji} to request the full log\" footer on every login snapshot. When an authorised Discord user (see BotAdminDiscordIds) adds that reaction, the server posts the cached full LogOutput.log to the same webhook. Requires the bot listener to be enabled."); LogRequestReactionEmoji = config.Bind<string>("Discord.ClientLogs", "LogRequestReactionEmoji", "\ud83d\udce5", "Emoji used for log-request reactions. Any single-codepoint Unicode emoji works. Default is the ?? inbox-tray glyph."); BotAdminDiscordIds = config.Bind<string>("Discord.ClientLogs", "BotAdminDiscordIds", "", "Comma-separated Discord user IDs allowed to trigger full-log uploads via reaction. Right-click a user in Discord (with developer mode on) ? Copy User ID. Leave empty to block every request."); ServerManagerWebhookUrl = config.Bind<string>("Discord.ServerManager", "ServerManagerWebhookUrl", "", "PRIVATE admin-only Discord webhook URL for the Server Manager control panel. This is where the live-updating 'Server Online' heartbeat posts (player list, uptime, last heartbeat) AND the restart/stop reaction controls live. DO NOT point this at a public channel - anyone with reaction perms there could trigger a restart/stop. Use a private admin-only channel. Leave empty to disable the manager panel entirely."); ServerManagerChannelId = config.Bind<string>("Discord.ServerManager", "ServerManagerChannelId", "", "Discord channel ID for the Server Manager control panel. Right-click the channel in Discord (developer mode on) -> Copy Channel ID. Required so the bot can edit the rolling heartbeat message and poll reactions. Falls back to BotListener.StatusChannelId if empty."); WebhookDisplayName = config.Bind<string>("Discord.Appearance", "WebhookDisplayName", "Valheim Server", "Display name shown on Discord webhook messages."); WebhookAvatarUrl = config.Bind<string>("Discord.Appearance", "WebhookAvatarUrl", "", "Avatar image URL for webhook messages. Leave empty for default."); EnableBotListener = config.Bind<bool>("Discord.BotListener", "EnableBotListener", false, "Enable the Discord bot listener that relays messages from a Discord channel into the game server chat. Requires a valid bot token (set in FiresDiscordIntegration_BotToken.cfg at the Valheim install root, NOT in this config file) and a ChatChannelId set below."); ChatChannelId = config.Bind<string>("Discord.BotListener", "ChatChannelId", "", "Discord Channel ID to listen for messages. Right-click the channel in Discord ? Copy Channel ID. This should be the same channel your ChatWebhookUrl posts to."); StatusChannelId = config.Bind<string>("Discord.BotListener", "StatusChannelId", "", "Discord Channel ID where the crash detector posts its live-updating status message. The bot must have Send Messages permission in this channel. Leave empty to use ChatChannelId instead."); BotPollIntervalSeconds = config.Bind<float>("Discord.BotListener", "BotPollIntervalSeconds", 3f, "How often (in seconds) to poll Discord for new messages. Lower = faster relay, more API calls. Range: 1-30. Discord rate limit is ~5 requests/second."); DiscordGuildId = config.Bind<string>("Discord.BotListener", "DiscordGuildId", "", "Your Discord Server (Guild) ID. Required for @mention resolution in chat webhooks. Right-click your server name in Discord ? Copy Server ID."); } public static void BindToSync(ConfigSync configSync) { if (configSync != null) { configSync.AddConfigEntry<bool>(Enabled); configSync.AddConfigEntry<bool>(VerboseLogging); configSync.AddConfigEntry<bool>(NotifyServerStart); configSync.AddConfigEntry<bool>(NotifyServerStop); configSync.AddConfigEntry<bool>(NotifyServerSave); configSync.AddConfigEntry<bool>(NotifyPlayerJoin); configSync.AddConfigEntry<bool>(NotifyPlayerLeave); configSync.AddConfigEntry<bool>(NotifyPlayerDeath); configSync.AddConfigEntry<bool>(NotifyPlayerShout); configSync.AddConfigEntry<bool>(NotifyPlayerSay); configSync.AddConfigEntry<bool>(NotifyPlayerWhisper); configSync.AddConfigEntry<bool>(NotifyRandomEvent); configSync.AddConfigEntry<bool>(NotifyNewDay); configSync.AddConfigEntry<bool>(NotifyAntiCheatKick); configSync.AddConfigEntry<bool>(NotifyAntiCheatViolation); configSync.AddConfigEntry<bool>(SendModListOnKick); configSync.AddConfigEntry<bool>(SendBepInExLogOnKick); configSync.AddConfigEntry<bool>(NotifyHourlySummary); configSync.AddConfigEntry<bool>(NotifyDailySummary); configSync.AddConfigEntry<bool>(EnableStatusHeartbeat); configSync.AddConfigEntry<float>(StatusHeartbeatIntervalMinutes); configSync.AddConfigEntry<string>(WebhookDisplayName); configSync.AddConfigEntry<string>(WebhookAvatarUrl); configSync.AddConfigEntry<bool>(NotifyClientLoginArtifacts); configSync.AddConfigEntry<bool>(AttachFullClientLogOnLogin); configSync.AddConfigEntry<bool>(AttachModListOnLogin); configSync.AddConfigEntry<bool>(AttachErrorsWarningsOnLogin); configSync.AddConfigEntry<bool>(OnlyNotifyLoginIfErrorsOrWarnings); configSync.AddConfigEntry<bool>(EnableLogRequestReaction); } } public static string ResolveWebhookUrl(WebhookCategory category) { string text = null; switch (category) { case WebhookCategory.AntiCheat: text = AntiCheatWebhookUrl?.Value; break; case WebhookCategory.Chat: text = ChatWebhookUrl?.Value; break; case WebhookCategory.ClientLog: text = ClientLogWebhookUrl?.Value; break; } if (string.IsNullOrEmpty(text)) { text = GeneralWebhookUrl?.Value; } return text; } } public enum WebhookCategory { General, AntiCheat, Chat, ClientLog } public static class DiscordIntegrationCore { private enum AttachedLogSource { ClientFresh, ClientCached, ServerFallback } public enum ChatLineType { Say, Shout, Whisper } public static bool IsActive() { if (DiscordIntegrationConfig.Enabled == null || !DiscordIntegrationConfig.Enabled.Value) { return false; } if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return false; } return true; } private static string GetUsername() { string text = DiscordIntegrationConfig.WebhookDisplayName?.Value; if (!string.IsNullOrEmpty(text) && text != "Valheim Server") { return text; } return DiscordBotIdentity.ResolveUsername(text ?? "Valheim Server"); } private static string GetAvatarUrl() { string text = DiscordIntegrationConfig.WebhookAvatarUrl?.Value; if (!string.IsNullOrEmpty(text)) { return text; } return DiscordBotIdentity.ResolveAvatarUrl(null); } private static string GetServerName() { return ServerNameResolver.GetOperatorServerName(); } private static string LogSourceFilenamePrefix(AttachedLogSource source) { if (1 == 0) { } string result = source switch { AttachedLogSource.ClientFresh => "client", AttachedLogSource.ClientCached => "client_cached", AttachedLogSource.ServerFallback => "server", _ => "log", }; if (1 == 0) { } return result; } private static string LogSourceFieldLabel(AttachedLogSource source) { if (1 == 0) { } string result = source switch { AttachedLogSource.ClientFresh => "Client Log", AttachedLogSource.ClientCached => "Client Log (cached)", AttachedLogSource.ServerFallback => "Server Log (fallback)", _ => "Log", }; if (1 == 0) { } return result; } public static void OnServerStart() { if (IsActive()) { DiscordMentionResolver.FetchRoles(); ConfigEntry<bool> notifyServerStart = DiscordIntegrationConfig.NotifyServerStart; if (notifyServerStart != null && notifyServerStart.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordEmbed embed = new DiscordEmbed().SetTitle("\ud83d\udfe2 Server Started").SetDescription("**" + GetServerName() + "** is now online.").SetColor(5763719) .SetFooter(GetServerName()); DiscordWebhookService.SendEmbed(webhookUrl, embed, GetUsername(), GetAvatarUrl()); LogVerbose("Sent ServerStart webhook"); } } } public static void OnServerStop() { if (IsActive()) { ConfigEntry<bool> notifyServerStop = DiscordIntegrationConfig.NotifyServerStop; if (notifyServerStop != null && notifyServerStop.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordEmbed embed = new DiscordEmbed().SetTitle("\ud83d\udd34 Server Stopped").SetDescription("**" + GetServerName() + "** has gone offline.").SetColor(15548997) .SetFooter(GetServerName()); DiscordWebhookService.SendEmbed(webhookUrl, embed, GetUsername(), GetAvatarUrl()); LogVerbose("Sent ServerStop webhook"); } } } public static void OnServerSave() { if (IsActive()) { ConfigEntry<bool> notifyServerSave = DiscordIntegrationConfig.NotifyServerSave; if (notifyServerSave != null && notifyServerSave.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordWebhookService.SendMessage(webhookUrl, "\ud83d\udcbe **World saved** \ufffd " + GetServerName(), GetUsername(), GetAvatarUrl()); LogVerbose("Sent ServerSave webhook"); } } } public static void OnPlayerJoin(string playerName, string steamId) { if (IsActive()) { ConfigEntry<bool> notifyPlayerJoin = DiscordIntegrationConfig.NotifyPlayerJoin; if (notifyPlayerJoin != null && notifyPlayerJoin.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordEmbed embed = new DiscordEmbed().SetTitle("\ud83d\udc4b Player Joined").SetColor(5763719).AddField("Player", playerName, inline: true) .AddField("Steam ID", steamId ?? "Unknown", inline: true) .SetFooter(GetServerName()); DiscordWebhookService.SendEmbed(webhookUrl, embed, GetUsername(), GetAvatarUrl()); LogVerbose("Sent PlayerJoin webhook: " + playerName); } } } public static void OnPlayerLeave(string playerName, string steamId) { if (IsActive()) { ConfigEntry<bool> notifyPlayerLeave = DiscordIntegrationConfig.NotifyPlayerLeave; if (notifyPlayerLeave != null && notifyPlayerLeave.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordEmbed embed = new DiscordEmbed().SetTitle("\ud83d\udc4b Player Left").SetColor(15548997).AddField("Player", playerName, inline: true) .AddField("Steam ID", steamId ?? "Unknown", inline: true) .SetFooter(GetServerName()); DiscordWebhookService.SendEmbed(webhookUrl, embed, GetUsername(), GetAvatarUrl()); LogVerbose("Sent PlayerLeave webhook: " + playerName); } } } public static void OnPlayerDeath(string playerName) { if (IsActive()) { ConfigEntry<bool> notifyPlayerDeath = DiscordIntegrationConfig.NotifyPlayerDeath; if (notifyPlayerDeath != null && notifyPlayerDeath.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordEmbed embed = new DiscordEmbed().SetTitle("\ud83d\udc80 Player Died").SetDescription("**" + playerName + "** has fallen.").SetColor(15548997) .SetFooter(GetServerName()); DiscordWebhookService.SendEmbed(webhookUrl, embed, GetUsername(), GetAvatarUrl()); LogVerbose("Sent PlayerDeath webhook: " + playerName); } } } public static void OnPlayerChat(string playerName, string message, ChatLineType lineType) { if (IsActive() && IsChatTypeEnabled(lineType)) { message = DiscordMentionResolver.ResolveMentions(message); string content = FormatChatLineBody(playerName, message, lineType); string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.Chat); DiscordWebhookService.SendMessage(webhookUrl, content, GetUsername(), GetAvatarUrl()); LogVerbose($"Sent {lineType} webhook: {playerName}: {message}"); } } public static void OnPlayerShout(string playerName, string message) { OnPlayerChat(playerName, message, ChatLineType.Shout); } private static bool IsChatTypeEnabled(ChatLineType lineType) { return lineType switch { ChatLineType.Shout => DiscordIntegrationConfig.NotifyPlayerShout?.Value ?? false, ChatLineType.Say => DiscordIntegrationConfig.NotifyPlayerSay?.Value ?? false, ChatLineType.Whisper => DiscordIntegrationConfig.NotifyPlayerWhisper?.Value ?? false, _ => false, }; } private static string FormatChatLineBody(string playerName, string message, ChatLineType lineType) { return lineType switch { ChatLineType.Shout => "\ud83d\udce2 **" + playerName + ":** " + message, ChatLineType.Whisper => "\ud83e\udd2b **" + playerName + "** *(whisper)*: *" + message + "*", _ => "\ud83d\udcac **" + playerName + ":** " + message, }; } public static void OnRandomEventStart(string eventName, string startMessage, Vector3 position) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) if (IsActive()) { ConfigEntry<bool> notifyRandomEvent = DiscordIntegrationConfig.NotifyRandomEvent; if (notifyRandomEvent != null && notifyRandomEvent.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordEmbed embed = new DiscordEmbed().SetTitle("⚔\ufe0f Event Started").SetColor(10181046).AddField("Event", eventName, inline: true) .AddField("Message", startMessage, inline: true) .AddField("Location", $"{position.x:F0}, {position.y:F0}, {position.z:F0}", inline: true) .SetFooter(GetServerName()); DiscordWebhookService.SendEmbed(webhookUrl, embed, GetUsername(), GetAvatarUrl()); LogVerbose("Sent EventStart webhook: " + eventName); } } } public static void OnRandomEventStop(string eventName, string endMessage) { if (IsActive()) { ConfigEntry<bool> notifyRandomEvent = DiscordIntegrationConfig.NotifyRandomEvent; if (notifyRandomEvent != null && notifyRandomEvent.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordEmbed embed = new DiscordEmbed().SetTitle("\ud83d\uded1 Event Ended").SetDescription(endMessage).SetColor(16776960) .AddField("Event", eventName, inline: true) .SetFooter(GetServerName()); DiscordWebhookService.SendEmbed(webhookUrl, embed, GetUsername(), GetAvatarUrl()); LogVerbose("Sent EventStop webhook: " + eventName); } } } public static void OnNewDay(int dayNumber) { if (IsActive()) { ConfigEntry<bool> notifyNewDay = DiscordIntegrationConfig.NotifyNewDay; if (notifyNewDay != null && notifyNewDay.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); DiscordWebhookService.SendMessage(webhookUrl, $"\ud83c\udf05 **New day: {dayNumber}**", GetUsername(), GetAvatarUrl()); LogVerbose($"Sent NewDay webhook: day {dayNumber}"); } } } public static void OnAntiCheatViolation(string playerName, string platformId, string reason, int violationCount) { if (IsActive()) { ConfigEntry<bool> notifyAntiCheatViolation = DiscordIntegrationConfig.NotifyAntiCheatViolation; if (notifyAntiCheatViolation != null && notifyAntiCheatViolation.Value) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.AntiCheat); DiscordEmbed embed = new DiscordEmbed().SetTitle("⚠\ufe0f Anti-Cheat Violation").SetColor(15105570).AddField("Player", playerName ?? "Unknown", inline: true) .AddField("Steam ID", platformId ?? "Unknown", inline: true) .AddField("Violation #", violationCount.ToString(), inline: true) .AddField("Reason", reason ?? "Unknown") .SetFooter("VAngarde \ufffd " + GetServerName()); DiscordWebhookService.SendEmbed(webhookUrl, embed, GetUsername(), GetAvatarUrl()); LogVerbose("Sent AntiCheatViolation webhook: " + platformId + " \ufffd " + reason); } } } public static void OnAntiCheatKick(string playerName, string platformId, string reason, Action onComplete = null) { OnAntiCheatKick(playerName, platformId, reason, null, null, onComplete); } public static void OnAntiCheatKick(string playerName, string platformId, string reason, Dictionary<string, string> clientModList, byte[] clientBepInExLog, Action onComplete = null) { if (!IsActive()) { onComplete?.Invoke(); return; } ConfigEntry<bool> notifyAntiCheatKick = DiscordIntegrationConfig.NotifyAntiCheatKick; if (notifyAntiCheatKick == null || !notifyAntiCheatKick.Value) { onComplete?.Invoke(); return; } string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.AntiCheat); DiscordEmbed discordEmbed = new DiscordEmbed().SetTitle("\ud83d\udeab Player Kicked \ufffd Anti-Cheat").SetColor(10038562).AddField("Player", playerName ?? "Unknown", inline: true) .AddField("Steam ID", platformId ?? "Unknown", inline: true) .AddField("Reason", reason ?? "Unknown") .SetFooter("VAngarde \ufffd " + GetServerName()); List<DiscordFileAttachment> list = new List<DiscordFileAttachment>(); string text = (platformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_"); if (clientModList != null && clientModList.Count > 0) { ConfigEntry<bool> sendModListOnKick = DiscordIntegrationConfig.SendModListOnKick; if (sendModListOnKick != null && sendModListOnKick.Value) { Dictionary<string, string> dictionary = null; try { dictionary = null; } catch { } string textContent = BuildModListText(playerName, platformId, clientModList, dictionary); list.Add(new DiscordFileAttachment($"modlist_{text}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.txt", textContent)); discordEmbed.AddField("Client Mods", clientModList.Count.ToString(), inline: true); if (dictionary != null) { discordEmbed.AddField("Server Mods", dictionary.Count.ToString(), inline: true); } } } ConfigEntry<bool> sendBepInExLogOnKick = DiscordIntegrationConfig.SendBepInExLogOnKick; if (sendBepInExLogOnKick != null && sendBepInExLogOnKick.Value) { byte[] array = clientBepInExLog; AttachedLogSource source = AttachedLogSource.ClientFresh; if (array == null || array.Length == 0) { byte[] array2 = null; if (array2 != null && array2.Length != 0) { array = array2; source = AttachedLogSource.ClientCached; } } if (array == null || array.Length == 0) { array = ReadBepInExLog(); source = AttachedLogSource.ServerFallback; } if (array != null && array.Length != 0) { list.Add(new DiscordFileAttachment($"{LogSourceFilenamePrefix(source)}_log_{text}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.txt", array)); discordEmbed.AddField(LogSourceFieldLabel(source), "Attached", inline: true); } } if (list.Count > 0) { DiscordWebhookService.SendEmbedWithFiles(webhookUrl, discordEmbed, list, GetUsername(), GetAvatarUrl(), onComplete); } else { DiscordWebhookService.SendEmbed(webhookUrl, discordEmbed, GetUsername(), GetAvatarUrl(), onComplete); } LogVerbose("Sent AntiCheatKick webhook: " + platformId + " \ufffd " + reason + ((list.Count > 0) ? $" (with {list.Count} file attachment(s))" : "")); } private static string BuildModListText(string playerName, string platformId, Dictionary<string, string> clientMods, Dictionary<string, string> serverMods) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine(" VAngarde Anti-Cheat \ufffd Mod Comparison Report"); stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine(); stringBuilder.AppendLine("Player: " + (playerName ?? "Unknown")); stringBuilder.AppendLine("Steam ID: " + (platformId ?? "Unknown")); stringBuilder.AppendLine("Server: " + GetServerName()); stringBuilder.AppendLine($"Timestamp: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); stringBuilder.AppendLine($"Client Mods: {clientMods.Count}"); stringBuilder.AppendLine($"Server Mods: {serverMods?.Count ?? 0}"); stringBuilder.AppendLine(); if (serverMods != null && serverMods.Count > 0) { List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>(); List<KeyValuePair<string, string>> list2 = new List<KeyValuePair<string, string>>(); List<(string, string, string)> list3 = new List<(string, string, string)>(); List<string> list4 = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, string> clientMod in clientMods) { hashSet.Add(clientMod.Key); } foreach (KeyValuePair<string, string> serverMod in serverMods) { hashSet.Add(serverMod.Key); } foreach (string item in hashSet.OrderBy<string, string>((string g) => g, StringComparer.OrdinalIgnoreCase)) { string value; bool flag = clientMods.TryGetValue(item, out value); string value2; bool flag2 = serverMods.TryGetValue(item, out value2); if (flag && flag2) { if (!string.Equals(value, value2, StringComparison.OrdinalIgnoreCase)) { list3.Add((item, value, value2)); } else { list4.Add(item); } } else if (flag && !flag2) { list.Add(new KeyValuePair<string, string>(item, value)); } else if (!flag && flag2) { list2.Add(new KeyValuePair<string, string>(item, value2)); } } stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine(" MISMATCH ANALYSIS"); stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine(); stringBuilder.AppendLine($" Matching: {list4.Count}"); stringBuilder.AppendLine($" Version Mismatch: {list3.Count}"); stringBuilder.AppendLine($" Client-Only: {list.Count} (mods the server does NOT have)"); stringBuilder.AppendLine($" Server-Only: {list2.Count} (mods the client is MISSING)"); stringBuilder.AppendLine(); if (list3.Count > 0) { stringBuilder.AppendLine(" ?? VERSION MISMATCHES ??????????????????????????????????????????"); stringBuilder.AppendLine(string.Format(" {0,-44} {1,-16} {2,-16}", "GUID", "Client", "Server")); stringBuilder.AppendLine(" " + new string('-', 44) + " " + new string('-', 16) + " " + new string('-', 16)); foreach (var (arg, arg2, arg3) in list3) { stringBuilder.AppendLine($" {arg,-44} {arg2,-16} {arg3,-16}"); } stringBuilder.AppendLine(); } if (list.Count > 0) { stringBuilder.AppendLine(" ?? CLIENT-ONLY MODS (not on server) ???????????????????????????"); stringBuilder.AppendLine(string.Format(" {0,-44} {1,-16}", "GUID", "Version")); stringBuilder.AppendLine(" " + new string('-', 44) + " " + new string('-', 16)); foreach (KeyValuePair<string, string> item2 in list) { stringBuilder.AppendLine($" {item2.Key,-44} {item2.Value,-16}"); } stringBuilder.AppendLine(); } if (list2.Count > 0) { stringBuilder.AppendLine(" ?? SERVER-ONLY MODS (missing from client) ?????????????????????"); stringBuilder.AppendLine(string.Format(" {0,-44} {1,-16}", "GUID", "Version")); stringBuilder.AppendLine(" " + new string('-', 44) + " " + new string('-', 16)); foreach (KeyValuePair<string, string> item3 in list2) { stringBuilder.AppendLine($" {item3.Key,-44} {item3.Value,-16}"); } stringBuilder.AppendLine(); } if (list3.Count == 0 && list.Count == 0 && list2.Count == 0) { stringBuilder.AppendLine(" No mismatches found \ufffd client and server mod lists are identical."); stringBuilder.AppendLine(); } } else { stringBuilder.AppendLine(" [Server mod list unavailable \ufffd cross-reference skipped]"); stringBuilder.AppendLine(); } stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine($" CLIENT MOD LIST ({clientMods.Count} mods)"); stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine(string.Format(" {0,-44} {1,-16}", "GUID", "Version")); stringBuilder.AppendLine(" " + new string('-', 44) + " " + new string('-', 16)); foreach (KeyValuePair<string, string> item4 in clientMods.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> kv) => kv.Key, StringComparer.OrdinalIgnoreCase)) { stringBuilder.AppendLine($" {item4.Key,-44} {item4.Value,-16}"); } stringBuilder.AppendLine(); if (serverMods != null && serverMods.Count > 0) { stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine($" SERVER MOD LIST ({serverMods.Count} mods)"); stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine(string.Format(" {0,-44} {1,-16}", "GUID", "Version")); stringBuilder.AppendLine(" " + new string('-', 44) + " " + new string('-', 16)); foreach (KeyValuePair<string, string> item5 in serverMods.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> kv) => kv.Key, StringComparer.OrdinalIgnoreCase)) { stringBuilder.AppendLine($" {item5.Key,-44} {item5.Value,-16}"); } stringBuilder.AppendLine(); } stringBuilder.AppendLine("=========================================================================="); stringBuilder.AppendLine(" End of Report"); stringBuilder.AppendLine("=========================================================================="); return stringBuilder.ToString(); } [Obsolete("Use ClientLogRelay.ReportArtifacts instead. Registered consumers handle Discord + disk.")] public static void OnClientLoginArtifacts(string playerName, string platformId, Dictionary<string, string> clientModList, byte[] clientBepInExLog, string errorsWarningsText, int errorCount, int warningCount) { try { ClientLogArtifacts artifacts = new ClientLogArtifacts(platformId, playerName, clientBepInExLog, clientModList); FiresDiscordIntegration.ClientLogRelay.ClientLogRelay.ReportArtifacts(artifacts); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] Legacy OnClientLoginArtifacts forwarder failed: " + ex.Message)); } } public static void SendClientLogOnRequest(string playerName, string platformId, byte[] logBytes) { if (IsActive() && logBytes != null && logBytes.Length != 0) { string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.AntiCheat); string arg = (platformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_"); DiscordEmbed embed = new DiscordEmbed().SetTitle("\ud83d\udcc4 Client Log \ufffd Admin Request").SetColor(3447003).AddField("Player", playerName ?? "Unknown", inline: true) .AddField("Steam ID", platformId ?? "Unknown", inline: true) .AddField("Log", "Attached", inline: true) .SetFooter("VAngarde \ufffd " + GetServerName()); List<DiscordFileAttachment> files = new List<DiscordFileAttachment> { new DiscordFileAttachment($"client_log_{arg}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.txt", logBytes) }; DiscordWebhookService.SendEmbedWithFiles(webhookUrl, embed, files, GetUsername(), GetAvatarUrl()); LogVerbose($"Sent on-request client log for {platformId} to Discord ({logBytes.Length} bytes)"); } } private static byte[] ReadBepInExLog() { try { string text = Path.Combine(Paths.BepInExRootPath, "LogOutput.log"); if (!File.Exists(text)) { LogVerbose("BepInEx log not found at: " + text); return null; } using FileStream fileStream = new FileStream(text, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); long length = fileStream.Length; if (length <= 0) { return null; } byte[] array = new byte[length]; int i; int num; for (i = 0; i < array.Length; i += num) { num = fileStream.Read(array, i, array.Length - i); if (num <= 0) { break; } } if (i < array.Length) { byte[] array2 = new byte[i]; Array.Copy(array, 0, array2, 0, i); return array2; } return array; } catch (Exception ex) { Debug.LogWarning((object)("[Discord] Failed to read BepInEx log: " + ex.Message)); return null; } } private static void LogVerbose(string msg) { if (DiscordIntegrationConfig.VerboseLogging != null && DiscordIntegrationConfig.VerboseLogging.Value) { Debug.Log((object)("[Discord] " + msg)); } } } [HarmonyPatch] public static class DiscordIntegrationPatches { private static readonly HashSet<string> _joinedPlayers = new HashSet<string>(); private static MethodInfo _envManGetCurrentDay; [HarmonyPatch(typeof(ZNet), "LoadWorld")] [HarmonyPostfix] public static void ZNet_LoadWorld_Postfix() { try { DiscordStatusTracker.Initialize(); DiscordIntegrationCore.OnServerStart(); DiscordBotListener.Start(); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] LoadWorld webhook error: " + ex.Message)); } } [HarmonyPatch(typeof(ZNet), "SaveWorld")] [HarmonyPostfix] public static void ZNet_SaveWorld_Postfix() { try { DiscordStatusTracker.RecordWorldSave(); DiscordIntegrationCore.OnServerSave(); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] SaveWorld webhook error: " + ex.Message)); } } [HarmonyPatch(typeof(Game), "OnApplicationQuit")] [HarmonyPostfix] public static void Game_OnApplicationQuit_Postfix() { try { ServerHeartbeat.OnServerStop(); DiscordBotListener.Stop(); DiscordIntegrationCore.OnServerStop(); DiscordStatusTracker.Shutdown(); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] OnApplicationQuit webhook error: " + ex.Message)); } } [HarmonyPatch(typeof(ZNet), "RPC_CharacterID")] [HarmonyPostfix] public static void ZNet_RPC_CharacterID_Postfix(ZNet __instance, ZRpc rpc, ZDOID characterID) { try { if (!__instance.IsServer()) { return; } ZNetPeer peer = __instance.GetPeer(rpc); if (peer != null) { string hostName = peer.m_socket.GetHostName(); if (_joinedPlayers.Add(hostName)) { DiscordStatusTracker.RecordJoin(peer.m_playerName, hostName); DiscordIntegrationCore.OnPlayerJoin(peer.m_playerName, hostName); } else if (((ZDOID)(ref peer.m_characterID)).ID != 0) { DiscordStatusTracker.RecordDeath(); DiscordIntegrationCore.OnPlayerDeath(peer.m_playerName); } } } catch (Exception ex) { Debug.LogWarning((object)("[Discord] RPC_CharacterID webhook error: " + ex.Message)); } } [HarmonyPatch(typeof(ZNet), "RPC_Disconnect")] [HarmonyPrefix] public static void ZNet_RPC_Disconnect_Prefix(ZNet __instance, ZRpc rpc) { try { if (!__instance.IsServer()) { return; } ZNetPeer peer = __instance.GetPeer(rpc); if (peer != null && peer.m_uid != 0) { string hostName = peer.m_socket.GetHostName(); if (_joinedPlayers.Remove(hostName)) { DiscordStatusTracker.RecordLeave(peer.m_playerName, hostName); DiscordIntegrationCore.OnPlayerLeave(peer.m_playerName, hostName); } } } catch (Exception ex) { Debug.LogWarning((object)("[Discord] RPC_Disconnect webhook error: " + ex.Message)); } } [HarmonyPatch(typeof(Chat), "OnNewChatMessage")] [HarmonyPrefix] public static void Chat_OnNewChatMessage_Prefix(GameObject go, long senderID, Vector3 pos, Type type, UserInfo sender, string text) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Invalid comparison between Unknown and I4 //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Invalid comparison between Unknown and I4 //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Invalid comparison between Unknown and I4 try { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && !string.IsNullOrEmpty(sender?.Name) && !string.IsNullOrEmpty(text) && !sender.Name.StartsWith("[Discord]", StringComparison.OrdinalIgnoreCase)) { if ((int)type == 2) { DiscordStatusTracker.RecordShout(); DiscordIntegrationCore.OnPlayerChat(sender.Name, text, DiscordIntegrationCore.ChatLineType.Shout); } else if ((int)type == 1) { DiscordIntegrationCore.OnPlayerChat(sender.Name, text, DiscordIntegrationCore.ChatLineType.Say); } else if ((int)type == 0) { DiscordIntegrationCore.OnPlayerChat(sender.Name, text, DiscordIntegrationCore.ChatLineType.Whisper); } } } catch (Exception ex) { Debug.LogWarning((object)("[Discord] OnNewChatMessage webhook error: " + ex.Message)); } } [HarmonyPatch(typeof(EnvMan), "UpdateTriggers")] [HarmonyPostfix] public static void EnvMan_UpdateTriggers_Postfix(float oldDayFraction, float newDayFraction) { try { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && !((Object)(object)EnvMan.instance == (Object)null) && oldDayFraction > 0.2f && oldDayFraction < 0.25f && newDayFraction > 0.25f && newDayFraction < 0.3f) { int currentDay = GetCurrentDay(); if (currentDay >= 0) { DiscordIntegrationCore.OnNewDay(currentDay); } } } catch (Exception ex) { Debug.LogWarning((object)("[Discord] UpdateTriggers webhook error: " + ex.Message)); } } private static int GetCurrentDay() { try { return EnvMan.instance.GetDay(ZNet.instance.GetTimeSeconds()); } catch { try { if (_envManGetCurrentDay == null) { _envManGetCurrentDay = typeof(EnvMan).GetMethod("GetCurrentDay", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } if (_envManGetCurrentDay != null) { return (int)_envManGetCurrentDay.Invoke(EnvMan.instance, null); } } catch { } return -1; } } [HarmonyPatch(typeof(RandEventSystem), "SetActiveEvent")] [HarmonyPostfix] public static void RandEventSystem_SetActiveEvent_Postfix(RandomEvent ___m_activeEvent) { try { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ___m_activeEvent != null) { DiscordStatusTracker.RecordEventStarted(); } } catch { } } [HarmonyPatch(typeof(ZNet), "Update")] [HarmonyPostfix] public static void ZNet_Update_Postfix(ZNet __instance) { try { if (__instance.IsServer()) { DiscordStatusTracker.Tick(); } } catch { } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] [HarmonyPostfix] public static void ZNet_OnDestroy_Postfix() { _joinedPlayers.Clear(); DiscordBotListener.Stop(); DiscordStatusTracker.Shutdown(); } } public static class DiscordMentionResolver { private enum MatchKind { Prefix, Substring } private class CoroutineHost : MonoBehaviour { } [CompilerGenerated] private sealed class <FetchRolesCoroutine>d__10 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string guildId; private string <url>5__1; private UnityWebRequest <request>5__2; private JArray <roles>5__3; private Dictionary<string, string> <map>5__4; private IEnumerator<JToken> <>s__5; private JToken <role>5__6; private string <roleName>5__7; private string <roleId>5__8; private Exception <ex>5__9; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <FetchRolesCoroutine>d__10(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <request>5__2 = null; <roles>5__3 = null; <map>5__4 = null; <>s__5 = null; <role>5__6 = null; <roleName>5__7 = null; <roleId>5__8 = null; <ex>5__9 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/guilds/" + guildId + "/roles"; <request>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <request>5__2.SetRequestHeader("Authorization", "Bot " + token); <request>5__2.timeout = 10; <>2__current = <request>5__2.SendWebRequest(); <>1__state = 1; return true; case 1: <>1__state = -3; if (<request>5__2.responseCode == 200) { try { <roles>5__3 = JArray.Parse(<request>5__2.downloadHandler.text); <map>5__4 = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); <>s__5 = <roles>5__3.GetEnumerator(); try { while (<>s__5.MoveNext()) { <role>5__6 = <>s__5.Current; <roleName>5__7 = <role>5__6.Value<string>((object)"name"); <roleId>5__8 = <role>5__6.Value<string>((object)"id"); if (!string.IsNullOrEmpty(<roleName>5__7) && !string.IsNullOrEmpty(<roleId>5__8) && !<map>5__4.ContainsKey(<roleName>5__7)) { <map>5__4[<roleName>5__7] = <roleId>5__8; } <roleName>5__7 = null; <roleId>5__8 = null; <role>5__6 = null; } } finally { if (<>s__5 != null) { <>s__5.Dispose(); } } <>s__5 = null; _roleMap = <map>5__4; _rolesFetched = true; if (<map>5__4.Count > 0) { LogVerbose($"Fetched {<map>5__4.Count} Discord roles for mention resolution"); } else { Debug.LogWarning((object)"[Discord] Role fetch succeeded but guild has no roles. @mentions will not resolve to pings."); } <roles>5__3 = null; <map>5__4 = null; } catch (Exception ex) { <ex>5__9 = ex; Debug.LogWarning((object)("[Discord] Failed to parse roles: " + <ex>5__9.Message)); _rolesFetched = false; } } else { string text = $"[Discord] Failed to fetch roles ({<request>5__2.responseCode}): "; DownloadHandler downloadHandler = <request>5__2.downloadHandler; Debug.LogWarning((object)(text + (((downloadHandler != null) ? downloadHandler.text : null) ?? <request>5__2.error))); _rolesFetched = false; } _fetchInProgress = false; <>m__Finally1(); <request>5__2 = null; return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__2 != null) { ((IDisposable)<request>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string DiscordApiBase = "https://discord.com/api/v10"; private static Dictionary<string, string> _roleMap; private static bool _rolesFetched; private static bool _fetchInProgress; private static float _lastFetchAttempt; private static MonoBehaviour _host; public static string ResolveMentions(string message) { if (string.IsNullOrEmpty(message)) { return message; } if (!message.Contains("@")) { return message; } if (!_rolesFetched && !_fetchInProgress) { FetchRoles(); } else if (_rolesFetched && (_roleMap == null || _roleMap.Count == 0) && !_fetchInProgress) { FetchRoles(); } if (_roleMap == null || _roleMap.Count == 0) { LogVerbose("@mention detected but role cache is empty \ufffd mention will not resolve. Ensure DiscordGuildId and BotToken are configured."); return message; } return Regex.Replace(message, "@(\\w+)", delegate(Match match) { string value = match.Groups[1].Value; if (string.Equals(value, "everyone", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "here", StringComparison.OrdinalIgnoreCase)) { return match.Value; } string text = ResolveRoleIdForTypedName(value); return (text != null) ? ("<@&" + text + ">") : match.Value; }); } private static string ResolveRoleIdForTypedName(string typedName) { if (_roleMap.TryGetValue(typedName, out var value)) { return value; } string text = FindShortestRoleMatching(typedName, MatchKind.Prefix); if (text != null) { return text; } return FindShortestRoleMatching(typedName, MatchKind.Substring); } private static string FindShortestRoleMatching(string typedName, MatchKind kind) { string text = null; string result = null; foreach (KeyValuePair<string, string> item in _roleMap) { string key = item.Key; if (((kind == MatchKind.Prefix) ? key.StartsWith(typedName, StringComparison.OrdinalIgnoreCase) : (key.IndexOf(typedName, StringComparison.OrdinalIgnoreCase) >= 0)) && (text == null || key.Length < text.Length)) { text = key; result = item.Value; } } return result; } public static void FetchRoles() { string token = BotTokenFile.Token; string text = DiscordIntegrationConfig.DiscordGuildId?.Value; if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(text) && !_fetchInProgress && !(Time.unscaledTime - _lastFetchAttempt < 30f)) { _fetchInProgress = true; _lastFetchAttempt = Time.unscaledTime; EnsureHost(); if (!((Object)(object)_host == (Object)null)) { _host.StartCoroutine(FetchRolesCoroutine(token, text)); } } } [IteratorStateMachine(typeof(<FetchRolesCoroutine>d__10))] private static IEnumerator FetchRolesCoroutine(string token, string guildId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <FetchRolesCoroutine>d__10(0) { token = token, guildId = guildId }; } public static void ClearCache() { _roleMap = null; _rolesFetched = false; _fetchInProgress = false; } private static void EnsureHost() { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Expected O, but got Unknown if ((Object)(object)_host != (Object)null) { return; } GameObject val = GameObject.Find("DiscordBotListenerHost"); if ((Object)(object)val != (Object)null) { _host = val.GetComponent<MonoBehaviour>(); if ((Object)(object)_host != (Object)null) { return; } } val = GameObject.Find("DiscordWebhookHost"); if ((Object)(object)val != (Object)null) { _host = val.GetComponent<MonoBehaviour>(); if ((Object)(object)_host != (Object)null) { return; } } GameObject val2 = new GameObject("DiscordMentionResolverHost"); Object.DontDestroyOnLoad((Object)(object)val2); ((Object)val2).hideFlags = (HideFlags)61; _host = (MonoBehaviour)(object)val2.AddComponent<CoroutineHost>(); } private static void LogVerbose(string msg) { if (DiscordIntegrationConfig.VerboseLogging != null && DiscordIntegrationConfig.VerboseLogging.Value) { Debug.Log((object)("[Discord] " + msg)); } } } public static class DiscordRoleColorResolver { private struct RoleColor { public int Color; public int Position; } [CompilerGenerated] private sealed class <EnsureRolesFetched>d__16 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private string <token>5__1; private string <guildId>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <EnsureRolesFetched>d__16(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <token>5__1 = null; <guildId>5__2 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (_rolesFetched) { return false; } if (_rolesFetchInProgress) { return false; } if (Time.unscaledTime - _rolesFetchLastAttempt < 30f) { return false; } <token>5__1 = BotTokenFile.Token; <guildId>5__2 = DiscordIntegrationConfig.DiscordGuildId?.Value; if (string.IsNullOrEmpty(<token>5__1) || string.IsNullOrEmpty(<guildId>5__2)) { return false; } _rolesFetchInProgress = true; _rolesFetchLastAttempt = Time.unscaledTime; <>1__state = -3; <>2__current = FetchRoles(<token>5__1, <guildId>5__2); <>1__state = 1; return true; case 1: <>1__state = -3; <>m__Finally1(); return false; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; _rolesFetchInProgress = false; } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <EnsureUserFetched>d__14 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string userId; private string <token>5__1; private string <guildId>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <EnsureUserFetched>d__14(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <token>5__1 = null; <guildId>5__2 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (string.IsNullOrEmpty(userId)) { return false; } if (_userColorHexById.ContainsKey(userId)) { return false; } <>2__current = EnsureRolesFetched(); <>1__state = 1; return true; case 1: <>1__state = -1; if (!_rolesFetched || _rolesById == null) { return false; } <token>5__1 = BotTokenFile.Token; <guildId>5__2 = DiscordIntegrationConfig.DiscordGuildId?.Value; if (string.IsNullOrEmpty(<token>5__1) || string.IsNullOrEmpty(<guildId>5__2)) { return false; } <>2__current = FetchMember(<token>5__1, <guildId>5__2, userId); <>1__state = 2; return true; case 2: <>1__state = -1; return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <FetchMember>d__18 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string guildId; public string userId; private string <url>5__1; private UnityWebRequest <req>5__2; private JObject <member>5__3; private JArray <roleIds>5__4; private Exception <ex>5__5; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <FetchMember>d__18(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <req>5__2 = null; <member>5__3 = null; <roleIds>5__4 = null; <ex>5__5 = null; <>1__state = -2; } private bool MoveNext() { bool result; try { switch (<>1__state) { default: result = false; break; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/guilds/" + guildId + "/members/" + userId; <req>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <req>5__2.SetRequestHeader("Authorization", "Bot " + token); <req>5__2.timeout = 10; <>2__current = <req>5__2.SendWebRequest(); <>1__state = 1; result = true; break; case 1: { <>1__state = -3; if (<req>5__2.responseCode != 200) { string text = $"[Discord] RoleColor: member {userId} fetch failed ({<req>5__2.responseCode}): "; DownloadHandler downloadHandler = <req>5__2.downloadHandler; Debug.LogWarning((object)(text + (((downloadHandler != null) ? downloadHandler.text : null) ?? <req>5__2.error))); result = false; goto IL_01ce; } try { <member>5__3 = JObject.Parse(<req>5__2.downloadHandler.text); } catch (Exception ex) { <ex>5__5 = ex; Debug.LogWarning((object)("[Discord] RoleColor: member parse failed: " + <ex>5__5.Message)); result = false; goto IL_01ce; } ref JArray reference = ref <roleIds>5__4; JToken obj = <member>5__3["roles"]; reference = (JArray)(object)((obj is JArray) ? obj : null); _userColorHexById[userId] = ((<roleIds>5__4 == null) ? null : ComputeDisplayColorHex(<roleIds>5__4)); <member>5__3 = null; <roleIds>5__4 = null; <>m__Finally1(); <req>5__2 = null; result = false; break; } IL_01ce: <>m__Finally1(); break; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__2 != null) { ((IDisposable)<req>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <FetchRoles>d__17 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string token; public string guildId; private string <url>5__1; private UnityWebRequest <req>5__2; private JArray <rolesJson>5__3; private Dictionary<string, RoleColor> <map>5__4; private Exception <ex>5__5; private IEnumerator<JToken> <>s__6; private JToken <role>5__7; private string <id>5__8; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <FetchRoles>d__17(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <url>5__1 = null; <req>5__2 = null; <rolesJson>5__3 = null; <map>5__4 = null; <ex>5__5 = null; <>s__6 = null; <role>5__7 = null; <id>5__8 = null; <>1__state = -2; } private bool MoveNext() { bool result; try { switch (<>1__state) { default: result = false; break; case 0: <>1__state = -1; <url>5__1 = "https://discord.com/api/v10/guilds/" + guildId + "/roles"; <req>5__2 = UnityWebRequest.Get(<url>5__1); <>1__state = -3; <req>5__2.SetRequestHeader("Authorization", "Bot " + token); <req>5__2.timeout = 10; <>2__current = <req>5__2.SendWebRequest(); <>1__state = 1; result = true; break; case 1: { <>1__state = -3; if (<req>5__2.responseCode != 200) { string text = $"[Discord] RoleColor: roles fetch failed ({<req>5__2.responseCode}): "; DownloadHandler downloadHandler = <req>5__2.downloadHandler; Debug.LogWarning((object)(text + (((downloadHandler != null) ? downloadHandler.text : null) ?? <req>5__2.error))); result = false; goto IL_028f; } try { <rolesJson>5__3 = JArray.Parse(<req>5__2.downloadHandler.text); } catch (Exception ex) { <ex>5__5 = ex; Debug.LogWarning((object)("[Discord] RoleColor: roles parse failed: " + <ex>5__5.Message)); result = false; goto IL_028f; } <map>5__4 = new Dictionary<string, RoleColor>(StringComparer.Ordinal); <>s__6 = <rolesJson>5__3.GetEnumerator(); try { while (<>s__6.MoveNext()) { <role>5__7 = <>s__6.Current; <id>5__8 = <role>5__7.Value<string>((object)"id"); if (!string.IsNullOrEmpty(<id>5__8)) { <map>5__4[<id>5__8] = new RoleColor { Color = <role>5__7.Value<int?>((object)"color").GetValueOrDefault(), Position = <role>5__7.Value<int?>((object)"position").GetValueOrDefault() }; <id>5__8 = null; <role>5__7 = null; } } } finally { if (<>s__6 != null) { <>s__6.Dispose(); } } <>s__6 = null; _rolesById = <map>5__4; _rolesFetched = true; <rolesJson>5__3 = null; <map>5__4 = null; <>m__Finally1(); <req>5__2 = null; result = false; break; } IL_028f: <>m__Finally1(); break; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__2 != null) { ((IDisposable)<req>5__2).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string DiscordApiBase = "https://discord.com/api/v10"; private const int DiscordRestTimeoutSec = 10; private const int HttpOk = 200; private const int NoColorSentinel = 0; private const float RolesFetchRetryCooldownSec = 30f; private const string ColorHexFormat = "#{0:X6}"; private static Dictionary<string, RoleColor> _rolesById; private static bool _rolesFetched; private static bool _rolesFetchInProgress; private static float _rolesFetchLastAttempt; private static readonly Dictionary<string, string> _userColorHexById = new Dictionary<string, string>(StringComparer.Ordinal); private static MonoBehaviour _host; public static bool TryGetUserColorHex(string userId, out string hex) { hex = null; if (string.IsNullOrEmpty(userId)) { return false; } return _userColorHexById.TryGetValue(userId, out hex); } [IteratorStateMachine(typeof(<EnsureUserFetched>d__14))] public static IEnumerator EnsureUserFetched(string userId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <EnsureUserFetched>d__14(0) { userId = userId }; } public static void ClearCache() { _rolesById = null; _rolesFetched = false; _rolesFetchInProgress = false; _rolesFetchLastAttempt = 0f; _userColorHexById.Clear(); } [IteratorStateMachine(typeof(<EnsureRolesFetched>d__16))] private static IEnumerator EnsureRolesFetched() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <EnsureRolesFetched>d__16(0); } [IteratorStateMachine(typeof(<FetchRoles>d__17))] private static IEnumerator FetchRoles(string token, string guildId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <FetchRoles>d__17(0) { token = token, guildId = guildId }; } [IteratorStateMachine(typeof(<FetchMember>d__18))] private static IEnumerator FetchMember(string token, string guildId, string userId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <FetchMember>d__18(0) { token = token, guildId = guildId, userId = userId }; } private static string ComputeDisplayColorHex(JArray memberRoleIds) { int num = -1; int num2 = 0; foreach (JToken memberRoleId in memberRoleIds) { string text = ((object)memberRoleId)?.ToString(); if (!string.IsNullOrEmpty(text) && _rolesById.TryGetValue(text, out var value) && value.Color != 0 && value.Position > num) { num = value.Position; num2 = value.Color; } } return (num < 0) ? null : $"#{num2:X6}"; } } public static class DiscordStatusTracker { private static int _joins; private static int _leaves; private static int _deaths; private static int _shouts; private static int _eventsStarted; private static int _worldSaves; private static DateTime _serverStartTimeUtc; private static DateTime _lastReportTimeUtc; private static DateTime _lastHeartbeatTimeUtc; private static int _peakPlayersThisHour; private static int _totalJoinsSession; private static int _totalDeathsSession; private const int EasternStandardUtcOffsetHours = -5; private static readonly TimeSpan EasternOffset = TimeSpan.FromHours(-5.0); private const double HourlyReportIntervalMinutes = 60.0; private const int DailyReportHourEst = 0; private static int _dailyJoins; private static int _dailyLeaves; private static int _dailyDeaths; private static int _dailyShouts; private static int _dailyEvents; private static int _dailyWorldSaves; private static int _dailyPeakPlayers; private static double _dailyPlayMinutes; private static readonly List<string> _dailyPlayerNames = new List<string>(); private static int _lastDailyReportEstDay = -1; private static readonly Dictionary<string, DateTime> _activeSessions = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase); private static double _playMinutesThisHour; private static readonly List<string> _playerNamesThisHour = new List<string>(); public static void Initialize() { _serverStartTimeUtc = DateTime.UtcNow; _lastReportTimeUtc = DateTime.UtcNow; _lastHeartbeatTimeUtc = DateTime.UtcNow; _lastDailyReportEstDay = GetEstNow().Day; ResetHourlyCounters(); ResetDailyCounters(); } public static void Shutdown() { _activeSessions.Clear(); _playerNamesThisHour.Clear(); _dailyPlayerNames.Clear(); ResetHourlyCounters(); ResetDailyCounters(); } public static void RecordJoin(string playerName, string platformId) { _joins++; _totalJoinsSession++; _dailyJoins++; if (!string.IsNullOrEmpty(playerName) && !_playerNamesThisHour.Contains(playerName)) { _playerNamesThisHour.Add(playerName); } if (!string.IsNullOrEmpty(playerName) && !_dailyPlayerNames.Contains(playerName)) { _dailyPlayerNames.Add(playerName); } if (!string.IsNullOrEmpty(platformId)) { _activeSessions[platformId] = DateTime.UtcNow; } UpdatePeakPlayers(); } public static void RecordLeave(string playerName, string platformId) { _leaves++; _dailyLeaves++; if (!string.IsNullOrEmpty(platformId) && _activeSessions.TryGetValue(platformId, out var value)) { double totalMinutes = (DateTime.UtcNow - value).TotalMinutes; _playMinutesThisHour += totalMinutes; _dailyPlayMinutes += totalMinutes; _activeSessions.Remove(platformId); } } public static void RecordDeath() { _deaths++; _totalDeathsSession++; _dailyDeaths++; } public static void RecordShout() { _shouts++; _dailyShouts++; } public static void RecordEventStarted() { _eventsStarted++; _dailyEvents++; } public static void RecordWorldSave() { _worldSaves++; _dailyWorldSaves++; } public static void Tick() { if ((DateTime.UtcNow - _lastReportTimeUtc).TotalMinutes >= 60.0) { SendHourlySummary(); _lastReportTimeUtc = DateTime.UtcNow; } DateTime estNow = GetEstNow(); if (estNow.Hour == 0 && estNow.Day != _lastDailyReportEstDay) { SendDailySummary(estNow); _lastDailyReportEstDay = estNow.Day; } } private static void SendStatusHeartbeat() { if (!DiscordIntegrationCore.IsActive()) { return; } ConfigEntry<bool> enableStatusHeartbeat = DiscordIntegrationConfig.EnableStatusHeartbeat; if (enableStatusHeartbeat == null || !enableStatusHeartbeat.Value) { return; } try { TimeSpan ts = DateTime.UtcNow - _serverStartTimeUtc; int currentPlayerCount = GetCurrentPlayerCount(); int inGameDay = GetInGameDay(); DiscordEmbed discordEmbed = new DiscordEmbed().SetTitle("?? Server Status").SetColor((currentPlayerCount > 0) ? 5763719 : 16776960).AddField("Server", GetServerName(), inline: true) .AddField("Uptime", FormatDuration(ts), inline: true) .AddField("Players Online", currentPlayerCount.ToString(), inline: true); if (inGameDay >= 0) { discordEmbed.AddField("In-Game Day", inGameDay.ToString(), inline: true); } string onlinePlayerList = GetOnlinePlayerList(); if (!string.IsNullOrEmpty(onlinePlayerList)) { discordEmbed.AddField("Currently Online", onlinePlayerList); } else { discordEmbed.AddField("Currently Online", "*No players connected*"); } discordEmbed.SetFooter($"Last updated: {DateTime.Now:HH:mm:ss}"); string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); string username = DiscordIntegrationConfig.WebhookDisplayName?.Value ?? "Valheim Server"; string text = DiscordIntegrationConfig.WebhookAvatarUrl?.Value; DiscordWebhookService.SendEmbed(webhookUrl, discordEmbed, username, string.IsNullOrEmpty(text) ? null : text); LogVerbose($"Sent status heartbeat: uptime={FormatDuration(ts)}, online={currentPlayerCount}"); TriggerOldMessageCleanup(); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] Failed to send status heartbeat: " + ex.Message)); } } private static void TriggerOldMessageCleanup() { ConfigEntry<bool> enableBotListener = DiscordIntegrationConfig.EnableBotListener; if (enableBotListener != null && enableBotListener.Value) { string token = BotTokenFile.Token; string text = DiscordIntegrationConfig.StatusChannelId?.Value; if (string.IsNullOrEmpty(text)) { text = DiscordIntegrationConfig.ChatChannelId?.Value; } if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(text)) { DiscordBotListener.TriggerMessageCleanup(text, TimeSpan.FromHours(24.0)); } } } private static void SendHourlySummary() { if (!DiscordIntegrationCore.IsActive()) { return; } ConfigEntry<bool> notifyHourlySummary = DiscordIntegrationConfig.NotifyHourlySummary; if (notifyHourlySummary == null || !notifyHourlySummary.Value) { return; } try { double num = 0.0; foreach (KeyValuePair<string, DateTime> activeSession in _activeSessions) { num += (DateTime.UtcNow - activeSession.Value).TotalMinutes; } double num2 = _playMinutesThisHour + num; TimeSpan ts = DateTime.UtcNow - _serverStartTimeUtc; int currentPlayerCount = GetCurrentPlayerCount(); DiscordEmbed discordEmbed = new DiscordEmbed().SetTitle("\ud83d\udcca Hourly Server Report").SetColor(3447003).AddField("Uptime", FormatDuration(ts), inline: true) .AddField("Online Now", currentPlayerCount.ToString(), inline: true) .AddField("Peak This Hour", _peakPlayersThisHour.ToString(), inline: true) .AddField("Joins", _joins.ToString(), inline: true) .AddField("Leaves", _leaves.ToString(), inline: true) .AddField("Deaths", _deaths.ToString(), inline: true) .AddField("Shouts", _shouts.ToString(), inline: true) .AddField("Events", _eventsStarted.ToString(), inline: true) .AddField("World Saves", _worldSaves.ToString(), inline: true); if (num2 > 0.0) { discordEmbed.AddField("Total Play Time", FormatDuration(TimeSpan.FromMinutes(num2)), inline: true); } discordEmbed.AddField("Session Joins", _totalJoinsSession.ToString(), inline: true); discordEmbed.AddField("Session Deaths", _totalDeathsSession.ToString(), inline: true); if (_playerNamesThisHour.Count > 0) { string text = string.Join(", ", _playerNamesThisHour.Take(20)); if (_playerNamesThisHour.Count > 20) { text += $" (+{_playerNamesThisHour.Count - 20} more)"; } discordEmbed.AddField("Active Players This Hour", text); } string onlinePlayerList = GetOnlinePlayerList(); if (!string.IsNullOrEmpty(onlinePlayerList)) { discordEmbed.AddField("Currently Online", onlinePlayerList); } int inGameDay = GetInGameDay(); if (inGameDay >= 0) { discordEmbed.AddField("In-Game Day", inGameDay.ToString(), inline: true); } string serverName = GetServerName(); discordEmbed.SetFooter(serverName + " • Next report in 1 hour"); string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); ResolveBotIdentity(out var username, out var avatarUrl); DiscordWebhookService.SendEmbed(webhookUrl, discordEmbed, username, avatarUrl); LogVerbose($"Sent hourly summary: {_joins}j/{_leaves}l/{_deaths}d, peak={_peakPlayersThisHour}, online={currentPlayerCount}"); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] Failed to send hourly summary: " + ex.Message)); } ResetHourlyCounters(); } private static void SendDailySummary(DateTime estNow) { if (!DiscordIntegrationCore.IsActive()) { return; } ConfigEntry<bool> notifyDailySummary = DiscordIntegrationConfig.NotifyDailySummary; if (notifyDailySummary == null || !notifyDailySummary.Value) { return; } try { double num = 0.0; foreach (KeyValuePair<string, DateTime> activeSession in _activeSessions) { num += (DateTime.UtcNow - activeSession.Value).TotalMinutes; } double num2 = _dailyPlayMinutes + num; TimeSpan ts = DateTime.UtcNow - _serverStartTimeUtc; int currentPlayerCount = GetCurrentPlayerCount(); string text = estNow.AddDays(-1.0).ToString("MMMM d, yyyy"); DiscordEmbed discordEmbed = new DiscordEmbed().SetTitle("\ud83d\udcc5 Daily Server Report \ufffd " + text).SetColor(10181046).AddField("Server Uptime", FormatDuration(ts), inline: true) .AddField("Online Now", currentPlayerCount.ToString(), inline: true) .AddField("Peak Players", _dailyPeakPlayers.ToString(), inline: true) .AddField("Joins", _dailyJoins.ToString(), inline: true) .AddField("Leaves", _dailyLeaves.ToString(), inline: true) .AddField("Deaths", _dailyDeaths.ToString(), inline: true) .AddField("Shouts", _dailyShouts.ToString(), inline: true) .AddField("Raids", _dailyEvents.ToString(), inline: true) .AddField("World Saves", _dailyWorldSaves.ToString(), inline: true); if (num2 > 0.0) { discordEmbed.AddField("Total Play Time", FormatDuration(TimeSpan.FromMinutes(num2)), inline: true); } discordEmbed.AddField("Session Joins", _totalJoinsSession.ToString(), inline: true); discordEmbed.AddField("Session Deaths", _totalDeathsSession.ToString(), inline: true); if (_dailyPlayerNames.Count > 0) { string text2 = string.Join(", ", _dailyPlayerNames.Take(30)); if (_dailyPlayerNames.Count > 30) { text2 += $" (+{_dailyPlayerNames.Count - 30} more)"; } discordEmbed.AddField($"Players Active Today ({_dailyPlayerNames.Count})", text2); } string onlinePlayerList = GetOnlinePlayerList(); if (!string.IsNullOrEmpty(onlinePlayerList)) { discordEmbed.AddField("Currently Online", onlinePlayerList); } int inGameDay = GetInGameDay(); if (inGameDay >= 0) { discordEmbed.AddField("In-Game Day", inGameDay.ToString(), inline: true); } discordEmbed.SetFooter(GetServerName() + " • Next daily report in ~24 hours (midnight EST)"); string webhookUrl = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.General); ResolveBotIdentity(out var username, out var avatarUrl); DiscordWebhookService.SendEmbed(webhookUrl, discordEmbed, username, avatarUrl); LogVerbose("Sent daily summary for " + text + ": " + $"{_dailyJoins}j/{_dailyLeaves}l/{_dailyDeaths}d, " + $"peak={_dailyPeakPlayers}, online={currentPlayerCount}"); } catch (Exception ex) { Debug.LogWarning((object)("[Discord] Failed to send daily summary: " + ex.Message)); } ResetDailyCounters(); } private static void ResolveBotIdentity(out string username, out string avatarUrl) { string text = DiscordIntegrationConfig.WebhookDisplayName?.Value; username = ((!string.IsNullOrEmpty(text)) ? text : DiscordBotIdentity.ResolveUsername("Valheim Server")); string text2 = DiscordIntegrationConfig.WebhookAvatarUrl?.Value; avatarUrl = ((!string.IsNullOrEmpty(text2)) ? text2 : DiscordBotIdentity.ResolveAvatarUrl(null)); } private static void ResetDailyCounters() { _dailyJoins = 0; _dailyLeaves = 0; _dailyDeaths = 0; _dailyShouts = 0; _dailyEvents = 0; _dailyWorldSaves = 0; _dailyPeakPlayers = GetCurrentPlayerCount(); _dailyPlayMinutes = 0.0; _dailyPlayerNames.Clear(); if (!((Object)(object)ZNet.instance != (Object)null)) { return; } List<ZNetPeer> peers = ZNet.instance.GetPeers(); if (peers == null) { return; } foreach (ZNetPeer item in peers) { if (!string.IsNullOrEmpty(item?.m_playerName)) { _dailyPlayerNames.Add(item.m_playerName); } } } private static DateTime GetEstNow() { return DateTime.UtcNow + EasternOffset; } private static void ResetHourlyCounters() { _joins = 0; _leaves = 0; _deaths = 0; _shouts = 0; _eventsStarted = 0; _worldSaves = 0; _peakPlayersThisHour = GetCurrentPlayerCount(); _playMinutesThisHour = 0.0; _playerNamesThisHour.Clear(); List<string> list = _activeSessions.Keys.ToList(); DateTime utcNow = DateTime.UtcNow; foreach (string item in list) { _activeSessions[item] = utcNow; } } private static void UpdatePeakPlayers() { int currentPlayerCount = GetCurrentPlayerCount(); if (currentPlayerCount > _peakPlayersThisHour) { _peakPlayersThisHour = currentPlayerCount; } if (currentPlayerCount > _dailyPeakPlayers) { _dailyPeakPlayers = currentPlayerCount; } } private static int GetCurrentPlayerCount() { try { ZNet instance = ZNet.instance; return ((instance == null) ? null : instance.GetPeers()?.Count).GetValueOrDefault(); } catch { return 0; } } private static string GetOnlinePlayerList() { try { ZNet instance = ZNet.instance; List<ZNetPeer> list = ((instance != null) ? instance.GetPeers() : null); if (list == null || list.Count == 0) { return null; } List<string> list2 = new List<string>(); foreach (ZNetPeer item in list) { if (!string.IsNullOrEmpty(item?.m_playerName)) { list2.Add(item.m_playerName); } } if (list2.Count == 0) { return null; } string text = string.Join(", ", list2.Take(20)); if (list2.Count > 20) { text += $" (+{list2.Count - 20} more)"; } return text; } catch { return null; } } private static int GetInGameDay() { try { if ((Object)(object)EnvMan.instance == (Object)null || (Object)(object)ZNet.instance == (Object)null) { return -1; } return EnvMan.instance.GetDay(ZNet.instance.GetTimeSeconds()); } catch { return -1; } } private static string GetServerName() { return ServerNameResolver.GetOperatorServerName(); } private static string FormatDuration(TimeSpan ts) { if (ts.TotalDays >= 1.0) { return $"{(int)ts.TotalDays}d {ts.Hours}h {ts.Minutes}m"; } if (ts.TotalHours >= 1.0) { return $"{(int)ts.TotalHours}h {ts.Minutes}m"; } return $"{ts.Minutes}m"; } private static void LogVerbose(string msg) { if (DiscordIntegrationConfig.VerboseLogging != null && DiscordIntegrationConfig.VerboseLogging.Value) { Debug.Log((object)("[Discord] " + msg)); } } } public static class DiscordWebhookService { private class CoroutineHost : MonoBehaviour { } [CompilerGenerated] private sealed class <PostCoroutine>d__11 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string webhookUrl; public Dictionary<string, object> payload; public Action onComplete; private string <json>5__1; private byte[] <bodyRaw>5__2; private float <wait>5__3; private Exception <ex>5__4; private UnityWebRequest <request>5__5; private long <code>5__6; private string <body>5__7; private Dictionary<string, object> <rateLimitInfo>5__8; private double <retryAfter>5__9; private string <body>5__10; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PostCoroutine>d__11(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 2) { try { } finally { <>m__Finally1(); } } <json>5__1 = null; <bodyRaw>5__2 = null; <ex>5__4 = null; <request>5__5 = null; <body>5__7 = null; <rateLimitInfo>5__8 = null; <body>5__10 = null; <>1__state = -2; } private bool MoveNext() { //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Expected O, but got Unknown //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Expected O, but got Unknown //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Expected O, but got Unknown //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Expected O, but got Unknown //IL_02d2: Unknown result type (might be due to invalid IL or missing references) //IL_02d8: Invalid comparison between Unknown and I4 //IL_02e0: Unknown result type (might be due to invalid IL or missing references) //IL_02e6: Invalid comparison between Unknown and I4 try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (Time.unscaledTime < _rateLimitUntil) { <wait>5__3 = _rateLimitUntil - Time.unscaledTime; if (<wait>5__3 > 0f && <wait>5__3 < 30f) { <>2__current = (object)new WaitForSecondsRealtime(<wait>5__3); <>1__state = 1; return true; } } goto IL_009e; case 1: <>1__state = -1; goto IL_009e; case 2: { <>1__state = -3; <code>5__6 = <request>5__5.responseCode; if (<code>5__6 >= 200 && <code>5__6 < 300) { LogVerbose("Webhook sent successfully"); } else if (<code>5__6 == 429) { DownloadHandler downloadHandler = <request>5__5.downloadHandler; <body>5__7 = ((downloadHandler != null) ? downloadHandler.text : null) ?? ""; LogVerbose("Rate limited by Discord: " + <body>5__7); try { <rateLimitInfo>5__8 = JsonConvert.DeserializeObject<Dictionary<string, object>>(<body>5__7); if (<rateLimitInfo>5__8 != null && <rateLimitInfo>5__8.ContainsKey("retry_after")) { <retryAfter>5__9 = Convert.ToDouble(<rateLimitInfo>5__8["retry_after"]); _rateLimitUntil = Time.unscaledTime + (float)<retryAfter>5__9 + 0.5f; } else { _rateLimitUntil = Time.unscaledTime + 5f; } <rateLimitInfo>5__8 = null; } catch { _rateLimitUntil = Time.unscaledTime + 5f; } <body>5__7 = null; } else if ((int)<request>5__5.result == 2 || (int)<request>5__5.result == 3) { Debug.LogWarning((object)$"[Discord] Webhook failed ({<code>5__6}): {<request>5__5.error}"); } else if (<code>5__6 > 0) { DownloadHandler downloadHandler2 = <request>5__5.downloadHandler; <body>5__10 = ((downloadHandler2 != null) ? downloadHandler2.text : null) ?? <request>5__5.error ?? "unknown"; Debug.LogWarning((object)$"[Discord] Webhook failed ({<code>5__6}): {<body>5__10}"); <body>5__10 = null; } <>m__Finally1(); <request>5__5 = null; onComplete?.Invoke(); return false; } IL_009e: try { <json>5__1 = JsonConvert.SerializeObject((object)payload); } catch (Exception ex) { <ex>5__4 = ex; Debug.LogWarning((object)("[Discord] JSON serialize error: " + <ex>5__4.Message)); return false; } <bodyRaw>5__2 = Encoding.UTF8.GetBytes(<json>5__1); <request>5__5 = new UnityWebRequest(webhookUrl, "POST"); <>1__state = -3; <request>5__5.uploadHandler = (UploadHandler)new UploadHandlerRaw(<bodyRaw>5__2); <request>5__5.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); <request>5__5.SetRequestHeader("Content-Type", "application/json"); <request>5__5.timeout = 10; <>2__current = <request>5__5.SendWebRequest(); <>1__state = 2; return true; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__5 != null) { ((IDisposable)<request>5__5).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PostMultipartCoroutine>d__12 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string webhookUrl; public DiscordEmbed embed; public List<DiscordFileAttachment> files; public string username; public string avatarUrl; public Action onComplete; private string <payloadJson>5__1; private List<IMultipartFormSection> <form>5__2; private float <wait>5__3; private Dictionary<string, object> <payload>5__4; private Exception <ex>5__5; private int <i>5__6; private DiscordFileAttachment <file>5__7; private UnityWebRequest <request>5__8; private long <code>5__9; private string <body>5__10; private Dictionary<string, object> <rateLimitInfo>5__11; private double <retryAfter>5__12; private string <body>5__13; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PostMultipartCoroutine>d__12(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 2) { try { } finally { <>m__Finally1(); } } <payloadJson>5__1 = null; <form>5__2 = null; <payload>5__4 = null; <ex>5__5 = null; <file>5__7 = null; <request>5__8 = null; <body>5__10 = null; <rateLimitInfo>5__11 = null; <body>5__13 = null; <>1__state = -2; } private bool MoveNext() { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Expected O, but got Unknown //IL_0426: Unknown result type (might be due to invalid IL or missing references) //IL_042c: Invalid comparison between Unknown and I4 //IL_0434: Unknown result type (might be due to invalid IL or missing references) //IL_043a: Invalid comparison between Unknown and I4 //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Expected O, but got Unknown //IL_0244: Unknown result type (might be due to invalid IL or missing references) //IL_024e: Expected O, but got Unknown try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (Time.unscaledTime < _rateLimitUntil) { <wait>5__3 = _rateLimitUntil - Time.unscaledTime; if (<wait>5__3 > 0f && <wait>5__3 < 30f) { <>2__current = (object)new WaitForSecondsRealtime(<wait>5__3); <>1__state = 1; return true; } } goto IL_009e; case 1: <>1__state = -1; goto IL_009e; case 2: { <>1__state = -3; <code>5__9 = <request>5__8.responseCode; if (<code>5__9 >= 200 && <code>5__9 < 300) { LogVerbose("Webhook (multipart) sent successfully"); } else if (<code>5__9 == 429) { DownloadHandler downloadHandler = <request>5__8.downloadHandler; <body>5__10 = ((downloadHandler != null) ? downloadHandler.text : null) ?? ""; LogVerbose("Rate limited by Discord (multipart): " + <body>5__10); try { <rateLimitInfo>5__11 = JsonConvert.DeserializeObject<Dictionary<string, object>>(<body>5__10); if (<rateLimitInfo>5__11 != null && <rateLimitInfo>5__11.ContainsKey("retry_after")) { <retryAfter>5__12 = Convert.ToDouble(<rateLimitInfo>5__11["retry_after"]); _rateLimitUntil = Time.unscaledTime + (float)<retryAfter>5__12 + 0.5f; } else { _rateLimitUntil = Time.unscaledTime + 5f; } <rateLimitInfo>5__11 = null; } catch { _rateLimitUntil = Time.unscaledTime + 5f; } <body>5__10 = null; } else if ((int)<request>5__8.result == 2 || (int)<request>5__8.result == 3) { Debug.LogWarning((object)$"[Discord] Webhook (multipart) failed ({<code>5__9}): {<request>5__8.error}"); } else if (<code>5__9 > 0) { DownloadHandler downloadHandler2 = <request>5__8.downloadHandler; <body>5__13 = ((downloadHandler2 != null) ? downloadHandler2.text : null) ?? <request>5__8.error ?? "unknown"; Debug.LogWarning((object)$"[Discord] Webhook (multipart) failed ({<code>5__9}): {<body>5__13}"); <body>5__13 = null; } <>m__Finally1(); <request>5__8 = null; onComplete?.Invoke(); return false; } IL_009e: try { <payload>5__4 = new Dictionary<string, object>(); if (embed != null) { <payload>5__4["embeds"] = new List<Dictionary<string, object>> { embed.ToDictionary() }; } if (!string.IsNullOrEmpty(username)) { <payload>5__4["username"] = username; } if (!string.IsNullOrEmpty(avatarUrl)) { <payload>5__4["avatar_url"] = avatarUrl; } <payloadJson>5__1 = JsonConvert.SerializeObject((object)<payload>5__4); <payload>5__4 = null; } catch (Exception ex) { <ex>5__5 = ex; Debug.LogWarning((object)("[Discord] JSON serialize error (multipart): " + <ex>5__5.Message)); return false; } <form>5__2 = new List<IMultipartFormSection>(); <form>5__2.Add((IMultipartFormSection)new MultipartFormDataSection("payload_json", <payloadJson>5__1, "application/json")); if (files != null) { <i>5__6 = 0; while (<i>5__6 < files.Count) { <file>5__7 = files[<i>5__6]; if (<file>5__7.Content != null && <file>5__7.Content.Length != 0) { <form>5__2.Add((IMultipartFormSection)new MultipartFormFileSection($"files[{<i>5__6}]", <file>5__7.Content, <file>5__7.FileName, <file>5__7.ContentType)); } <file>5__7 = null; <i>5__6++; } } <request>5__8 = UnityWebRequest.Post(webhookUrl, <form>5__2); <>1__state = -3; <request>5__8.timeout = 30; <>2__current = <request>5__8.SendWebRequest(); <>1__state = 2; return true; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__8 != null) { ((IDisposable)<request>5__8).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private static float _rateLimitUntil; private static CoroutineHost _host; private const int MaxContentLength = 1990; public static bool IsValidWebhookUrl(string url) { if (string.IsNullOrEmpty(url) || !url.StartsWith("https://")) { return false; } return url.Contains("discord.com/api/webhooks/") || url.Contains("discordapp.com/api/webhooks/"); } public static void SendMessage(string webhookUrl, string content, string username = null, string avatarUrl = null) { if (!IsValidWebhookUrl(webhookUrl) || string.IsNullOrEmpty(content)) { return; } List<string> list = SplitMessage(content, 1990); foreach (string item in list) { Dictionary<string, object> dictionary = new Dictionary<string, object> { { "content", item }, { "allowed_mentions", new Dictionary<string, object> { { "parse", new List<string> { "roles", "users" } } } } }; if (!string.IsNullOrEmpty(username)) { dictionary["username"] = username; } if (!string.IsNullOrEmpty(avatarUrl)) { dictionary["avatar_url"] = avatarUrl; } EnqueuePost(webhookUrl, dictionary); } } private static List<string> SplitMessage(string content, int maxLen) { List<string> list = new List<string>(); if (content.Length <= maxLen) { list.Add(content); return list; } int num = 0; while (num < content.Length) { int num2 = content.Length - num; if (num2 <= maxLen) { list.Add(content.Substring(num)); break; } int num3 = -1; int num4 = num + maxLen; int num5 = content.LastIndexOf('\n', num4 - 1, maxLen); if (num5 > num) { num3 = num5 + 1; } if (num3 < 0) { int num6 = content.LastIndexOf(' ', num4 - 1, maxLen); if (num6 > num) { num3 = num6 + 1; } } if (num3 <= num) { num3 = num4; } list.Add(content.Substring(num, num3 - num)); num = num3; } return list; } public static void SendEmbed(string webhookUrl, DiscordEmbed embed, string username = null, string avatarUrl = null) { SendEmbed(webhookUrl, embed, username, avatarUrl, null); } public static void SendEmbed(string webhookUrl, DiscordEmbed embed, string username, string avatarUrl, Action onComplete) { if (!IsValidWebhookUrl(webhookUrl)) { onComplete?.Invoke(); return; } if (embed == null) { onComplete?.Invoke(); return; } Dictionary<string, object> item = embed.ToDictionary(); Dictionary<string, object> dictionary = new Dictionary<string, object> { { "embeds", new List<Dictionary<string, object>> { item } } }; if (!string.IsNullOrEmpty(username)) { dictionary["username"] = username; } if (!string.IsNullOrEmpty(avatarUrl)) { dictionary["avatar_url"] = avatarUrl; } EnqueuePost(webhookUrl, dictionary, onComplete); } public static void SendEmbedWithFiles(string webhookUrl, DiscordEmbed embed, List<DiscordFileAttachment> files, string username = null, string avatarUrl = null) { SendEmbedWithFiles(webhookUrl, embed, files, username, avatarUrl, null); } public static void SendEmbedWithFiles(string webhookUrl, DiscordEmbed embed, List<DiscordFileAttachment> files, string username, string avatarUrl, Action onComplete) { if (!IsValidWebhookUrl(webhookUrl)) { onComplete?.Invoke(); return; } if (embed == null && (files == null || files.Count == 0)) { onComplete?.Invoke(); return; } EnsureHost(); if ((Object)(object)_host == (Object)null) { onComplete?.Invoke(); } else { ((MonoBehaviour)_host).StartCoroutine(PostMultipartCoroutine(webhookUrl, embed, files, username, avatarUrl, onComplete)); } } private static void EnqueuePost(string webhookUrl, Dictionary<string, object> payload, Action onComplete = null) { EnsureHost(); if ((Object)(object)_host == (Object)null) { onComplete?.Invoke(); } else { ((MonoBehaviour)_host).StartCoroutine(PostCoroutine(webhookUrl, payload, onComplete)); } } [IteratorStateMachine(typeof(<PostCoroutine>d__11))] private static IEnumerator PostCoroutine(string webhookUrl, Dictionary<string, object> payload, Action onComplete = null) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PostCoroutine>d__11(0) { webhookUrl = webhookUrl, payload = payload, onComplete = onComplete }; } [IteratorStateMachine(typeof(<PostMultipartCoroutine>d__12))] private static IEnumerator PostMultipartCoroutine(string webhookUrl, DiscordEmbed embed, List<DiscordFileAttachment> files, string username, string avatarUrl, Action onComplete = null) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PostMultipartCoroutine>d__12(0) { webhookUrl = webhookUrl, embed = embed, files = files, username = username, avatarUrl = avatarUrl, onComplete = onComplete }; } private static void EnsureHost() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown if (!((Object)(object)_host != (Object)null)) { GameObject val = new GameObject("DiscordWebhookHost"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; _host = val.AddComponent<CoroutineHost>(); } } private static void LogVerbose(string msg) { if (DiscordIntegrationConfig.VerboseLogging != null && DiscordIntegrationConfig.VerboseLogging.Value) { Debug.Log((object)("[Discord] " + msg)); } } } public class DiscordEmbed { public struct EmbedField { public string Name; public string Value; public bool Inline; } public string Title; public string Description; public int Color = 3447003; public string AuthorName; public string AuthorIconUrl; public string FooterText; public string ThumbnailUrl; public string ImageUrl; public List<EmbedField> Fields = new List<EmbedField>(); public DiscordEmbed SetTitle(string title) { Title = title; return this; } public DiscordEmbed SetDescription(string desc) { Description = desc; return this; } public DiscordEmbed SetColor(int color) { Color = color; return this; } public DiscordEmbed SetAuthor(string name, string iconUrl = null) { AuthorName = name; AuthorIconUrl = iconUrl; return this; } public DiscordEmbed SetFooter(string text) { FooterText = text; return this; } public DiscordEmbed SetThumbnail(string url) { ThumbnailUrl = url; return this; } public DiscordEmbed SetImage(string url) { ImageUrl = url; return this; } public DiscordEmbed AddField(string name, string value, bool inline = false) { Fields.Add(new EmbedField { Name = name, Value = value, Inline = inline }); return this; } public Dictionary<string, object> ToDictionary() { Dictionary<string, object> dictionary = new Dictionary<string, object>(); if (!string.IsNullOrEmpty(Title)) { dictionary["title"] = ((Title.Length > 256) ? (Title.Substring(0, 253) + "...") : Title); } if (!string.IsNullOrEmpty(Description)) { dictionary["description"] = ((Description.Length > 4096) ? (Description.Substring(0, 4093) + "...") : Description); } dictionary["color"] = Color; if (!string.IsNullOrEmpty(AuthorName)) { Dictionary<string, object> dictionary2 = new Dictionary<string, object> { { "name", AuthorName } }; if (!string.IsNullOrEmpty(AuthorIconUrl)) { dictionary2["icon_url"] = AuthorIconUrl; } dictionary["author"] = dictionary2; } if (!string.IsNullOrEmpty(FooterText)) { dictionary["footer"] = new Dictionary<string, object> { { "text", (FooterText.Length > 2048) ? (FooterText.Substring(0, 2045) + "...") : FooterText } }; } if (!string.IsNullOrEmpty(ThumbnailUrl)) { dictionary["thumbnail"] = new Dictionary<string, object> { { "url", ThumbnailUrl } }; } if (!string.IsNullOrEmpty(ImageUrl)) { dictionary["image"] = new Dictionary<string, object> { { "url", ImageUrl } }; } if (Fields.Count > 0) { List<Dictionary<string, object>> list = new List<Dictionary<string, object>>(); foreach (EmbedField field in Fields) { list.Add(new Dictionary<string, object> { { "name", string.IsNullOrEmpty(field.Name) ? "\u200b" : field.Name }, { "value", string.IsNullOrEmpty(field.Value) ? "\u200b" : field.Value }, { "inline", field.Inline } }); } dictionary["fields"] = list; } dictionary["timestamp"] = DateTime.UtcNow.ToString("o"); return dictionary; } } public class DiscordFileAttachment { public string FileName; public byte[] Content; public string ContentType; public DiscordFileAttachment(string fileName, byte[] content, string contentType = "text/plain") { FileName = fileName; Content = content; ContentType = contentType; } public DiscordFileAttachment(string fileName, string textContent, string contentType = "text/plain") { FileName = fileName; Content = Encoding.UTF8.GetBytes(textContent ?? ""); ContentType = contentType; } } public static class DiscordColors { public const int Green = 5763719; public const int Red = 15548997; public const int Yellow = 16776960; public const int Blue = 3447003; public const int Orange = 15105570; public const int White = 16777215; public const int Purple = 10181046; public const int DarkRed = 10038562; } } namespace FiresDiscordIntegration.ChatRelay { public static class ChatMediaDetector { public enum MediaType { StaticImage, AnimatedGif } public struct DetectedMedia { public string Url; public MediaType Type; } private static readonly Regex UrlPattern = new Regex("(https?://\\S+\\.(?:png|jpe?g|gif|webp)(?:\\?\\S*)?)|(https?://(?:media\\.discordapp\\.net|cdn\\.discordapp\\.com)/\\S+)|(https?://media\\.tenor\\.com/\\S+)|(https?://tenor\\.com/view/\\S+)|(https?://(?:i\\.)?giphy\\.com/\\S+)|(https?://(?:media\\.)?giphy\\.com/media/\\S+)|(https?://klipy\\.com/gifs/\\S+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static List<DetectedMedia> ExtractImageUrls(string text) { List<DetectedMedia> list = new List<DetectedMedia>(); if (string.IsNullOrEmpty(text)) { return list; } MatchCollection matchCollection = UrlPattern.Matches(text); foreach (Match item in matchCollection) { string url = item.Value.TrimEnd('.', ',', ')', ']', '>'); MediaType type = ClassifyUrl(url); list.Add(new DetectedMedia { Url = url, Type = type }); } return list; } public static string StripMediaUrls(string text) { if (string.IsNullOrEmpty(text)) { return text; } return UrlPattern.Replace(text, "").Trim(); } private static MediaType ClassifyUrl(string url) { string text = url.ToLowerInvariant(); if (text.Contains(".gif")) { return MediaType.AnimatedGif; } if (text.Contains("tenor.com")) { return MediaType.AnimatedGif; } if (text.Contains("giphy.com")) { return MediaType.AnimatedGif; } if (text.Contains("klipy.com")) { return MediaType.AnimatedGif; } return MediaType.StaticImage; } } public class ChatMediaEmbed : MonoBehaviour { [CompilerGenerated] private sealed class <>c__DisplayClass13_0 { public string resolvedUrl; internal void <DownloadAndDisplay>b__0(string result) { resolvedUrl = result; } } [CompilerGenerated] private sealed class <DownloadAndDisplay>d__13 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string url; public ChatMediaDetector.MediaType mediaType; public ChatMediaEmbed <>4__this; private <>c__DisplayClass13_0 <>8__1; private byte[] <data>5__2; private bool <success>5__3; private UnityWebRequest <request>5__4; private string <fallbackUrl>5__5; private UnityWebRequest <retry>5__6; private GifDecoder.GifResult? <result>5__7; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <DownloadAndDisplay>d__13(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { switch (<>1__state) { case -3: case 2: try { } finally { <>m__Finally1(); } break; case -4: case 3: try { } finally { <>m__Finally2(); } break; } <>8__1 = null; <data>5__2 = null; <request>5__4 = null; <fallbackUrl>5__5 = null; <retry>5__6 = null; <result>5__7 = null; <>1__state = -2; } private bool MoveNext() { //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Invalid comparison between Unknown and I4 //IL_02ab: Unknown result type (might be due to invalid IL or missing references) //IL_02b1: Invalid comparison between Unknown and I4 try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass13_0(); <>8__1.resolvedUrl = null; <>2__current = <>4__this.ResolveMediaUrl(url, delegate(string result) { <>8__1.resolvedUrl = result; }); <>1__state = 1; return true; case 1: <>1__state = -1; if (!string.IsNullOrEmpty(<>8__1.resolvedUrl)) { url = <>8__1.resolvedUrl; } Debug.Log((object)("[ChatMedia] Downloading: " + url)); <data>5__2 = null; <success>5__3 = false; <request>5__4 = UnityWebRequest.Get(url); <>1__state = -3; <request>5__4.timeout = 30; <request>5__4.SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) VerdantsAscent/1.0"); <>2__current = <request>5__4.SendWebRequest(); <>1__state = 2; return true; case 2: <>1__state = -3; if ((int)<request>5__4.result == 1) { <data>5__2 = <request>5__4.downloadHandler.data; <success>5__3 = <data>5__2 != null && <data>5__2.Length != 0 && <data>5__2.Length <= 52428800; } else { Debug.LogWarning((object)$"[ChatMedia] First attempt failed ({<request>5__4.responseCode}): {<request>5__4.error} — URL: {url}"); } <>m__Finally1(); <request>5__4 = null; if (<success>5__3) { break; } <fallbackUrl>5__5 = BuildFallbackUrl(url); if (!string.IsNullOrEmpty(<fallbackUrl>5__5) && <fallbackUrl>5__5 != url) { Debug.Log((object)("[ChatMedia] Retrying with fallback: " + <fallbackUrl>5__5)); <retry>5__6 = UnityWebRequest.Get(<fallbackUrl>5__5); <>1__state = -4; <retry>5__6.timeout = 15; <retry>5__6.SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) VerdantsAscent/1.0"); <>2__current = <retry>5__6.SendWebRequest(); <>1__state = 3; return true; } goto IL_035e; case 3: { <>1__state = -4; if ((int)<retry>5__6.result == 1) { <data>5__2 = <retry>5__6.downloadHandler.data; <success>5__3 = <data>5__2 != null && <data>5__2.Length != 0 && <data>5__2.Length <= 52428800; if (<success>5__3) { Debug.Log((object)("[ChatMedia] Fallback succeeded: " + <fallbackUrl>5__5)); } } else { Debug.LogWarning((object)$"[ChatMedia] Fallback also failed ({<retry>5__6.responseCode}): {<retry>5__6.error}"); } <>m__Finally2(); <retry>5__6 = null; goto IL_035e; } IL_035e: <fallbackUrl>5__5 = null; break; } <>4__this.DestroyLoadingLabel(); if (!<success>5__3 || <data>5__2 == null) { Object.Destroy((Object)(object)((Component)<>4__this).gameObject); return false; } if (mediaType == ChatMediaDetector.MediaType.AnimatedGif && IsGifData(<data>5__2)) { <result>5__7 = GifDecoder.Decode(<data>5__2); if (<result>5__7.HasValue && <result>5__7.Value.Frames.Length > 1) { <>4__this._gifFrames = <result>5__7.Value.Frames; <>4__this._frameDelays = <result>5__7.Value.Delays; <>4__this._currentFrame = 0; <>4__this._frameTimer = 0f; <>4__this._isAnimating = true; <>4__this.ApplyTexture(<>4__this._gifFrames[0], <result>5__7.Value.Width, <result>5__7.Value.Height); } else if (<result>5__7.HasValue && <result>5__7.Value.Frames.Length == 1) { <>4__this.ApplyTexture(<result>5__7.Value.Frames[0], <result>5__7.Value.Width, <result>5__7.Value.Height); } else { <>4__this.LoadAsStaticImage(<data>5__2); } <result>5__7 = null; } else { <>4__this.LoadAsStaticImage(<data>5__2); } return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__4 != null) { ((IDisposable)<request>5__4).Dispose(); } } private void <>m__Finally2() { <>1__state = -1; if (<retry>5__6 != null) { ((IDisposable)<retry>5__6).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <ResolveMediaUrl>d__23 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string url; public Action<string> callback; public ChatMediaEmbed <>4__this; private string <giphyId>5__1; private string <giphyId>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ResolveMediaUrl>d__23(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <giphyId>5__1 = null; <giphyId>5__2 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (url.Contains("tenor.com/view/")) { <>2__current = <>4__this.ResolveTenorOEmbed(url, callback); <>1__state = 1; return true; } if (url.Contains("giphy.com/gifs/")) { <giphyId>5__1 = ExtractGiphyId(url); if (!string.IsNullOrEmpty(<giphyId>5__1)) { callback("https://media.giphy.com/media/" + <giphyId>5__1 + "/giphy.gif"); return false; } <giphyId>5__1 = null; } if (url.Contains("i.giphy.com") && !url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) { <giphyId>5__2 = ExtractLastPathSegment(url); if (!string.IsNullOrEmpty(<giphyId>5__2)) { callback("https://i.giphy.com/media/" + <giphyId>5__2 + "/giphy.gif"); return false; } <giphyId>5__2 = null; } if (url.Contains("klipy.com/gifs/") && !url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) { callback(url.TrimEnd(new char[1] { '/' }) + ".gif"); return false; } callback(null); return false; case 1: <>1__state = -1; return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <ResolveTenorOEmbed>d__24 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string tenorUrl; public Action<string> callback; public ChatMediaEmbed <>4__this; private string <oembedUrl>5__1; private string <gifId>5__2; private UnityWebRequest <request>5__3; private string <body>5__4; private Match <htmlSrcMatch>5__5; private Match <thumbMatch>5__6; private string <src>5__7; private string <thumb>5__8; private string <upgraded>5__9; private Exception <ex>5__10; private string <directUrl>5__11; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ResolveTenorOEmbed>d__24(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <oembedUrl>5__1 = null; <gifId>5__2 = null; <request>5__3 = null; <body>5__4 = null; <htmlSrcMatch>5__5 = null; <thumbMatch>5__6 = null; <src>5__7 = null; <thumb>5__8 = null; <upgraded>5__9 = null; <ex>5__10 = null; <directUrl>5__11 = null; <>1__state = -2; } private bool MoveNext() { bool result; try { switch (<>1__state) { default: result = false; break; case 0: <>1__state = -1; <oembedUrl>5__1 = "https://tenor.com/oembed?url=" + UnityWebRequest.EscapeURL(tenorUrl); <request>5__3 = UnityWebRequest.Get(<oembedUrl>5__1); <>1__state = -3; <request>5__3.timeout = 5; <request>5__3.SetRequestHeader("User-Agent", "Mozilla/5.0 VerdantsAscent/1.0"); <>2__current = <request>5__3.SendWebRequest(); <>1__state = 1; result = true; break; case 1: { <>1__state = -3; if (<request>5__3.responseCode == 200 && <request>5__3.downloadHandler != null) { try { <body>5__4 = <request>5__3.downloadHandler.text; <htmlSrcMatch>5__5 = Regex.Match(<body>5__4, "src=\"([^\"]+\\.gif[^\"]*)\" ", RegexOptions.IgnoreCase); if (<htmlSrcMatch>5__5.Success) { <src>5__7 = <htmlSrcMatch>5__5.Groups[1].Value.Replace("\\u002F", "/").Replace("\\/", "/"); Debug.Log((object)("[ChatMedia] Tenor oEmbed html src: " + <src>5__7)); callback(<src>5__7); result = false; } else { <thumbMatch>5__6 = Regex.Match(<body>5__4, "\"thumbnail_url\"\\s*:\\s*\"([^\"]+)\""); if (!<thumbMatch>5__6.Success) { <body>5__4 = null; <htmlSrcMatch>5__5 = null; <thumbMatch>5__6 = null; goto IL_02bc; } <thumb>5__8 = <thumbMatch>5__6.Groups[1].Value.Replace("\\u002F", "/").Replace("\\/", "/"); <upgraded>5__9 = UpgradeTenorPosterToGif(<thumb>5__8); if (<upgraded>5__9 != <thumb>5__8) { Debug.Log((object)("[ChatMedia] Tenor oEmbed thumbnail upgraded to gif: " + <upgraded>5__9)); } else { Debug.Log((object)("[ChatMedia] Tenor oEmbed thumbnail: " + <thumb>5__8)); } callback(<upgraded>5__9); result = false; } } catch (Exception ex) { <ex>5__10 = ex; Debug.LogWarning((object)("[ChatMedia] Tenor oEmbed parse error: " + <ex>5__10.Message)); goto IL_02bc; } <>m__Finally1(); break; } Debug.LogWarning((object)$"[ChatMedia] Tenor oEmbed failed ({<request>5__3.responseCode}): {<request>5__3.error}"); goto IL_02bc; } IL_02bc: <>m__Finally1(); <request>5__3 = null; <gifId>5__2 = ExtractTenorGifId(tenorUrl); if (!string.IsNullOrEmpty(<gifId>5__2)) { <directUrl>5__11 = "https://media.tenor.com/images/" + <gifId>5__2 + "/tenor.gif"; Debug.Log((object)("[ChatMedia] Tenor fallback from ID: " + <directUrl>5__11)); callback(<directUrl>5__11); result = false; } else { callback(null); result = false; } break; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<request>5__3 != null) { ((IDisposable)<request>5__3).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const float MaxEmbedWidth = 697f; private const float MaxEmbedHeight = 380f; private const float LifetimeSeconds = 30f; private const int MaxDownloadBytes = 52428800; private RawImage _image; private Texture2D[] _gifFrames; private float[] _frameDelays; private int _currentFrame; private float _frameTimer; private bool _isAnimating; private float _spawnTime; private GameObject _loadingLabel; private const char TenorAnimatedGifSuffixChar = 'C'; private static readonly Regex TenorPosterUrlPattern = new Regex("^(?<prefix>https?://media\\.tenor\\.com/[A-Za-z0-9_-]+AAAA)[A-Za-z0-9](?<tail>/[^/?]+)\\.(?:png|jpe?g|webp)(?<query>\\?.*)?$", RegexOptions.IgnoreCase | RegexOptions.Compiled); public void Load(string url, ChatMediaDetector.MediaType mediaType) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) _spawnTime = Time.unscaledTime; _image = ((Component)this).gameObject.AddComponent<RawImage>(); ((Graphic)_image).color = new Color(1f, 1f, 1f, 0f); ShowLoadingLabel(mediaType); ((MonoBehaviour)this).StartCoroutine(DownloadAndDisplay(url, mediaType)); } [IteratorStateMachine(typeof(<DownloadAndDisplay>d__13))] private IEnumerator DownloadAndDisplay(string url, ChatMediaDetector.MediaType mediaType) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <DownloadAndDisplay>d__13(0) { <>4__this = this, url = url, mediaType = mediaType }; } private void LoadAsStaticImage(byte[] data) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false); if (LoadImageSafe(val, data)) { ApplyTexture(val, ((Texture)val).width, ((Texture)val).height); return; } Object.Destroy((Object)(object)val); Object.Destroy((Object)(object)((Component)this).gameObject); } private static bool LoadImageSafe(Texture2D tex, byte[] data) { try { Type type = Type.GetType("UnityEngine.ImageConversion, UnityEngine.ImageConversionModule"); if (type != null) { MethodInfo method = type.GetMethod("LoadImage", new Type[3] { typeof(Texture2D), typeof(byte[]), typeof(bool) }); if (method != null) { return (bool)method.Invoke(null, new object[3] { tex, data, false }); } } MethodInfo method2 = typeof(Texture2D).GetMethod("LoadImage", new Type[1] { typeof(byte[]) }); if (method2 != null) { return (bool)method2.Invoke(tex, new object[1] { data }); } } catch { } return false; } private void ApplyTexture(Texture2D tex, int srcWidth, int srcHeight) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_image == (Object)null) { return; } _image.texture = (Texture)(object)tex; ((Graphic)_image).color = Color.white; float num = (float)srcWidth / (float)Mathf.Max(srcHeight, 1); float num2 = Mathf.Min((float)srcWidth, 697f); float num3 = num2 / num; if (num3 > 380f) { num3 = 380f; num2 = num3 * num; } RectTransform component = ((Component)this).GetComponent<RectTransform>(); if ((Object)(object)component != (Object)null) { component.sizeDelta = new Vector2(num2, num3); } LayoutElement component2 = ((Component)this).GetComponent<LayoutElement>(); if ((Object)(object)component2 != (Object)null) { component2.preferredWidth = num2; component2.preferredHeight = num3; } if ((Object)(object)component != (Object)null && (Object)(object)((Transform)component).parent != (Object)null) { Transform parent = ((Transform)component).parent; RectTransform val = (RectTransform)(object)((parent is RectTransform) ? parent : null); if ((Object)(object)val != (Object)null) { LayoutRebuilder.ForceRebuildLayoutImmediate(val); } } } private void Update() { if (_isAnimating && _gifFrames != null && _gifFrames.Length > 1) { _frameTimer += Time.unscaledDeltaTime; float num = ((_currentFrame < _frameDelays.Length) ? _frameDelays[_currentFrame] : 0.1f); if (_frameTimer >= num) { _frameTimer -= num; _currentFrame = (_currentFrame + 1) % _gifFrames.Length; if ((Object)(object)_image != (Object)null && (Object)(object)_gifFrames[_currentFrame] != (Object)null) { _image.texture = (Texture)(object)_gifFrames[_currentFrame]; } } } if (Time.unscaledTime - _spawnTime > 30f) { Cleanup(); } } private void ShowLoadingLabel(ChatMediaDetector.MediaType mediaType) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) string text = ((mediaType == ChatMediaDetector.MediaType.AnimatedGif) ? "Loading GIF…" : "Loading image…"); _loadingLabel = new GameObject("LoadingLabel"); _loadingLabel.transform.SetParent(((Component)this).transform, false); RectTransform val = _loadingLabel.AddComponent<RectTransform>(); val.anchorMin = Vector2.zero; val.anchorMax = Vector2.one; val.sizeDelta = Vector2.zero; val.offsetMin = Vector2.zero; val.offsetMax = Vector2.zero; TextMeshProUGUI val2 = _loadingLabel.AddComponent<TextMeshProUGUI>(); ((TMP_Text)val2).text = text; ((TMP_Text)val2).fontSize = 16f; ((TMP_Text)val2).alignment = (TextAlignmentOptions)514; ((Graphic)val2).color = new Color(0.8f, 0.8f, 0.8f, 0.7f); ((TMP_Text)val2).textWrappingMode = (TextWrappingModes)0; ((TMP_Text)val2).overflowMode = (TextOverflowModes)1; ((Graphic)val2).raycastTarget = false; } private void DestroyLoadingLabel() { if ((Object)(object)_loadingLabel != (Object)null) { Object.Destroy((Object)(object)_loadingLabel); _loadingLabel = null; } } public void Cleanup() { _isAnimating = false; if (_gifFrames != null) { Texture2D[] gifFrames = _gifFrames; foreach (Texture2D val in gifFrames) { if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } } _gifFrames = null; } if ((Object)(object)_image != (Object)null && (Object)(object)_image.texture != (Object)null && _gifFrames == null) { Object.Destroy((Object)(object)_image.texture); } DestroyLoadingLabel(); Object.Destroy((Object)(object)((Component)this).gameObject); } private void OnDestroy() { if (_gifFrames == null) { return; } Texture2D[] gifFrames = _gifFrames; foreach (Texture2D val in gifFrames) { if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } } _gifFrames = null; } private static bool IsGifData(byte[] data) { return data.Length >= 3 && data[0] == 71 && data[1] == 73 && data[2] == 70; } [IteratorStateMachine(typeof(<ResolveMediaUrl>d__23))] private IEnumerator ResolveMediaUrl(string url, Action<string> callback) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ResolveMediaUrl>d__23(0) { <>4__this = this, url = url, callback = callback }; } [IteratorStateMachine(typeof(<ResolveTenorOEmbed>d__24))] private IEnumerator ResolveTenorOEmbed(string tenorUrl, Action<string> callback) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ResolveTenorOEmbed>d__24(0) { <>4__this = this, tenorUrl = tenorUrl, callback = callback }; } private static string BuildFallbackUrl(string failedUrl) { if (failedUrl.Contains("media.tenor.com") && !failedUrl.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) && !failedUrl.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && !failedUrl.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase)) { return failedUrl.TrimEnd(new char[1] { '/' }) + "/tenor.gif"; } if (failedUrl.Contains("klipy.com") && failedUrl.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) { return failedUrl.Substring(0, failedUrl.Length - 4); } return null; } private static string UpgradeTenorPosterToGif(string url) { if (string.IsNullOrEmpty(url)) { return url; } Match match = TenorPosterUrlPattern.Match(url); if (!match.Success) { return url; } return match.Groups["prefix"].Value + "C" + match.Groups["tail"].Value + ".gif" + match.Groups["query"].Value; } private static string ExtractTenorGifId(string url) { try { int num = url.IndexOf('?'); if (num >= 0) { url = url.Substring(0, num); } url = url.TrimEnd(new char[1] { '/' }); int num2 = url.LastIndexOf('-'); long result; if (num2 >= 0 && num2 < url.Length - 1) { string text = url.Substring(num2 + 1); if (long.TryParse(text, out result)) { return text; } } int num3 = url.LastIndexOf('/'); if (num3 >= 0 && num3 < url.Length - 1) { string text2 = url.Substring(num3 + 1); num2 = text2.LastIndexOf('-'); if (num2 >= 0) { string text3 = text2.Substring(num2 + 1); if (long.TryParse(text3, out result)) { return text3; } } } } catch { } return null; } private static string ExtractGiphyId(string url) { try { int num = url.IndexOf('?'); if (num >= 0) { url = url.Substring(0, num); } url = url.TrimEnd(new char[1] { '/' }); int num2 = url.LastIndexOf('/'); if (num2 < 0) { return null; } string text = url.Substring(num2 + 1); int num3 = text.LastIndexOf('-'); return (num3 >= 0) ? text.Substring(num3 + 1) : text; } catch { } return null; } private static string ExtractLastPathSegment(string url) { try { int num = url.IndexOf('?'); if (num >= 0) { url = url.Substring(0, num); } url = url.TrimEnd(new char[1] { '/' }); int num2 = url.LastIndexOf('/'); return (num2 >= 0) ? url.Substring(num2 + 1) : null; } catch { } return null; } } public static class ChatMediaEmbedManager { private const int MaxVisibleEmbeds = 3; private const float EmbedPadding = 8f; private const float ContainerWidth = 713.06f; private const float ContainerHeight = 783.27f; private const float ContainerPosX = -80f; private const float ContainerPosY = -200f; private static readonly List<ChatMediaEmbed> _activeEmbeds = new List<ChatMediaEmbed>(); private static GameObject _overlayRoot; private static RectTransform _overlayRect; private static ScrollRect _scrollRect; private static RectTransform _contentRect; public static void QueueEmbed(string url, ChatMediaDetector.MediaType mediaType) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Expected O, but got Unknown if (string.IsNullOrEmpty(url)) { return; } EnsureOverlay(); if ((Object)(object)_contentRect == (Object)null) { return; } while (_activeEmbeds.Count >= 3) { ChatMediaEmbed chatMediaEmbed = _activeEmbeds[0]; _activeEmbeds.RemoveAt(0); if ((Object)(object)chatMediaEmbed != (Object)null) { chatMediaEmbed.Cleanup(); } } GameObject val = new GameObject("ChatMediaEmbed"); val.transform.SetParent((Transform)(object)_contentRect, false); LayoutElement val2 = val.AddComponent<LayoutElement>(); val2.preferredWidth = 697.06f; val2.preferredHeight = 200f; val2.flexibleWidth = 0f; ChatMediaEmbed chatMediaEmbed2 = val.AddComponent<ChatMediaEmbed>(); chatMediaEmbed2.Load(url, mediaType); _activeEmbeds.Add(chatMediaEmbed2); ScrollToBottom(); } public static void ClearAll() { foreach (ChatMediaEmbed activeEmbed in _activeEmbeds) { if ((Object)(object)activeEmbed != (Object)null) { activeEmbed.Cleanup(); } } _activeEmbeds.Clear(); } public static void PruneDestroyed() { int count = _activeEmbeds.Count; _activeEmbeds.RemoveAll((ChatMediaEmbed e) => (Object)(object)e == (Object)null); if (_activeEmbeds.Count != count) { RebuildLayout(); } } public static void ResetOverlay() { _activeEmbeds.Clear(); if ((Object)(object)_overlayRoot != (Object)null) { Object.Destroy((Object)(object)_overlayRoot); } _overlayRoot = null; _overlayRect = null; _scrollRect = null; _contentRect = null; } private static void EnsureOverlay() { if ((Object)(object)_overlayRoot == (Object)null) { _overlayRect = null; _scrollRect = null; _contentRect = null; } if ((Object)(object)_overlayRect != (Object)null) { return; } Transform val = null; if ((Object)(object)Hud.instance != (Object)null) { val = ((Component)Hud.instance).transform; } else if ((Object)(object)Chat.instance != (Object)null) { Transform val2 = ((Component)Chat.instance).transform; while ((Object)(object)val2.parent != (Object)null) { val2 = val2.parent; } val = val2; } if (!((Object)(object)val == (Object)null)) { BuildOverlay(val); } } private static void BuildOverlay(Transform parent) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_005a: 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_008e: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Expected O, but got Unknown //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Expected O, but got Unknown //IL_0196: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) //IL_0213: Unknown result type (might be due to invalid IL or missing references) //IL_021d: Expected O, but got Unknown _overlayRoot = new GameObject("VA_ChatMediaOverlay"); _overlayRoot.transform.SetParent(parent, false); _overlayRect = _overlayRoot.AddComponent<RectTransform>(); _overlayRect.anchorMin = new Vector2(1f, 1f); _overlayRect.anchorMax = new Vector2(1f, 1f); _overlayRect.pivot = new Vector2(1f, 1f); _overlayRect.sizeDelta = new Vector2(713.06f, 783.27f); _overlayRect.anchoredPosition = new Vector2(-80f, -200f); GameObject val = new GameObject("Scroll"); val.transform.SetParent(_overlayRoot.transform, false); RectTransform val2 = val.AddComponent<RectTransform>(); val2.anchorMin = Vector2.zero; val2.anchorMax = Vector2.one; val2.sizeDelta = Vector2.zero; val2.offsetMin = Vector2.zero; val2.offsetMax = Vector2.zero; RectMask2D val3 = val.AddComponent<RectMask2D>(); _scrollRect = val.AddComponent<ScrollRect>(); _scrollRect.horizontal = false; _scrollRect.vertical = true; _scrollRect.movementType = (MovementType)2; _scrollRect.scrollSensitivity = 30f; GameObject val4 = new GameObject("Content"); val4.transform.SetParent(val.transform, false); _contentRect = val4.AddComponent<RectTransform>(); _contentRect.anchorMin = new Vector2(0f, 1f); _contentRect.anchorMax = new Vector2(1f, 1f); _contentRect.pivot = new Vector2(0f, 1f); _contentRect.sizeDelta = new Vector2(0f, 0f); VerticalLayoutGroup val5 = val4.AddComponent<VerticalLayoutGroup>(); ((LayoutGroup)val5).childAlignment = (TextAnchor)6; ((HorizontalOrVerticalLayoutGroup)val5).spacing = 8f; ((LayoutGroup)val5).padding = new RectOffset(0, 0, 0, 8); ((HorizontalOrVerticalLayoutGroup)val5).childForceExpandWidth = false; ((HorizontalOrVerticalLayoutGroup)val5).childForceExpandHeight = false; ((HorizontalOrVerticalLayoutGroup)val5).childControlWidth = false; ((HorizontalOrVerticalLayoutGroup)val5).childControlHeight = false; ContentSizeFitter val6 = val4.AddComponent<ContentSizeFitter>(); val6.horizontalFit = (FitMode)0; val6.verticalFit = (FitMode)2; _scrollRect.content = _contentRect; _scrollRect.viewport = val2; } private static void ScrollToBottom() { if ((Object)(object)_scrollRect != (Object)null) { Canvas.ForceUpdateCanvases(); _scrollRect.verticalNormalizedPosition = 0f; } } private static void RebuildLayout() { if ((Object)(object)_contentRect != (Object)null) { LayoutRebuilder.ForceRebuildLayoutImmediate(_contentRect); } } } [HarmonyPatch] public static class ChatPatches { private const string DiscordBrandColorHex = "#7289DA"; private const string MediaLoadingColorHex = "#888888"; private const string DiscordPrefixToken = "[Discord]"; private const string DiscordPrefixTokenUpper = "[DISCORD]"; private const string DiscordSenderNamePrefix = "[Discord]"; private const char DiscordSenderNameDelimiter = '|'; private static string RecolorDiscordChatLine(string entry, string authorColorHex) { entry = Regex.Replace(entry, "<color=[^>]*>", ""); entry = entry.Replace("</color>", ""); if (string.Equals(authorColorHex, "#7289DA", StringComparison.OrdinalIgnoreCase)) { return "<color=#7289DA>" + entry + "</color>"; } int num = entry.IndexOf("[DISCORD]", StringComparison.OrdinalIgnoreCase); if (num < 0) { return "<color=#7289DA>" + entry + "</color>"; } int i; for (i = num + "[DISCORD]".Length; i < entry.Length && entry[i] == ' '; i++) { } int num2 = entry.IndexOf(": ", i, StringComparison.Ordinal); if (num2 <= i) { return "<color=#7289DA>" + entry + "</color>"; } string text = entry.Substring(0, i); string text2 = entry.Substring(i, num2 - i); string text3 = entry.Substring(num2); return "<color=#7289DA>" + text + "<color=" + authorColorHex + ">" + text2 + "</color>" + text3 + "</color>"; } private static string ReadDiscordAuthorColorOrDefault(UserInfo sender) { string text = sender?.Name; if (string.IsNullOrEmpty(text)) { return "#7289DA"; } if (!text.StartsWith("[Discord]", StringComparison.Ordinal)) { return "#7289DA"; } int num = text.IndexOf('|'); if (num < 0 || num >= text.Length - 1) { return "#7289DA"; } string text2 = text.Substring(num + 1).Trim(); if (text2.Length == 0) { return "#7289DA"; } return text2.StartsWith("#") ? text2 : ("#" + text2); } [HarmonyPatch(typeof(Chat), "OnNewChatMessage")] [HarmonyPostfix] private static void Chat_OnNewChatMessage_Postfix(GameObject go, long senderID, Vector3 pos, Type type, UserInfo sender, string text) { List<ChatMediaDetector.DetectedMedia> list = null; try { list = ChatMediaDetector.ExtractImageUrls(text); } catch { } bool flag = list != null && list.Count > 0; bool flag2 = text != null && text.IndexOf("[Discord]", StringComparison.OrdinalIgnoreCase) >= 0; string authorColorHex = (flag2 ? ReadDiscordAuthorColorOrDefault(sender) : null); if (flag2 || flag) { try { if ((Object)(object)Chat.instance != (Object)null) { List<string> chatBuffer = ((Terminal)Chat.instance).m_chatBuffer; if (chatBuffer != null && chatBuffer.Count > 0) { int index = chatBuffer.Count - 1; string text2 = chatBuffer[index]; if (flag) { text2 = ChatMediaDetector.StripMediaUrls(text2); } if (flag2 && text2.IndexOf("[DISCORD]", StringComparison.OrdinalIgnoreCase) >= 0) { text2 = RecolorDiscordChatLine(text2, authorColorHex); } chatBuffer[index] = text2; ((Terminal)Chat.instance).UpdateChat(); } } } catch { } } if (!flag) { return; } try { if ((Object)(object)Chat.instance != (Object)null) { bool flag3 = false; foreach (ChatMediaDetector.DetectedMedia item2 in list) { if (item2.Type == ChatMediaDetector.MediaType.AnimatedGif) { flag3 = true; break; } } string item = (flag3 ? "<color=#888888><i>Loading GIF…</i></color>" : "<color=#888888><i>Loading image…</i></color>"); ((Terminal)Chat.instance).m_chatBuffer?.Add(item); ((Terminal)Chat.instance).UpdateChat(); } } catch { } foreach (ChatMediaDetector.DetectedMedia item3 in list) { ChatMediaEmbedManager.QueueEmbed(item3.Url, item3.Type); } } [HarmonyPatch(typeof(Chat), "Update")] [HarmonyPostfix] private static void Chat_Update_Postfix() { ChatMediaEmbedManager.PruneDestroyed(); } [HarmonyPatch(typeof(Chat), "OnDestroy")] [HarmonyPostfix] private static void Chat_OnDestroy_Postfix() { try { ChatMediaEmbedManager.ClearAll(); ChatMediaEmbedManager.ResetOverlay(); } catch { } } } public static class GifDecoder { public struct GifResult { public Texture2D[] Frames; public float[] Delays; public int Width; public int Height; } public const int MaxFrames = 500; public const int MaxBytes = 52428800; public const int MaxDimension = 1024; public static GifResult? Decode(byte[] data) { //IL_0308: Unknown result type (might be due to invalid IL or missing references) //IL_030d: Unknown result type (might be due to invalid IL or missing references) if (data == null || data.Length < 13 || data.Length > 52428800) { return null; } try { using MemoryStream memoryStream = new MemoryStream(data); using BinaryReader binaryReader = new BinaryReader(memoryStream); string text = new string(binaryReader.ReadChars(3)); string text2 = new string(binaryReader.ReadChars(3)); if (text != "GIF" || (text2 != "89a" && text2 != "87a")) { return null; } int num = binaryReader.ReadUInt16(); int num2 = binaryReader.ReadUInt16(); byte b = binaryReader.ReadByte(); binaryReader.ReadByte(); binaryReader.ReadByte(); bool flag = (b & 0x80) != 0; int count = 1 << (b & 7) + 1; Color32[] array = null; if (flag) { array = ReadColorTable(binaryReader, count); } List<Texture2D> list = new List<Texture2D>(); List<float> list2 = new List<float>(); Color32[] array2 = (Color32[])(object)new Color32[num * num2]; Color32[] array3 = (Color32[])(object)new Color32[num * num2]; int num3 = 0; float num4 = 0.1f; int num5 = -1; bool flag2 = false; while (memoryStream.Position < memoryStream.Length && list.Count < 500) { switch (binaryReader.ReadByte()) { case 33: { int num10 = binaryReader.ReadByte(); if (num10 == 249) { binaryReader.ReadByte(); byte b3 = binaryReader.ReadByte(); num3 = (b3 >> 2) & 7; flag2 = (b3 & 1) != 0; ushort num11 = binaryReader.ReadUInt16(); num4 = (float)(int)num11 * 0.01f; if (num4 < 0.02f) { num4 = 0.1f; } num5 = binaryReader.ReadByte(); binaryReader.ReadByte(); } else { SkipSubBlocks(binaryReader); } continue; } case 44: { int num6 = binaryReader.ReadUInt16(); int num7 = binaryReader.ReadUInt16(); int num8 = binaryReader.ReadUInt16(); int num9 = binaryReader.ReadUInt16(); byte b2 = binaryReader.ReadByte(); bool flag3 = (b2 & 0x80) != 0; bool interlaced = (b2 & 0x40) != 0; int count2 = 1 << (b2 & 7) + 1; Color32[] array4 = array; if (flag3) { array4 = ReadColorTable(binaryReader, count2); } if (array4 == null) { SkipImageData(binaryReader); continue; } Array.Copy(array2, array3, array2.Length); byte[] pixels = DecodeLzw(binaryReader, num8 * num9); RenderFrame(array2, pixels, array4, num6, num7, num8, num9, num, num2, interlaced, flag2 ? num5 : (-1)); Texture2D item = CanvasToTexture(array2, num, num2); list.Add(item); list2.Add(num4); switch (num3) { case 2: { for (int i = num7; i < num7 + num9 && i < num2; i++) { for (int j = num6; j < num6 + num8 && j < num; j++) { array2[i * num + j] = new Color32((byte)0, (byte)0, (byte)0, (byte)0); } } break; } case 3: Array.Copy(array3, array2, array2.Length); break; } num5 = -1; flag2 = false; num3 = 0; num4 = 0.1f; continue; } default: continue; case 59: break; } break; } if (list.Count == 0) { return null; } GifResult value = default(GifResult); value.Frames = list.ToArray(); value.Delays = list2.ToArray(); value.Width = num; value.Height = num2; return value; } catch (Exception ex) { Debug.LogWarning((object)("[ChatMedia] GIF decode error: " + ex.Message)); return null; } } private static Color32[] ReadColorTable(BinaryReader reader, int count) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) Color32[] array = (Color32[])(object)new Color32[count]; for (int i = 0; i < count; i++) { byte b = reader.ReadByte(); byte b2 = reader.ReadByte(); byte b3 = reader.ReadByte(); array[i] = new Color32(b, b2, b3, byte.MaxValue); } return array; } private static void RenderFrame(Color32[] canvas, byte[] pixels, Color32[] colorTable, int left, int top, int fw, int fh, int canvasW, int canvasH, bool interlaced, int transparentIndex) { //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) int[] array = null; if (interlaced) { array = new int[fh]; int num = 0; for (int i = 0; i < 4; i++) { int num2 = i switch { 2 => 2, 1 => 4, 0 => 0, _ => 1, }; int num3 = i switch { 2 => 4, 1 => 8, 0 => 8, _ => 2, }; for (int j = num2; j < fh; j += num3) { array[num++] = j; } } } for (int k = 0; k < fh; k++) { int num4 = (interlaced ? array[k] : k); int num5 = top + num4; if (num5 < 0 || num5 >= canvasH) { continue; } for (int l = 0; l < fw; l++) { int num6 = left + l; if (num6 < 0 || num6 >= canvasW) { continue; } int num7 = k * fw + l; if (num7 < pixels.Length) { int num8 = pixels[num7] & 0xFF; if (num8 != transparentIndex && num8 < colorTable.Length) { int num9 = canvasH - 1 - num5; canvas[num9 * canvasW + num6] = colorTable[num8]; } } } } } private static Texture2D CanvasToTexture(Color32[] canvas, int width, int height) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown Texture2D val = new Texture2D(width, height, (TextureFormat)4, false); ((Texture)val).filterMode = (FilterMode)1; val.SetPixels32(canvas); val.Apply(false, false); return val; } private static byte[] DecodeLzw(BinaryReader reader, int pixelCount) { int num = reader.ReadByte(); byte[] array = ReadSubBlocksAsBytes(reader); int num2 = 1 << num; int num3 = num2 + 1; int num4 = num + 1; int num5 = num3 + 1; int num6 = (1 << num4) - 1; int num7 = 4096; int[] array2 = new int[num7]; byte[] array3 = new byte[num7]; int[] array4 = new int[num7]; for (int i = 0; i < num2; i++) { array2[i] = -1; array3[i] = (byte)i; array4[i] = 1; } byte[] array5 = new byte[Math.Max(pixelCount, 1)]; int num8 = 0; int num9 = 0; int j = 0; int num10 = 0; int num11 = -1; while (num8 < pixelCount && num10 < array.Length) { for (; j < num4; j += 8) { if (num10 >= array.Length) { break; } num9 |= array[num10++] << j; } int num12 = num9 & num6; num9 >>= num4; j -= num4; if (num12 == num3) { break; } if (num12 == num2) { num4 = num + 1; num6 = (1 << num4) - 1; num5 = num3 + 1; num11 = -1; continue; } bool flag = num12 >= num5; if (flag && num11 < 0) { break; } int num13 = ((!flag) ? num12 : num11); int num14 = ((num13 >= num5 || num13 < 0) ? 1 : array4[num13]); if (flag) { num14++; } int num15 = num8; int num16 = num8 + num14 - 1; if (num16 >= array5.Length) { num16 = array5.Length - 1; num14 = num16 - num15 + 1; } int num17 = num13; int num18 = num16; while (num18 >= num15 && num17 >= 0 && num17 < num7) { array5[num18] = array3[num17]; num17 = array2[num17]; num18--; } if (flag && num15 + num14 - 1 < array5.Length) { array5[num15 + num14 - 1] = array5[num15]; } num8 += num14; if (num11 >= 0 && num5 < num7) { array2[num5] = num11; array3[num5] = array5[num15]; array4[num5] = array4[num11] + 1; num5++; if (num5 > num6 && num4 < 12) { num4++; num6 = (1 << num4) - 1; } } num11 = num12; } return array5; } private static byte[] ReadSubBlocksAsBytes(BinaryReader reader) { List<byte> list = new List<byte>(); while (true) { int num = reader.ReadByte(); if (num == 0) { break; } list.AddRange(reader.ReadBytes(num)); } return list.ToArray(); } private static void SkipSubBlocks(BinaryReader reader) { while (true) { int num = reader.ReadByte(); if (num == 0) { break; } reader.BaseStream.Position += num; } } private static void SkipImageData(BinaryReader reader) { reader.ReadByte(); SkipSubBlocks(reader); } } } namespace FiresDiscordIntegration.Utilities { public static class PlayerSpawnGate { private class Driver : MonoBehaviour { private struct Entry { public Player Target; public bool UseLocal; public float DeadlineUnscaled; public Action<Player> Work; } private static Driver _instance; private readonly List<Entry> _queue = new List<Entry>(8); private const int MAX_QUEUED = 256; public static Driver GetOrCreate() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown if ((Object)(object)_instance != (Object)null) { return _instance; } GameObject val = new GameObject("FiresRPGmaker_PlayerSpawnGateDriver"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; _instance = val.AddComponent<Driver>(); return _instance; } public void Enqueue(Player target, float timeoutSeconds, Action<Player> work) { if (_queue.Count >= 256) { Debug.LogWarning((object)string.Format("{0} queue at MAX_QUEUED ({1}) \ufffd dropping enqueue.", "[PlayerSpawnGate]", 256)); return; } _queue.Add(new Entry { Target = target, UseLocal = false, DeadlineUnscaled = Time.unscaledTime + Mathf.Max(0.1f, timeoutSeconds), Work = work }); } public void EnqueueLocal(float timeoutSeconds, Action<Player> work) { if (_queue.Count >= 256) { Debug.LogWarning((object)string.Format("{0} queue at MAX_QUEUED ({1}) \ufffd dropping enqueue.", "[PlayerSpawnGate]", 256)); return; } _queue.Add(new Entry { Target = null, UseLocal = true, DeadlineUnscaled = Time.unscaledTime + Mathf.Max(0.1f, timeoutSeconds), Work = work }); } private void Update() { if (_queue.Count == 0) { return; } float unscaledTime = Time.unscaledTime; for (int num = _queue.Count - 1; num >= 0; num--) { Entry entry = _queue[num]; Player val = (entry.UseLocal ? Player.m_localPlayer : entry.Target); if (IsReadyForCustomDataWrite(val)) { _queue.RemoveAt(num); InvokeWork(val, entry.Work); } else if (unscaledTime >= entry.DeadlineUnscaled) { _queue.RemoveAt(num); if (VerboseLogging) { Debug.LogWarning((object)("[PlayerSpawnGate] deferred action timed out before player became ready " + string.Format("(useLocal={0}, target={1}) \ufffd dropping.", entry.UseLocal, ((Object)(object)val == (Object)null) ? "null" : SafeName(val)))); } } } } private static string SafeName(Player p) { try { return p.GetPlayerName() ?? "<unnamed>"; } catch { return "<unnameable>"; } } } private const string LogPrefix = "[PlayerSpawnGate]"; public static bool VerboseLogging; public static bool IsReadyForCustomDataWrite(Player p) { //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Invalid comparison between Unknown and I4 if ((Object)(object)p == (Object)null) { return false; } try { if (!Object.op_Implicit((Object)(object)p)) { return false; } } catch { return false; } if (p.m_customData == null) { return false; } try { if ((Object)(object)((Character)p).m_nview == (Object)null) { return false; } if (!((Character)p).m_nview.IsValid()) { return false; } } catch { return false; } if ((Object)(object)ZNet.instance == (Object)null) { return false; } if (!ZNet.instance.IsServer() && (int)ZNet.GetConnectionStatus() != 2) { return false; } try { if (((Character)p).IsTeleporting()) { return false; } } catch { return false; } return true; } public static void RunWhenReady(Player player, float timeoutSeconds, Action<Player> work) { if (work == null) { return; } if ((Object)(object)player == (Object)null) { if (VerboseLogging) { Debug.Log((object)"[PlayerSpawnGate] RunWhenReady called with null player \ufffd dropping."); } } else { Driver.GetOrCreate().Enqueue(player, timeoutSeconds, work); } } public static void RunWhenLocalReady(float timeoutSeconds, Action<Player> work) { if (work != null) { Driver.GetOrCreate().EnqueueLocal(timeoutSeconds, work); } } public static void SafeSetCustomData(Player player, string key, string value, float timeoutSeconds = 30f) { if (!string.IsNullOrEmpty(key)) { RunWhenReady(player, timeoutSeconds, delegate(Player p) { p.m_customData[key] = value; }); } } public static void SafeSetLocalCustomData(string key, string value, float timeoutSeconds = 30f) { if (!string.IsNullOrEmpty(key)) { RunWhenLocalReady(timeoutSeconds, delegate(Player p) { p.m_customData[key] = value; }); } } private static void InvokeWork(Player p, Action<Player> work) { string text = "<null player>"; try { if ((Object)(object)p != (Object)null) { text = p.GetPlayerName() ?? "<unnamed>"; } } catch { text = "<unnameable>"; } string text2 = work?.Method?.DeclaringType?.FullName + "." + (work?.Method?.Name ?? "<unknown>"); Debug.Log((object)("[LoginFreeze][DIAG] PlayerSpawnGate.InvokeWork ENTER target=" + text + " work=" + text2)); try { work(p); } catch (Exception ex) { Debug.LogWarning((object)("[PlayerSpawnGate] deferred action threw: " + ex.Message)); } Debug.Log((object)("[LoginFreeze][DIAG] PlayerSpawnGate.InvokeWork EXIT target=" + text + " work=" + text2)); } } public static class BotTokenFile { private const string TokenFileName = "FiresDiscordIntegration_BotToken.cfg"; private const string TokenKey = "BotToken"; private const string EmptyTemplateContents = "# FiresDiscordIntegration bot token\n#\n# This file deliberately lives OUTSIDE BepInEx/config/ so it can't get\n# included in a modpack / profile export by Thunderstore Mod Manager or\n# r2modman. DO NOT share this file. DO NOT commit it to a public repo.\n#\n# Get your token from https://discord.com/developers/applications:\n# 1. Open your bot application\n# 2. Sidebar -> Bot -> Reset Token\n# 3. Copy the long string\n# 4. Paste it after the '=' below, save the file, restart your server\n#\n# Make sure the bot's Server Members intent is enabled (same page as the\n# token reset button) — that's what lets the mod read each Discord user's\n# role color so chat messages get their author colour in-game.\n\nBotToken = \n"; private static string _cachedToken; private static bool _loaded; public static string Token { get { if (!_loaded) { LoadFromDisk(); } return _cachedToken ?? string.Empty; } } public static string FilePath => Path.Combine(Paths.GameRootPath, "FiresDiscordIntegration_BotToken.cfg"); public static void InvalidateCache() { _loaded = false; _cachedToken = null; } private static void LoadFromDisk() { _loaded = true; if (!Application.isBatchMode) { return; } string filePath = FilePath; try { if (!File.Exists(filePath)) { try { File.WriteAllText(filePath, "# FiresDiscordIntegration bot token\n#\n# This file deliberately lives OUTSIDE BepInEx/config/ so it can't get\n# included in a modpack / profile export by Thunderstore Mod Manager or\n# r2modman. DO NOT share this file. DO NOT commit it to a public repo.\n#\n# Get your token from https://discord.com/developers/applications:\n# 1. Open your bot application\n# 2. Sidebar -> Bot -> Reset Token\n# 3. Copy the long string\n# 4. Paste it after the '=' below, save the file, restart your server\n#\n# Make sure the bot's Server Members intent is enabled (same page as the\n# token reset button) — that's what lets the mod read each Discord user's\n# role color so chat messages get their author colour in-game.\n\nBotToken = \n"); } catch (Exception ex) { Debug.LogWarning((object)("[FiresDiscordIntegration] Failed to create bot token template at '" + filePath + "': " + ex.Message)); return; } Debug.Log((object)("[FiresDiscordIntegration] Bot token file not found — created empty template at: " + filePath + "\nOpen it, paste your Discord bot token after 'BotToken = ', save, and restart the server. Until then the bot listener / status panel / reaction features stay disabled (webhook events still work).")); return; } string[] array = File.ReadAllLines(filePath); foreach (string text in array) { if (string.IsNullOrWhiteSpace(text)) { continue; } string text2 = text.TrimStart(Array.Empty<char>()); if (text2.StartsWith("#")) { continue; } int num = text2.IndexOf('='); if (num > 0) { string a = text2.Substring(0, num).Trim(); string cachedToken = text2.Substring(num + 1).Trim(); if (string.Equals(a, "BotToken", StringComparison.OrdinalIgnoreCase)) { _cachedToken = cachedToken; break; } } } } catch (Exception ex2) { Debug.LogWarning((object)("[FiresDiscordIntegration] Failed to read bot token file '" + filePath + "': " + ex2.Message)); } } } public static class ServerNameResolver { private const string UnknownNameFallback = "Unknown"; private const string NameLaunchArg = "-name"; private static string _cachedServerName; public static string GetOperatorServerName() { if (!string.IsNullOrEmpty(_cachedServerName)) { return _cachedServerName; } string text = TryReadZNetServerName(); if (!string.IsNullOrEmpty(text)) { return _cachedServerName = text; } string text2 = TryReadNameLaunchArg(); if (!string.IsNullOrEmpty(text2)) { return _cachedServerName = text2; } string text3 = TryReadWorldName(); if (!string.IsNullOrEmpty(text3)) { return _cachedServerName = text3; } return "Unknown"; } private static string TryReadZNetServerName() { try { return ZNet.m_ServerName; } catch { return null; } } private static string TryReadNameLaunchArg() { try { string[] commandLineArgs = Environment.GetCommandLineArgs(); for (int i = 0; i < commandLineArgs.Length - 1; i++) { if (commandLineArgs[i].Equals("-name", StringComparison.OrdinalIgnoreCase)) { return commandLineArgs[i + 1]; } } } catch { } return null; } private static string TryReadWorldName() { try { ZNet instance = ZNet.instance; return (instance != null) ? instance.GetWorldName() : null; } catch { return null; } } } internal static class WrapperBatExtractor { private const string ResourceSuffix = "valheim_server_wrapper.bat"; private const string TargetDirName = "FiresDiscordIntegration"; private const string TargetFileName = "valheim_server_wrapper.bat"; public static void ExtractIfMissing() { try { string text = Path.Combine(Paths.ConfigPath, "FiresDiscordIntegration"); string text2 = Path.Combine(text, "valheim_server_wrapper.bat"); if (File.Exists(text2)) { ConfigManager instance = ConfigManager.Instance; if (instance != null && (instance.configVerboseLogging?.Value).GetValueOrDefault()) { Debug.Log((object)("[FiresDiscordIntegration] Wrapper bat already present at " + text2 + " — skipping extract.")); } return; } if (!Directory.Exists(text)) { Directory.CreateDirectory(text); } Assembly executingAssembly = Assembly.GetExecutingAssembly(); string text3 = executingAssembly.GetManifestResourceNames().FirstOrDefault((string n) => n.EndsWith("valheim_server_wrapper.bat", StringComparison.OrdinalIgnoreCase)); if (text3 == null) { Debug.LogWarning((object)("[FiresDiscordIntegration] Wrapper bat embedded resource not found. Available resources: " + string.Join(", ", executingAssembly.GetManifestResourceNames()))); return; } using (Stream stream = executingAssembly.GetManifestResourceStream(text3)) { using FileStream destination = File.Create(text2); if (stream == null) { Debug.LogWarning((object)("[FiresDiscordIntegration] Could not open embedded resource '" + text3 + "' for read.")); return; } stream.CopyTo(destination); } Debug.Log((object)("[FiresDiscordIntegration] Extracted wrapper bat TEMPLATE → " + text2 + ". This is a TEMPLATE — open it in a text editor and fill in SERVER_NAME, SERVER_PORT, SERVER_WORLD, SERVER_PASSWORD (min 5 chars), and SERVER_PUBLIC (1/0) to match your server. Then copy the edited bat to your Valheim dedicated server install root (next to valheim_server.exe) and run it instead of valheim_server.exe directly to get auto-restart + Discord control-panel integration. Subsequent mod updates will NEVER overwrite this file once you've edited it.")); } catch (Exception ex) { Debug.LogWarning((object)("[FiresDiscordIntegration] WrapperBatExtractor failed: " + ex.GetType().Name + ": " + ex.Message)); } } } }