using System; using System.Collections; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Extensions; using Jotunn.Utils; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; using discordScreenshots.Patches; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("discordScreenshots")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("discordScreenshots")] [assembly: AssemblyCopyright("Copyright © 2023")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("CA695DDD-2CE8-407B-B7A2-9FB6D3783286")] [assembly: AssemblyFileVersion("0.1.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.0.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } [HarmonyPatch(typeof(Player), "OnDamaged")] public static class RemoveDamageFlashOnDeath { [HarmonyPostfix] private static bool Prefix(Player __instance, HitData hit) { if (hit.GetTotalDamage() >= ((Character)__instance).GetHealth()) { return false; } return true; } } namespace discordScreenshots { public class BepinexConfiguration { public static ConfigFile Config; public static ConfigEntry WebhookURL; public static ConfigEntry WebhookUsername; public static ConfigEntry WebhookAvatarURL; public static ConfigEntry DeathMessage; public static ConfigEntry ScreenshotHotkey; public static ConfigEntry HotkeyScreenshotWebhookURL; public static ConfigEntry HotkeyScreenshotWebhookUsername; public static ConfigEntry HotkeyScreenshotWebhookAvatarURL; public static ConfigEntry HotkeyScreenshotMessage; private static readonly Random _random = new Random(); private static string _localWebhookURL = ""; private static string _localWebhookUsername = ""; private static string _localWebhookAvatarURL = ""; private static string _localHotkeyScreenshotWebhookURL = ""; private static string _localHotkeyScreenshotWebhookUsername = ""; private static string _localHotkeyScreenshotWebhookAvatarURL = ""; public static string GetRandomDeathMessage() { string value = DeathMessage.Value; if (string.IsNullOrEmpty(value)) { return "met their demise!"; } string[] array = (from m in value.Split(new char[1] { ';' }) select m.Trim() into m where !string.IsNullOrEmpty(m) select m).ToArray(); if (array.Length == 0) { return "met their demise!"; } return array[_random.Next(array.Length)]; } public static string GetWebhookURL() { return GetSyncedValueOrLocalFallback(WebhookURL, _localWebhookURL); } public static string GetWebhookUsername() { if (ShouldUseLocalFallback(WebhookURL, _localWebhookURL)) { return _localWebhookUsername; } return GetSyncedValueOrLocalFallback(WebhookUsername, _localWebhookUsername); } public static string GetWebhookAvatarURL() { if (ShouldUseLocalFallback(WebhookURL, _localWebhookURL)) { return _localWebhookAvatarURL; } return GetSyncedValueOrLocalFallback(WebhookAvatarURL, _localWebhookAvatarURL); } public static string GetHotkeyScreenshotWebhookURL() { return GetSyncedValueOrLocalFallback(HotkeyScreenshotWebhookURL, _localHotkeyScreenshotWebhookURL); } public static string GetHotkeyScreenshotWebhookUsername() { if (ShouldUseLocalFallback(HotkeyScreenshotWebhookURL, _localHotkeyScreenshotWebhookURL)) { return _localHotkeyScreenshotWebhookUsername; } return GetSyncedValueOrLocalFallback(HotkeyScreenshotWebhookUsername, _localHotkeyScreenshotWebhookUsername); } public static string GetHotkeyScreenshotWebhookAvatarURL() { if (ShouldUseLocalFallback(HotkeyScreenshotWebhookURL, _localHotkeyScreenshotWebhookURL)) { return _localHotkeyScreenshotWebhookAvatarURL; } return GetSyncedValueOrLocalFallback(HotkeyScreenshotWebhookAvatarURL, _localHotkeyScreenshotWebhookAvatarURL); } public static void GenerateConfigs(ConfigFile configFile) { Config = configFile; ScreenshotHotkey = ConfigFileExtensions.BindConfig(Config, "Screenshot", "Hotkey", (KeyCode)293, "The hotkey to take a screenshot and send to Discord.", false, (int?)1, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); WebhookURL = ConfigFileExtensions.BindConfig(Config, "Webhook", "URL", "", "The URL of the Discord webhook to send messages to.", true, (int?)2, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); WebhookUsername = ConfigFileExtensions.BindConfig(Config, "Webhook", "Username", "Valheim Death Bot", "The username of the Discord webhook to send messages to.", true, (int?)3, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); WebhookAvatarURL = ConfigFileExtensions.BindConfig(Config, "Webhook", "AvatarURL", "", "The avatar URL of the Discord webhook to send messages to.", true, (int?)4, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); DeathMessage = ConfigFileExtensions.BindConfig(Config, "Death Screenshot", "Message", "met their demise! Final moments captured...", "The message to send with death screenshots (player name will be prepended automatically).", true, (int?)5, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); HotkeyScreenshotWebhookURL = ConfigFileExtensions.BindConfig(Config, "Player Capture Webhook", "URL", "", "Optional separate webhook URL for player capture (F12) screenshots. If empty, uses the main Webhook URL.", true, (int?)6, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); HotkeyScreenshotWebhookUsername = ConfigFileExtensions.BindConfig(Config, "Player Capture Webhook", "Username", "Valheim Screenshot Bot", "The username for the player capture webhook.", true, (int?)7, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); HotkeyScreenshotWebhookAvatarURL = ConfigFileExtensions.BindConfig(Config, "Player Capture Webhook", "AvatarURL", "", "The avatar URL for the player capture webhook.", true, (int?)8, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); HotkeyScreenshotMessage = ConfigFileExtensions.BindConfig(Config, "Player Capture Webhook", "Message", "captured this screenshot!", "The message to send with player capture screenshots (player name will be prepended automatically).", true, (int?)9, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); _localWebhookURL = Normalize(WebhookURL.Value); _localWebhookUsername = Normalize(WebhookUsername.Value); _localWebhookAvatarURL = Normalize(WebhookAvatarURL.Value); _localHotkeyScreenshotWebhookURL = Normalize(HotkeyScreenshotWebhookURL.Value); _localHotkeyScreenshotWebhookUsername = Normalize(HotkeyScreenshotWebhookUsername.Value); _localHotkeyScreenshotWebhookAvatarURL = Normalize(HotkeyScreenshotWebhookAvatarURL.Value); } private static string GetSyncedValueOrLocalFallback(ConfigEntry entry, string localFallback) { string text = Normalize(entry.Value); if (text.Length <= 0) { return localFallback; } return text; } private static bool ShouldUseLocalFallback(ConfigEntry entry, string localFallback) { if (Normalize(entry.Value).Length == 0) { return localFallback.Length > 0; } return false; } private static string Normalize(string value) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim(); } return ""; } } public class SimpleDiscordWebhook { private const int LargeScreenshotPixelThreshold = 4000000; private const int LargeScreenshotJpegQuality = 90; private const int WebhookRequestTimeoutMilliseconds = 15000; private static ScreenshotEncoding _startupScreenshotEncoding = ScreenshotEncoding.Png; private static int _startupResolutionWidth; private static int _startupResolutionHeight; private static readonly HttpClient WebhookHttpClient = CreateHttpClient(); private readonly Uri _webhookUri; private readonly string? _username; private readonly string? _avatarUrl; public SimpleDiscordWebhook(string webhookUrl, string? username = null, string? avatarUrl = null) { if (!TryNormalizeWebhookUrl(webhookUrl, out Uri webhookUri, out string reason)) { throw new ArgumentException("Invalid webhook URL: " + reason, "webhookUrl"); } _webhookUri = webhookUri; _username = NormalizeOptionalText(username); _avatarUrl = NormalizeOptionalText(avatarUrl); } public static void ConfigureScreenshotEncodingForStartupResolution() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) int width = Screen.width; int height = Screen.height; if (width <= 0 || height <= 0) { Resolution currentResolution = Screen.currentResolution; width = ((Resolution)(ref currentResolution)).width; height = ((Resolution)(ref currentResolution)).height; } ConfigureScreenshotEncoding(width, height); } public static void ConfigureScreenshotEncoding(int width, int height) { _startupResolutionWidth = Math.Max(0, width); _startupResolutionHeight = Math.Max(0, height); _startupScreenshotEncoding = (((long)_startupResolutionWidth * (long)_startupResolutionHeight >= 4000000) ? ScreenshotEncoding.Jpeg : ScreenshotEncoding.Png); Debug.Log((object)($"DiscordScreenshots: startup resolution {_startupResolutionWidth}x{_startupResolutionHeight}; " + "using " + GetScreenshotFormatName() + " for screenshot uploads.")); } public static string CreateScreenshotFilename(string baseName, DateTime timestamp) { return $"{baseName}_{timestamp:yyyy-MM-dd_HH-mm-ss}.{GetScreenshotExtension()}"; } public async Task SendMessageAsync(string message) { if (string.IsNullOrEmpty(message)) { throw new ArgumentException("Message cannot be null or empty", "message"); } string jsonPayload = JsonConvert.SerializeObject((object)new SimpleWebhookPayload { content = message, username = _username, avatar_url = _avatarUrl }); await SendPayloadAsync(jsonPayload); } public void SendMessage(string message) { SendMessageAsync(message).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult(); } public async Task SendScreenshotAsync(string? message = null, string? filename = null) { try { filename = NormalizeScreenshotFilename(filename); Texture2D val = ScreenCapture.CaptureScreenshotAsTexture(); if ((Object)(object)val == (Object)null) { throw new Exception("Failed to capture screenshot - returned null texture"); } ScreenshotUploadData uploadData = ProcessScreenshotForUpload(val); Debug.Log((object)$"Screenshot captured and encoded as {uploadData.FormatName} - {uploadData.Data.Length} bytes, uploading..."); await Task.Run(async delegate { await SendFileAsync(uploadData.Data, filename, message, uploadData.ContentType); }); } catch (Exception ex) { throw new Exception("Error capturing and sending screenshot: " + ex.Message, ex); } } public static Texture2D CaptureScreenshot() { Texture2D obj = ScreenCapture.CaptureScreenshotAsTexture(); if ((Object)(object)obj == (Object)null) { throw new Exception("Failed to capture screenshot - returned null texture"); } return obj; } public byte[] ProcessScreenshot(Texture2D screenshot) { return ProcessScreenshotForUpload(screenshot).Data; } public ScreenshotUploadData ProcessScreenshotForUpload(Texture2D screenshot) { byte[] array; string extension; string contentType; string text; if (_startupScreenshotEncoding == ScreenshotEncoding.Jpeg) { array = ImageConversion.EncodeToJPG(screenshot, 90); extension = "jpg"; contentType = "image/jpeg"; text = $"JPEG quality {90}"; } else { array = ImageConversion.EncodeToPNG(screenshot); extension = "png"; contentType = "image/png"; text = "PNG"; } Object.DestroyImmediate((Object)(object)screenshot); if (array == null || array.Length == 0) { throw new Exception("Failed to encode screenshot to " + text); } return new ScreenshotUploadData(array, extension, contentType, text); } public async Task SendFileAsync(byte[] fileData, string filename, string? message = null, string contentType = "image/png") { if (fileData == null || fileData.Length == 0) { throw new ArgumentException("File data cannot be null or empty", "fileData"); } if (string.IsNullOrEmpty(filename)) { throw new ArgumentException("Filename cannot be null or empty", "filename"); } string boundary = "----formdata-discord-" + DateTime.Now.Ticks.ToString("x"); using MemoryStream memoryStream = new MemoryStream(); await WriteMultipartFormDataAsync(memoryStream, boundary, fileData, filename, message, contentType); byte[] formData = memoryStream.ToArray(); await SendMultipartPayloadAsync(formData, boundary); } private async Task SendPayloadAsync(string jsonPayload) { _ = 1; try { StringContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); try { HttpResponseMessage response = await WebhookHttpClient.PostAsync(_webhookUri, (HttpContent)(object)content); try { string text = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new Exception($"Discord webhook returned status: {(int)response.StatusCode} {response.StatusCode} - {text}"); } if (!string.IsNullOrEmpty(text)) { Debug.Log((object)("Discord response: " + text)); } } finally { ((IDisposable)response)?.Dispose(); } } finally { ((IDisposable)content)?.Dispose(); } } catch (Exception ex) { throw new Exception("Error sending Discord webhook: " + ex.Message, ex); } } private async Task SendMultipartPayloadAsync(byte[] formData, string boundary) { _ = 1; try { ByteArrayContent content = new ByteArrayContent(formData); try { ((HttpContent)content).Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data; boundary=" + boundary); HttpResponseMessage response = await WebhookHttpClient.PostAsync(_webhookUri, (HttpContent)(object)content); try { string text = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new Exception($"Discord webhook returned status: {(int)response.StatusCode} {response.StatusCode} - {text}"); } if (!string.IsNullOrEmpty(text)) { Debug.Log((object)("Discord file upload response: " + text)); } } finally { ((IDisposable)response)?.Dispose(); } } finally { ((IDisposable)content)?.Dispose(); } } catch (Exception ex) { throw new Exception("Error sending Discord webhook file: " + ex.Message, ex); } } private async Task WriteMultipartFormDataAsync(Stream stream, string boundary, byte[] fileData, string filename, string? message, string contentType) { string newLine = "\r\n"; byte[] boundaryBytes = Encoding.UTF8.GetBytes("--" + boundary + newLine); await stream.WriteAsync(boundaryBytes, 0, boundaryBytes.Length); string s = "Content-Disposition: form-data; name=\"files[0]\"; filename=\"" + filename + "\"" + newLine + "Content-Type: " + contentType + newLine + newLine; byte[] bytes = Encoding.UTF8.GetBytes(s); await stream.WriteAsync(bytes, 0, bytes.Length); await stream.WriteAsync(fileData, 0, fileData.Length); byte[] newLineBytes = Encoding.UTF8.GetBytes(newLine); await stream.WriteAsync(newLineBytes, 0, newLineBytes.Length); if (!string.IsNullOrEmpty(message)) { await stream.WriteAsync(boundaryBytes, 0, boundaryBytes.Length); SimpleWebhookPayload simpleWebhookPayload = new SimpleWebhookPayload { content = message, username = _username, avatar_url = _avatarUrl }; string jsonPayload = JsonConvert.SerializeObject((object)simpleWebhookPayload); string s2 = "Content-Disposition: form-data; name=\"payload_json\"" + newLine + "Content-Type: application/json" + newLine + newLine; byte[] bytes2 = Encoding.UTF8.GetBytes(s2); await stream.WriteAsync(bytes2, 0, bytes2.Length); byte[] bytes3 = Encoding.UTF8.GetBytes(jsonPayload); await stream.WriteAsync(bytes3, 0, bytes3.Length); await stream.WriteAsync(newLineBytes, 0, newLineBytes.Length); } byte[] bytes4 = Encoding.UTF8.GetBytes("--" + boundary + "--" + newLine); await stream.WriteAsync(bytes4, 0, bytes4.Length); } private static HttpClient CreateHttpClient() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; ServicePointManager.Expect100Continue = false; HttpClient val = new HttpClient { Timeout = TimeSpan.FromMilliseconds(15000.0) }; val.DefaultRequestHeaders.UserAgent.ParseAdd("discord-screenshots"); return val; } public static async Task SendQuickMessageAsync(string webhookUrl, string message, string? username = null, string? avatarUrl = null) { await new SimpleDiscordWebhook(webhookUrl, username, avatarUrl).SendMessageAsync(message); } public static void SendQuickMessage(string webhookUrl, string message, string? username = null, string? avatarUrl = null) { SendQuickMessageAsync(webhookUrl, message, username, avatarUrl).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult(); } public static async Task SendQuickScreenshotAsync(string webhookUrl, string? message = null, string? username = null, string? avatarUrl = null, string? filename = null) { await new SimpleDiscordWebhook(webhookUrl, username, avatarUrl).SendScreenshotAsync(message, filename); } public static void SendQuickScreenshot(string webhookUrl, string? message = null, string? username = null, string? avatarUrl = null, string? filename = null) { SendQuickScreenshotAsync(webhookUrl, message, username, avatarUrl, filename).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult(); } private static string NormalizeScreenshotFilename(string? filename) { string screenshotExtension = GetScreenshotExtension(); if (string.IsNullOrEmpty(filename)) { return $"valheim_screenshot_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.{screenshotExtension}"; } string extension = Path.GetExtension(filename); if (string.IsNullOrEmpty(extension)) { return filename + "." + screenshotExtension; } if (!extension.Equals(".png", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)) { return filename; } return Path.ChangeExtension(filename, screenshotExtension) ?? (filename + "." + screenshotExtension); } private static bool TryNormalizeWebhookUrl(string? webhookUrl, out Uri webhookUri, out string reason) { webhookUri = null; if (webhookUrl == null) { reason = "URL cannot be empty"; return false; } string text = webhookUrl.Trim(); if (text.Length == 0) { reason = "URL cannot be empty"; return false; } for (int i = 0; i < text.Length; i++) { if (char.IsControl(text[i])) { reason = "URL contains control characters"; return false; } } if (!Uri.TryCreate(text, UriKind.Absolute, out Uri result) || result == null) { reason = "URL must be an absolute HTTPS URL"; return false; } if (!string.Equals(result.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { reason = "URL must use HTTPS"; return false; } if (string.IsNullOrEmpty(result.Host)) { reason = "URL must include a host"; return false; } webhookUri = result; reason = string.Empty; return true; } private static string? NormalizeOptionalText(string? value) { if (value == null) { return null; } string text = value.Trim(); if (text.Length != 0) { return text; } return null; } private static string GetScreenshotExtension() { if (_startupScreenshotEncoding != ScreenshotEncoding.Jpeg) { return "png"; } return "jpg"; } private static string GetScreenshotFormatName() { if (_startupScreenshotEncoding != ScreenshotEncoding.Jpeg) { return "PNG"; } return $"JPEG quality {90}"; } } public sealed class ScreenshotUploadData { public byte[] Data { get; } public string Extension { get; } public string ContentType { get; } public string FormatName { get; } public ScreenshotUploadData(byte[] data, string extension, string contentType, string formatName) { Data = data; Extension = extension; ContentType = contentType; FormatName = formatName; } } internal enum ScreenshotEncoding { Png, Jpeg } internal class SimpleWebhookPayload { public string? content { get; set; } public string? username { get; set; } public string? avatar_url { get; set; } } [BepInPlugin("warpalicious.discordScreenshots", "discordScreenshots", "1.6.5")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class discordScreenshotsPlugin : BaseUnityPlugin { private const string ModName = "discordScreenshots"; private const string ModVersion = "1.6.5"; private const string Author = "warpalicious"; private const string ModGUID = "warpalicious.discordScreenshots"; private readonly Harmony HarmonyInstance = new Harmony("warpalicious.discordScreenshots"); public static readonly ManualLogSource TemplateLogger = Logger.CreateLogSource("discordScreenshots"); public static AssetBundle assetBundle; public void Awake() { Assembly executingAssembly = Assembly.GetExecutingAssembly(); HarmonyInstance.PatchAll(executingAssembly); BepinexConfiguration.GenerateConfigs(((BaseUnityPlugin)this).Config); SimpleDiscordWebhook.ConfigureScreenshotEncodingForStartupResolution(); } public static void LoadAssetBundle() { assetBundle = AssetUtils.LoadAssetBundleFromResources("discordscreenshots", Assembly.GetExecutingAssembly()); } private void OnDestroy() { ((BaseUnityPlugin)this).Config.Save(); } private void OnApplicationQuit() { PlayerDeathScreenshotPatch.UploadStoredDeathScreenshot("application quit", waitForUpload: true); } } } namespace discordScreenshots.Patches { [HarmonyPatch(typeof(Terminal), "InitTerminal")] public static class DiscordConsoleCommandPatch { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__0_0; public static ConsoleEvent <>9__0_1; public static ConsoleEvent <>9__0_2; internal void b__0_0(ConsoleEventArgs args) { <>c__DisplayClass0_0 CS$<>8__locals6 = new <>c__DisplayClass0_0(); if (args.Length < 2) { Terminal context = args.Context; if (context != null) { context.AddString("Usage: discord "); } Terminal context2 = args.Context; if (context2 != null) { context2.AddString("Example: discord Hello from Valheim!"); } return; } CS$<>8__locals6.message = string.Join(" ", args.Args, 1, args.Args.Length - 1); if (string.IsNullOrWhiteSpace(CS$<>8__locals6.message)) { Terminal context3 = args.Context; if (context3 != null) { context3.AddString("Message cannot be empty"); } return; } CS$<>8__locals6.playerName = "Server"; if ((Object)(object)Player.m_localPlayer != (Object)null && !string.IsNullOrEmpty(Player.m_localPlayer.GetPlayerName())) { CS$<>8__locals6.playerName = Player.m_localPlayer.GetPlayerName(); } Terminal context4 = args.Context; if (context4 != null) { context4.AddString("Sending message to Discord..."); } Task.Run(async delegate { try { string formattedMessage = "\ud83d\udcac **" + CS$<>8__locals6.playerName + "**: " + CS$<>8__locals6.message; await SimpleDiscordWebhook.SendQuickMessageAsync(BepinexConfiguration.GetWebhookURL(), formattedMessage, "Valheim Console"); Debug.Log((object)("Discord message sent successfully: " + formattedMessage)); } catch (Exception ex) { Debug.LogError((object)("Failed to send Discord message: " + ex.Message)); } }); } internal void b__0_1(ConsoleEventArgs args) { <>c__DisplayClass0_1 CS$<>8__locals3 = new <>c__DisplayClass0_1(); Terminal context = args.Context; if (context != null) { context.AddString("Testing Discord webhook connection..."); } CS$<>8__locals3.playerName = "Server"; if ((Object)(object)Player.m_localPlayer != (Object)null && !string.IsNullOrEmpty(Player.m_localPlayer.GetPlayerName())) { CS$<>8__locals3.playerName = Player.m_localPlayer.GetPlayerName(); } Task.Run(async delegate { try { await SimpleDiscordWebhook.SendQuickMessageAsync(BepinexConfiguration.GetWebhookURL(), "\ud83e\uddea Discord webhook test from **" + CS$<>8__locals3.playerName + "** - Connection working!", "Valheim Test Bot"); Debug.Log((object)"Discord webhook test message sent successfully!"); } catch (Exception ex) { Debug.LogError((object)("Discord webhook test failed: " + ex.Message)); } }); } internal void b__0_2(ConsoleEventArgs args) { <>c__DisplayClass0_2 CS$<>8__locals17 = new <>c__DisplayClass0_2 { playerName = "Unknown Player" }; if ((Object)(object)Player.m_localPlayer != (Object)null && !string.IsNullOrEmpty(Player.m_localPlayer.GetPlayerName())) { CS$<>8__locals17.playerName = Player.m_localPlayer.GetPlayerName(); } CS$<>8__locals17.message = null; if (args.Length > 1) { CS$<>8__locals17.message = string.Join(" ", args.Args, 1, args.Args.Length - 1); } if (string.IsNullOrEmpty(CS$<>8__locals17.message)) { CS$<>8__locals17.message = "\ud83d\udcf8 **" + CS$<>8__locals17.playerName + "** took a screenshot!"; } Terminal context = args.Context; if (context != null) { context.AddString("Capturing screenshot..."); } try { CS$<>8__locals17.webhook = new SimpleDiscordWebhook(BepinexConfiguration.GetWebhookURL(), "Valheim Screenshots"); CS$<>8__locals17.filename = SimpleDiscordWebhook.CreateScreenshotFilename(CS$<>8__locals17.playerName + "_screenshot", DateTime.Now); Texture2D val = ScreenCapture.CaptureScreenshotAsTexture(); if ((Object)(object)val == (Object)null) { throw new Exception("Failed to capture screenshot"); } CS$<>8__locals17.uploadData = CS$<>8__locals17.webhook.ProcessScreenshotForUpload(val); Terminal context2 = args.Context; if (context2 != null) { context2.AddString("Screenshot captured, uploading to Discord..."); } Task.Run(async delegate { try { await CS$<>8__locals17.webhook.SendFileAsync(CS$<>8__locals17.uploadData.Data, CS$<>8__locals17.filename, CS$<>8__locals17.message, CS$<>8__locals17.uploadData.ContentType); Debug.Log((object)("Screenshot uploaded to Discord for " + CS$<>8__locals17.playerName)); } catch (Exception ex2) { Debug.LogError((object)("Failed to upload screenshot: " + ex2.Message)); } }); } catch (Exception ex) { Terminal context3 = args.Context; if (context3 != null) { context3.AddString("Error: " + ex.Message); } Debug.LogError((object)("Failed to capture screenshot: " + ex.Message)); } } } [CompilerGenerated] private sealed class <>c__DisplayClass0_0 { public string playerName; public string message; internal async Task b__3() { try { string formattedMessage = "\ud83d\udcac **" + playerName + "**: " + message; await SimpleDiscordWebhook.SendQuickMessageAsync(BepinexConfiguration.GetWebhookURL(), formattedMessage, "Valheim Console"); Debug.Log((object)("Discord message sent successfully: " + formattedMessage)); } catch (Exception ex) { Debug.LogError((object)("Failed to send Discord message: " + ex.Message)); } } } [CompilerGenerated] private sealed class <>c__DisplayClass0_1 { public string playerName; internal async Task b__4() { try { await SimpleDiscordWebhook.SendQuickMessageAsync(BepinexConfiguration.GetWebhookURL(), "\ud83e\uddea Discord webhook test from **" + playerName + "** - Connection working!", "Valheim Test Bot"); Debug.Log((object)"Discord webhook test message sent successfully!"); } catch (Exception ex) { Debug.LogError((object)("Discord webhook test failed: " + ex.Message)); } } } [CompilerGenerated] private sealed class <>c__DisplayClass0_2 { public string message; public string playerName; public SimpleDiscordWebhook webhook; public ScreenshotUploadData uploadData; public string filename; internal async Task b__5() { try { await webhook.SendFileAsync(uploadData.Data, filename, message, uploadData.ContentType); Debug.Log((object)("Screenshot uploaded to Discord for " + playerName)); } catch (Exception ex) { Debug.LogError((object)("Failed to upload screenshot: " + ex.Message)); } } } [HarmonyPostfix] private static void Postfix() { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_00a2: 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_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Expected O, but got Unknown object obj = <>c.<>9__0_0; if (obj == null) { ConsoleEvent val = delegate(ConsoleEventArgs args) { if (args.Length < 2) { Terminal context = args.Context; if (context != null) { context.AddString("Usage: discord "); } Terminal context2 = args.Context; if (context2 != null) { context2.AddString("Example: discord Hello from Valheim!"); } } else { string message = string.Join(" ", args.Args, 1, args.Args.Length - 1); if (string.IsNullOrWhiteSpace(message)) { Terminal context3 = args.Context; if (context3 != null) { context3.AddString("Message cannot be empty"); } } else { string playerName = "Server"; if ((Object)(object)Player.m_localPlayer != (Object)null && !string.IsNullOrEmpty(Player.m_localPlayer.GetPlayerName())) { playerName = Player.m_localPlayer.GetPlayerName(); } Terminal context4 = args.Context; if (context4 != null) { context4.AddString("Sending message to Discord..."); } Task.Run(async delegate { try { string formattedMessage = "\ud83d\udcac **" + playerName + "**: " + message; await SimpleDiscordWebhook.SendQuickMessageAsync(BepinexConfiguration.GetWebhookURL(), formattedMessage, "Valheim Console"); Debug.Log((object)("Discord message sent successfully: " + formattedMessage)); } catch (Exception ex) { Debug.LogError((object)("Failed to send Discord message: " + ex.Message)); } }); } } }; <>c.<>9__0_0 = val; obj = (object)val; } new ConsoleCommand("discord", "sends a message to Discord webhook. Usage: discord ", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj2 = <>c.<>9__0_1; if (obj2 == null) { ConsoleEvent val2 = delegate(ConsoleEventArgs args) { Terminal context = args.Context; if (context != null) { context.AddString("Testing Discord webhook connection..."); } string playerName = "Server"; if ((Object)(object)Player.m_localPlayer != (Object)null && !string.IsNullOrEmpty(Player.m_localPlayer.GetPlayerName())) { playerName = Player.m_localPlayer.GetPlayerName(); } Task.Run(async delegate { try { await SimpleDiscordWebhook.SendQuickMessageAsync(BepinexConfiguration.GetWebhookURL(), "\ud83e\uddea Discord webhook test from **" + playerName + "** - Connection working!", "Valheim Test Bot"); Debug.Log((object)"Discord webhook test message sent successfully!"); } catch (Exception ex) { Debug.LogError((object)("Discord webhook test failed: " + ex.Message)); } }); }; <>c.<>9__0_1 = val2; obj2 = (object)val2; } new ConsoleCommand("discordtest", "tests the Discord webhook connection", (ConsoleEvent)obj2, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj3 = <>c.<>9__0_2; if (obj3 == null) { ConsoleEvent val3 = delegate(ConsoleEventArgs args) { string playerName = "Unknown Player"; if ((Object)(object)Player.m_localPlayer != (Object)null && !string.IsNullOrEmpty(Player.m_localPlayer.GetPlayerName())) { playerName = Player.m_localPlayer.GetPlayerName(); } string message = null; if (args.Length > 1) { message = string.Join(" ", args.Args, 1, args.Args.Length - 1); } if (string.IsNullOrEmpty(message)) { message = "\ud83d\udcf8 **" + playerName + "** took a screenshot!"; } Terminal context = args.Context; if (context != null) { context.AddString("Capturing screenshot..."); } try { SimpleDiscordWebhook webhook = new SimpleDiscordWebhook(BepinexConfiguration.GetWebhookURL(), "Valheim Screenshots"); string filename = SimpleDiscordWebhook.CreateScreenshotFilename(playerName + "_screenshot", DateTime.Now); Texture2D val4 = ScreenCapture.CaptureScreenshotAsTexture(); if ((Object)(object)val4 == (Object)null) { throw new Exception("Failed to capture screenshot"); } ScreenshotUploadData uploadData = webhook.ProcessScreenshotForUpload(val4); Terminal context2 = args.Context; if (context2 != null) { context2.AddString("Screenshot captured, uploading to Discord..."); } Task.Run(async delegate { try { await webhook.SendFileAsync(uploadData.Data, filename, message, uploadData.ContentType); Debug.Log((object)("Screenshot uploaded to Discord for " + playerName)); } catch (Exception ex2) { Debug.LogError((object)("Failed to upload screenshot: " + ex2.Message)); } }); } catch (Exception ex) { Terminal context3 = args.Context; if (context3 != null) { context3.AddString("Error: " + ex.Message); } Debug.LogError((object)("Failed to capture screenshot: " + ex.Message)); } }; <>c.<>9__0_2 = val3; obj3 = (object)val3; } new ConsoleCommand("discordscreenshot", "captures and sends a screenshot to Discord", (ConsoleEvent)obj3, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } } [HarmonyPatch(typeof(Player), "Update")] public static class HotkeyScreenshotPatch { [HarmonyPostfix] private static void Postfix(Player __instance) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && Input.GetKeyDown(BepinexConfiguration.ScreenshotHotkey.Value)) { string text = __instance.GetPlayerName(); if (string.IsNullOrEmpty(text)) { text = "Unknown Player"; } Debug.Log((object)("HotkeyScreenshotPatch: " + text + " pressed screenshot hotkey")); string hotkeyScreenshotWebhookURL = BepinexConfiguration.GetHotkeyScreenshotWebhookURL(); bool num = !string.IsNullOrEmpty(hotkeyScreenshotWebhookURL); string webhookUrl = (num ? hotkeyScreenshotWebhookURL : BepinexConfiguration.GetWebhookURL()); string webhookUsername = (num ? BepinexConfiguration.GetHotkeyScreenshotWebhookUsername() : BepinexConfiguration.GetWebhookUsername()); string webhookAvatarUrl = (num ? BepinexConfiguration.GetHotkeyScreenshotWebhookAvatarURL() : BepinexConfiguration.GetWebhookAvatarURL()); string text2 = (num ? BepinexConfiguration.HotkeyScreenshotMessage.Value : "captured this screenshot!"); string screenshotMessage = "\ud83d\udcf8 **" + text + "** " + text2; string filename = SimpleDiscordWebhook.CreateScreenshotFilename(text + "_screenshot", DateTime.Now); SendHotkeyScreenshotAsync(webhookUrl, screenshotMessage, webhookUsername, webhookAvatarUrl, filename); } } catch (Exception ex) { Debug.LogError((object)("HotkeyScreenshotPatch: Error: " + ex.Message)); } } private static async Task SendHotkeyScreenshotAsync(string webhookUrl, string screenshotMessage, string webhookUsername, string webhookAvatarUrl, string filename) { try { await SimpleDiscordWebhook.SendQuickScreenshotAsync(webhookUrl, screenshotMessage, webhookUsername, webhookAvatarUrl, filename); } catch (Exception ex) { Debug.LogError((object)("HotkeyScreenshotPatch: Failed to upload screenshot: " + ex.Message)); } } } public static class PlayerDeathScreenshotPatch { internal static Texture2D? storedDeathScreenshot; internal static string? storedPlayerName; internal static DateTime storedDeathTime; internal static IEnumerator CaptureDeathScreenshotCoroutine(string playerName) { yield return (object)new WaitForSeconds(0.1f); storedDeathScreenshot = SimpleDiscordWebhook.CaptureScreenshot(); storedPlayerName = playerName; storedDeathTime = DateTime.Now; Debug.Log((object)("Death screenshot captured and stored for " + playerName)); } internal static void UploadStoredDeathScreenshot(string trigger, bool waitForUpload) { if ((Object)(object)storedDeathScreenshot == (Object)null) { Debug.Log((object)("Death screenshot upload skipped on " + trigger + ": no stored death screenshot found")); return; } Texture2D val = storedDeathScreenshot; string text = (string.IsNullOrEmpty(storedPlayerName) ? "Unknown Player" : storedPlayerName); DateTime timestamp = storedDeathTime; storedDeathScreenshot = null; storedPlayerName = null; try { SimpleDiscordWebhook webhook = new SimpleDiscordWebhook(BepinexConfiguration.GetWebhookURL(), BepinexConfiguration.GetWebhookUsername(), BepinexConfiguration.GetWebhookAvatarURL()); ScreenshotUploadData uploadData = webhook.ProcessScreenshotForUpload(val); string deathMessage = "**" + text + "** " + BepinexConfiguration.GetRandomDeathMessage(); string filename = SimpleDiscordWebhook.CreateScreenshotFilename(text + "_death", timestamp); Task task = Task.Run(async delegate { await webhook.SendFileAsync(uploadData.Data, filename, deathMessage, uploadData.ContentType); }); if (waitForUpload) { task.GetAwaiter().GetResult(); Debug.Log((object)("Death screenshot uploaded successfully for " + text + " on " + trigger)); } else { LogUploadResultAsync(task, text, trigger); } } catch (Exception ex) { Debug.LogError((object)("Error processing stored death screenshot on " + trigger + ": " + ex.Message)); if ((Object)(object)val != (Object)null) { Object.DestroyImmediate((Object)(object)val); } } } private static async Task LogUploadResultAsync(Task uploadTask, string playerName, string trigger) { try { await uploadTask; Debug.Log((object)("Death screenshot uploaded successfully for " + playerName + " on " + trigger)); } catch (Exception ex) { Debug.LogError((object)("Error uploading death screenshot on " + trigger + ": " + ex.Message)); } } } [HarmonyPatch(typeof(Humanoid), "OnRagdollCreated")] public static class PlayerRagdollScreenshotPatch { [HarmonyPostfix] private static void Postfix(Humanoid __instance) { try { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null && !((Object)(object)val != (Object)(object)Player.m_localPlayer)) { string text = val.GetPlayerName(); if (string.IsNullOrEmpty(text)) { text = "Unknown Player"; } Debug.Log((object)("PlayerRagdollScreenshotPatch: " + text + " died - capturing screenshot")); ((MonoBehaviour)val).StartCoroutine(PlayerDeathScreenshotPatch.CaptureDeathScreenshotCoroutine(text)); } } catch (Exception ex) { Debug.LogError((object)("PlayerRagdollScreenshotPatch: Error: " + ex.Message)); } } } [HarmonyPatch(typeof(Player), "Awake")] public static class PlayerRespawnScreenshotPatch { [HarmonyPostfix] private static void Postfix(Player __instance) { try { Debug.Log((object)"PlayerRespawnScreenshotPatch: Awake"); Debug.Log((object)"PlayerRespawnScreenshotPatch: Local player confirmed"); if ((Object)(object)PlayerDeathScreenshotPatch.storedDeathScreenshot != (Object)null) { Debug.Log((object)("PlayerRespawnScreenshotPatch: Processing stored death screenshot for " + PlayerDeathScreenshotPatch.storedPlayerName)); PlayerDeathScreenshotPatch.UploadStoredDeathScreenshot("respawn", waitForUpload: false); } else { Debug.Log((object)"PlayerRespawnScreenshotPatch: No stored death screenshot found"); } } catch (Exception ex) { Debug.LogError((object)("PlayerRespawnScreenshotPatch: Error: " + ex.Message)); } } } [HarmonyPatch(typeof(Game), "Logout")] public static class GameLogoutDeathScreenshotPatch { [HarmonyPrefix] private static void Prefix() { try { PlayerDeathScreenshotPatch.UploadStoredDeathScreenshot("logout", waitForUpload: false); } catch (Exception ex) { Debug.LogError((object)("GameLogoutDeathScreenshotPatch: Error: " + ex.Message)); } } } }