Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of FiresDiscordIntegration v1.0.1
FiresDiscordIntegration.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
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<bool> 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<bool>("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 <SendZPackage>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 <SendZPackage>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 <SendZPackage>d__59 : IEnumerator<object>, 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<ZNetPeer> <peers>5__2; private IEnumerator <enumerator>5__3; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SendZPackage>d__59(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <peers>5__2 = null; <enumerator>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; } <peers>5__2 = ((List<ZNetPeer>)AccessTools.Field(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance)).Where((ZNetPeer p) => <>8__1.target == ZRoutedRpc.Everybody || p.m_uid == <>8__1.target).ToList(); <enumerator>5__3 = <>4__this.SendZPackage(<peers>5__2, package); break; case 1: <>1__state = -1; break; } if (<enumerator>5__3.MoveNext()) { <>2__current = <enumerator>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 <SendZPackage>d__60 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public List<ZNetPeer> peers; public ZPackage package; public ConfigSync <>4__this; private <>c__DisplayClass60_0 <>8__1; private byte[] <data>5__2; private List<IEnumerator> <writers>5__3; private ZPackage <compressed>5__4; private MemoryStream <output>5__5; private DeflateStream <deflate>5__6; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SendZPackage>d__60(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <data>5__2 = null; <writers>5__3 = null; <compressed>5__4 = null; <output>5__5 = null; <deflate>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; } <data>5__2 = <>8__1.package.GetArray(); if (<data>5__2.Length > 10000) { <compressed>5__4 = new ZPackage(); <compressed>5__4.Write((byte)4); <output>5__5 = new MemoryStream(); try { <deflate>5__6 = new DeflateStream(<output>5__5, CompressionLevel.Optimal); try { <deflate>5__6.Write(<data>5__2, 0, <data>5__2.Length); } finally { if (<deflate>5__6 != null) { ((IDisposable)<deflate>5__6).Dispose(); } } <deflate>5__6 = null; <compressed>5__4.Write(<output>5__5.ToArray()); <>8__1.package = <compressed>5__4; } finally { if (<output>5__5 != null) { ((IDisposable)<output>5__5).Dispose(); } } <compressed>5__4 = null; <output>5__5 = null; } <writers>5__3 = (from p in peers where p.IsReady() select <>8__1.<>4__this.distributeConfigToPeers(p, <>8__1.package)).ToList(); <writers>5__3.RemoveAll((IEnumerator w) => !w.MoveNext()); break; case 1: <>1__state = -1; <writers>5__3.RemoveAll((IEnumerator w) => !w.MoveNext()); break; } if (<writers>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 <distributeConfigToPeers>d__61 : IEnumerator<object>, 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[] <data>5__2; private int <fragments>5__3; private long <packageId>5__4; private int <i>5__5; private ZPackage <pkg>5__6; private IEnumerator<bool> <>s__7; private bool <wait>5__8; private IEnumerator<bool> <>s__9; private bool <wait>5__10; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <distributeConfigToPeers>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; <data>5__2 = null; <pkg>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; <data>5__2 = package.GetArray(); if (<data>5__2.Length > 250000) { <fragments>5__3 = (<data>5__2.Length + 249999) / 250000; <packageId>5__4 = ++packageCounter; <i>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()) { <wait>5__10 = <>s__9.Current; <>2__current = <wait>5__10; <>1__state = 3; return true; } <>m__Finally2(); <>s__9 = null; SendPackage(package); break; IL_0247: if (<i>5__5 < <fragments>5__3) { <>s__7 = waitForQueue().GetEnumerator(); <>1__state = -3; goto IL_0128; } break; IL_022d: <pkg>5__6 = null; <i>5__5++; goto IL_0247; IL_0128: if (<>s__7.MoveNext()) { <wait>5__8 = <>s__7.Current; <>2__current = <wait>5__8; <>1__state = 1; return true; } <>m__Finally1(); <>s__7 = null; if (!<>8__1.peer.m_socket.IsConnected()) { break; } <pkg>5__6 = new ZPackage(); <pkg>5__6.Write((byte)2); <pkg>5__6.Write(<packageId>5__4); <pkg>5__6.Write(<i>5__5); <pkg>5__6.Write(<fragments>5__3); <pkg>5__6.Write(<data>5__2.Skip(250000 * <i>5__5).Take(250000).ToArray()); SendPackage(<pkg>5__6); if (<i>5__5 < <fragments>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.<<distributeConfigToPeers>g__waitForQueue|0>d))] IEnumerable<bool> waitForQueue() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <>c__DisplayClass61_0.<<distributeConfigToPeers>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<ConfigSync> configSyncs = new HashSet<ConfigSync>(); public HashSet<OwnConfigEntryBase> allConfigs = new HashSet<OwnConfigEntryBase>(); public HashSet<CustomSyncedValueBase> allCustomValues = new HashSet<CustomSyncedValueBase>(); 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<string, SortedDictionary<int, byte[]>> configValueCache = new Dictionary<string, SortedDictionary<int, byte[]>>(); private readonly List<KeyValuePair<long, string>> cacheExpirations = new List<KeyValuePair<long, string>>(); 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<bool> 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<ZNetPeer> source = (List<ZNetPeer>)AccessTools.Field(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance); ZNetPeer val = ((IEnumerable<ZNetPeer>)source).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.m_server)); if (val != null) { ZRoutedRpc.instance.InvokeRoutedRPC(val.m_uid, Name + " ConfigSync", new object[1] { (object)new ZPackage() }); } } } public SyncedConfigEntry<T> AddConfigEntry<T>(ConfigEntry<T> configEntry) { SyncedConfigEntry<T> syncedEntry = (configData((ConfigEntryBase)(object)configEntry) as SyncedConfigEntry<T>) ?? new SyncedConfigEntry<T>(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), "<Tags>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<T> AddLockingConfigEntry<T>(ConfigEntry<T> lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry<T>(lockingConfig); lockingConfig.SettingChanged += delegate { this.lockedConfigChanged?.Invoke(); }; return (SyncedConfigEntry<T>)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<CustomSyncedValueBase>(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<ConfigEntryBase>)(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<long, string> 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<int, byte[]>(); configValueCache[text2] = value; cacheExpirations.Add(new KeyValuePair<long, string>(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<OwnConfigEntryBase, object> 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<CustomSyncedValueBase, object> 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<ConfigurationManagerAttributes>().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<string, OwnConfigEntryBase> 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<object> source = new List<object>(); ZRpc.Deserialize(new ParameterInfo[2] { null, parameterInfo }, package, ref source); return source.First(); } internal ZPackage ConfigsToPackage(IEnumerable<ConfigEntryBase> configs = null, IEnumerable<CustomSyncedValueBase> customValues = null, IEnumerable<PackageEntry> 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<ConfigEntryBase> list = configs?.Where((ConfigEntryBase c) => configData(c).SynchronizedConfig).ToList() ?? new List<ConfigEntryBase>(); List<CustomSyncedValueBase> list2 = customValues?.ToList() ?? new List<CustomSyncedValueBase>(); List<PackageEntry> list3 = packageEntries?.ToList() ?? new List<PackageEntry>(); 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(<SendZPackage>d__59))] public IEnumerator SendZPackage(long target, ZPackage package) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SendZPackage>d__59(0) { <>4__this = this, target = target, package = package }; } [IteratorStateMachine(typeof(<SendZPackage>d__60))] public IEnumerator SendZPackage(List<ZNetPeer> peers, ZPackage package) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SendZPackage>d__60(0) { <>4__this = this, peers = peers, package = package }; } [IteratorStateMachine(typeof(<distributeConfigToPeers>d__61))] private IEnumerator distributeConfigToPeers(ZNetPeer peer, ZPackage package) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <distributeConfigToPeers>d__61(0) { <>4__this = this, peer = peer, package = package }; } internal static OwnConfigEntryBase configData(ConfigEntryBase config) { return config.Description.Tags?.OfType<OwnConfigEntryBase>().SingleOrDefault(); } private static T configAttribute<T>(ConfigEntryBase config) where T : class { object[] tags = config.Description.Tags; return (tags != null) ? tags.OfType<T>().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<T> : OwnConfigEntryBase { public T Value { get { if (!(BaseConfig is ConfigEntry<T> val)) { throw new InvalidOperationException("Cannot cast BaseConfig to ConfigEntry<" + typeof(T).Name + ">"); } return val.Value; } set { ((ConfigEntry<T>)(object)BaseConfig).Value = value; } } public T DefaultValue => (T)BaseConfig.BoxedValue; public SyncedConfigEntry(ConfigEntry<T> 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<OwnConfigEntryBase, object> configValues = new Dictionary<OwnConfigEntryBase, object>(); public readonly Dictionary<CustomSyncedValueBase, object> customValues = new Dictionary<CustomSyncedValueBase, object>(); } 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<ZPackage>(configSync.Name + " ConfigSync", (Action<long, ZPackage>)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<ZPackage>(configSync.Name + " ConfigSync", (Action<ZRpc, ZPackage>)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<string, string> ModList { get; } public IReadOnlyDictionary<string, string> 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<LogErrorWarningExtractor.SourceIssueCount> 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<string, string> modList, IReadOnlyDictionary<string, string> serverMods = null, string brandLabel = null) { PlatformId = platformId ?? string.Empty; PlayerName = (string.IsNullOrEmpty(playerName) ? "unknown" : playerName); LogBytes = logBytes ?? Array.Empty<byte>(); ModList = modList ?? new Dictionary<string, string>(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<string, string> item in artifacts.ModList.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> 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<IClientLogConsumer> _consumers = new List<IClientLogConsumer>(); 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 <DelayedClientPush>b__0(Player _) { playerReady = true; } } [CompilerGenerated] private sealed class <DelayedClientPush>d__41 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private <>c__DisplayClass41_0 <>8__1; private float <timeoutAt>5__2; private long <serverId>5__3; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <DelayedClientPush>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; }); <timeoutAt>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; } <serverId>5__3 = ZRoutedRpc.instance.GetServerPeerID(); if (<serverId>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(<serverId>5__3)); <>1__state = 3; return true; case 3: { <>1__state = -1; return false; } IL_009f: if (!<>8__1.playerReady && Time.realtimeSinceStartup < <timeoutAt>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 <PeriodicClientRepush>d__43 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; private long <serverId>5__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PeriodicClientRepush>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; } <serverId>5__1 = ZRoutedRpc.instance.GetServerPeerID(); if (<serverId>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(<serverId>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 <PeriodicTransferCleanup>d__44 : 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 <PeriodicTransferCleanup>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 <PostDisconnectLogAsync>d__52 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string platformId; public string playerName; public string requestedByName; private Exception <ex>5__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PostDisconnectLogAsync>d__52(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <ex>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) { <ex>5__1 = ex; Debug.LogWarning((object)("[ClientLogRelay] PostDisconnectLogAsync threw: " + <ex>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 <PushLogToServerCoroutine>d__42 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public long serverId; private byte[] <logBytes>5__1; private Dictionary<string, string> <modList>5__2; private string <steamId>5__3; private int <logSize>5__4; private int <totalChunks>5__5; private Exception <ex>5__6; private Exception <sidEx>5__7; private ZPackage <metaPayload>5__8; private byte[] <metaData>5__9; private Dictionary<string, string>.Enumerator <>s__10; private KeyValuePair<string, string> <kv>5__11; private int <chunkIndex>5__12; private ZPackage <payload>5__13; private int <offset>5__14; private int <size>5__15; private byte[] <chunk>5__16; private byte[] <packageData>5__17; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PushLogToServerCoroutine>d__42(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <logBytes>5__1 = null; <modList>5__2 = null; <steamId>5__3 = null; <ex>5__6 = null; <sidEx>5__7 = null; <metaPayload>5__8 = null; <metaData>5__9 = null; <>s__10 = default(Dictionary<string, string>.Enumerator); <kv>5__11 = default(KeyValuePair<string, string>); <payload>5__13 = null; <chunk>5__16 = null; <packageData>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..."); <logBytes>5__1 = null; <modList>5__2 = null; try { ClientLogCollector.TryCollect(out <logBytes>5__1, out <modList>5__2); if (<modList>5__2 == null) { <modList>5__2 = new Dictionary<string, string>(0, StringComparer.OrdinalIgnoreCase); } byte[] array2 = <logBytes>5__1; Debug.Log((object)$"[ClientLogRelay] PushLogToServer read complete - logBytes={((array2 != null) ? array2.Length : 0)}B, mods={<modList>5__2.Count}."); } catch (Exception ex) { <ex>5__6 = ex; Debug.LogWarning((object)("[ClientLogRelay] TryCollect failed: " + <ex>5__6.Message)); return false; } <>2__current = null; <>1__state = 1; return true; case 1: { <>1__state = -1; <steamId>5__3 = string.Empty; try { <steamId>5__3 = GetOwnSteamId() ?? string.Empty; } catch (Exception ex) { <sidEx>5__7 = ex; Debug.Log((object)("[ClientLogRelay] SteamID unavailable (" + <sidEx>5__7.GetType().Name + ": " + <sidEx>5__7.Message + "); pushing without it.")); } <logBytes>5__1 = <logBytes>5__1 ?? new byte[0]; <logSize>5__4 = <logBytes>5__1.Length; <totalChunks>5__5 = ((<logSize>5__4 == 0) ? 1 : ((<logSize>5__4 + 409600 - 1) / 409600)); Debug.Log((object)$"[ClientLogRelay] Pushing log to server: {<logSize>5__4}B in {<totalChunks>5__5} chunk(s) + metadata, mods={<modList>5__2.Count}"); <metaPayload>5__8 = new ZPackage(); <metaPayload>5__8.Write(-1); <metaPayload>5__8.Write(<totalChunks>5__5); <metaPayload>5__8.Write(<modList>5__2.Count); <>s__10 = <modList>5__2.GetEnumerator(); try { while (<>s__10.MoveNext()) { <kv>5__11 = <>s__10.Current; <metaPayload>5__8.Write(<kv>5__11.Key ?? string.Empty); <metaPayload>5__8.Write(<kv>5__11.Value ?? string.Empty); <kv>5__11 = default(KeyValuePair<string, string>); } } finally { ((IDisposable)<>s__10).Dispose(); } <>s__10 = default(Dictionary<string, string>.Enumerator); <metaPayload>5__8.Write(<steamId>5__3 ?? string.Empty); <metaData>5__9 = <metaPayload>5__8.GetArray(); if (<metaData>5__9 != null && <metaData>5__9.Length > 420000) { Debug.LogWarning((object)$"[ClientLogRelay] Metadata ZPackage is {<metaData>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] { <metaPayload>5__8 }); byte[] array = <metaData>5__9; Debug.Log((object)$"[ClientLogRelay] -> Sent metadata ({((array != null) ? array.Length : 0)}B total, {<modList>5__2.Count} mods)"); <metaPayload>5__8 = null; <metaData>5__9 = null; <>2__current = null; <>1__state = 2; return true; } case 2: <>1__state = -1; <chunkIndex>5__12 = 0; break; case 3: <>1__state = -1; <payload>5__13 = null; <chunk>5__16 = null; <packageData>5__17 = null; <chunkIndex>5__12++; break; } if (<chunkIndex>5__12 < <totalChunks>5__5) { if (ZRoutedRpc.instance == null) { Debug.LogWarning((object)$"[ClientLogRelay] ZRoutedRpc gone at chunk {<chunkIndex>5__12 + 1}/{<totalChunks>5__5} - aborting push."); return false; } <payload>5__13 = new ZPackage(); <payload>5__13.Write(<chunkIndex>5__12); <payload>5__13.Write(<totalChunks>5__5); <offset>5__14 = <chunkIndex>5__12 * 409600; <size>5__15 = Math.Min(409600, <logSize>5__4 - <offset>5__14); <chunk>5__16 = new byte[<size>5__15]; if (<size>5__15 > 0) { Array.Copy(<logBytes>5__1, <offset>5__14, <chunk>5__16, 0, <size>5__15); } <payload>5__13.Write(<chunk>5__16); <packageData>5__17 = <payload>5__13.GetArray(); if (<packageData>5__17 != null && <packageData>5__17.Length > 420000) { Debug.LogWarning((object)$"[ClientLogRelay] CRITICAL: Chunk {<chunkIndex>5__12 + 1} ZPackage is {<packageData>5__17.Length}B!"); } ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_SubmitClientLog", new object[1] { <payload>5__13 }); if (<totalChunks>5__5 > 1) { object[] obj = new object[4] { <chunkIndex>5__12 + 1, <totalChunks>5__5, <size>5__15, null }; byte[] array3 = <packageData>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: {<logSize>5__4}B in {<totalChunks>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 <RepushLogToServerCoroutine>d__45 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public long serverId; private byte[] <logBytes>5__1; private string <steamId>5__2; private int <logSize>5__3; private int <totalChunks>5__4; private Dictionary<string, string> <modList>5__5; private Exception <ex>5__6; private ZPackage <payload>5__7; private int <chunkIndex>5__8; private ZPackage <payload>5__9; private int <offset>5__10; private int <size>5__11; private byte[] <chunk>5__12; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <RepushLogToServerCoroutine>d__45(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <logBytes>5__1 = null; <steamId>5__2 = null; <modList>5__5 = null; <ex>5__6 = null; <payload>5__7 = null; <payload>5__9 = null; <chunk>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; <logBytes>5__1 = null; try { ClientLogCollector.TryCollect(out <logBytes>5__1, out <modList>5__5); <modList>5__5 = null; } catch (Exception ex) { <ex>5__6 = ex; Debug.LogWarning((object)("[ClientLogRelay] Repush TryCollect failed: " + <ex>5__6.Message)); return false; } <>2__current = null; <>1__state = 1; return true; case 1: <>1__state = -1; <steamId>5__2 = string.Empty; try { <steamId>5__2 = GetOwnSteamId() ?? string.Empty; } catch { } <logBytes>5__1 = <logBytes>5__1 ?? new byte[0]; <logSize>5__3 = <logBytes>5__1.Length; if (<logSize>5__3 <= 409600) { if (ZRoutedRpc.instance == null) { return false; } <payload>5__7 = new ZPackage(); <payload>5__7.Write(<logBytes>5__1); <payload>5__7.Write(<steamId>5__2); ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_UpdateClientLog", new object[1] { <payload>5__7 }); Debug.Log((object)string.Format("[ClientLogRelay] Re-pushed '{0}' to server: {1}B (single message)", "VAG_UpdateClientLog", <logSize>5__3)); return false; } <totalChunks>5__4 = (<logSize>5__3 + 409600 - 1) / 409600; Debug.Log((object)$"[ClientLogRelay] Re-pushing large log to server: {<logSize>5__3}B in {<totalChunks>5__4} chunk(s)"); <chunkIndex>5__8 = 0; break; case 2: <>1__state = -1; <payload>5__9 = null; <chunk>5__12 = null; <chunkIndex>5__8++; break; } if (<chunkIndex>5__8 < <totalChunks>5__4) { if (ZRoutedRpc.instance == null) { return false; } <payload>5__9 = new ZPackage(); <payload>5__9.Write(<chunkIndex>5__8); <payload>5__9.Write(<totalChunks>5__4); <offset>5__10 = <chunkIndex>5__8 * 409600; <size>5__11 = Math.Min(409600, <logSize>5__3 - <offset>5__10); <chunk>5__12 = new byte[<size>5__11]; if (<size>5__11 > 0) { Array.Copy(<logBytes>5__1, <offset>5__10, <chunk>5__12, 0, <size>5__11); } <payload>5__9.Write(<chunk>5__12); <payload>5__9.Write(<steamId>5__2); ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_UpdateClientLog", new object[1] { <payload>5__9 }); if (<totalChunks>5__4 > 1) { Debug.Log((object)$"[ClientLogRelay] -> Re-pushed chunk {<chunkIndex>5__8 + 1}/{<totalChunks>5__4} ({<size>5__11}B)"); } <>2__current = null; <>1__state = 2; return true; } Debug.Log((object)$"[ClientLogRelay] Re-push complete: {<logSize>5__3}B in {<totalChunks>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<long> _postedPeers = new HashSet<long>(); 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<long, string> _peerToPlatformId = new Dictionary<long, string>(); private static readonly Dictionary<long, string> _peerToPlayerName = new Dictionary<long, string>(); 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<ZPackage>("VAG_SubmitClientLog", (Action<long, ZPackage>)OnServerReceiveLog); ZRoutedRpc.instance.Register<ZPackage>("VAG_UpdateClientLog", (Action<long, ZPackage>)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(<DelayedClientPush>d__41))] private static IEnumerator DelayedClientPush() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <DelayedClientPush>d__41(0); } [IteratorStateMachine(typeof(<PushLogToServerCoroutine>d__42))] private static IEnumerator PushLogToServerCoroutine(long serverId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PushLogToServerCoroutine>d__42(0) { serverId = serverId }; } [IteratorStateMachine(typeof(<PeriodicClientRepush>d__43))] private static IEnumerator PeriodicClientRepush() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PeriodicClientRepush>d__43(0); } [IteratorStateMachine(typeof(<PeriodicTransferCleanup>d__44))] private static IEnumerator PeriodicTransferCleanup() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PeriodicTransferCleanup>d__44(0); } [IteratorStateMachine(typeof(<RepushLogToServerCoroutine>d__45))] private static IEnumerator RepushLogToServerCoroutine(long serverId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <RepushLogToServerCoroutine>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<string, string> dictionary = new Dictionary<string, string>(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<string, string> dictionary2 = transferResult.ModList; string steamId2 = transferResult.SteamId; if (dictionary2 == null) { dictionary2 = new Dictionary<string, string>(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<string, string> 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<string, string> dictionary = new Dictionary<string, string>(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<string, string> 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(<PostDisconnectLogAsync>d__52))] private static IEnumerator PostDisconnectLogAsync(string platformId, string playerName, string requestedByName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PostDisconnectLogAsync>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<string> list = new List<string>(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<MinimalWebhookPoster.Attachment> list2 = new List<MinimalWebhookPoster.Attachment>(); list2.Add(new MinimalWebhookPoster.Attachment("session_log_" + text2 + "_" + text5 + ".log", array)); List<MinimalWebhookPoster.Attachment> 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<MinimalWebhookPoster.Attachment> files2 = new List<MinimalWebhookPoster.Attachment> { 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<SourceIssueCount> SourceBreakdown; } private sealed class SourceGroup { public string Source; public int Errors; public int Warnings; public List<string> ErrorLines = new List<string>(); public List<string> WarningLines = new List<string>(); } public static readonly List<string> BenignPatterns = new List<string> { "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 pla