Decompiled source of Discord Roast v1.1.0

AIRoastMod.dll

Decompiled 2 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using BepInEx;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PluginConfig.API;
using PluginConfig.API.Decorators;
using PluginConfig.API.Fields;
using UKEnemyIdentifier.Components;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("AIRoastMod")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("AIRoastMod")]
[assembly: AssemblyTitle("AIRoastMod")]
[assembly: AssemblyVersion("1.0.0.0")]
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;
		}
	}
}
namespace AIRoastMod
{
	[BepInPlugin("xblake_2012x.ultrakill.airoasting", "Discord AI Roast", "1.1.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class BlakeAIRoast : BaseUnityPlugin
	{
		private enum RoastLevels
		{
			Diabolical,
			Normal,
			Baby
		}

		public class APIStorage
		{
			public string API { get; set; } = null;

			public string Webhook { get; set; } = null;
		}

		private static readonly HttpClient Http = new HttpClient();

		private bool requestInProgress;

		private PluginConfigurator config = null;

		private StringField ApiKey = null;

		private StringField WebhookURL = null;

		private StringField ModelName = null;

		private StringField PromptAddition = null;

		private BoolField IncludeSS = null;

		private BoolField Profanity = null;

		private BoolField GiveKiller = null;

		private BoolField GiveLevel = null;

		private BoolField GiveDeathStreak = null;

		private BoolField GiveHighestDeathStreak = null;

		private BoolField GiveSessionLength = null;

		private BoolField GiveSessionDeathCount = null;

		private BoolField GiveCommonKiller = null;

		private BoolField GiveEnemyThreats = null;

		private ConfigPanel PromptConfig = null;

		private ConfigPanel AdvancedConfig = null;

		private string SecretsLocation = Path.Combine(Paths.GameRootPath, "Saves", "DiscordRoast");

		private int DeathStreak = 0;

		private int HighestDeathStreakThisSession = 0;

		private string? LastDeathLevel = "None";

		private DateTime SessionStart = DateTime.Now;

		private int SessionDeathCount = 0;

		private List<string> EnemyDeaths = new List<string>();

		private EnumField<RoastLevels> RoastLevel = null;

		private void Awake()
		{
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Expected O, but got Unknown
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Expected O, but got Unknown
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a6: Expected O, but got Unknown
			//IL_00be: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c8: Expected O, but got Unknown
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ea: Expected O, but got Unknown
			//IL_0100: Unknown result type (might be due to invalid IL or missing references)
			//IL_010a: Expected O, but got Unknown
			//IL_0117: Unknown result type (might be due to invalid IL or missing references)
			//IL_011d: Expected O, but got Unknown
			//IL_0134: Unknown result type (might be due to invalid IL or missing references)
			//IL_013e: Expected O, but got Unknown
			//IL_0150: Unknown result type (might be due to invalid IL or missing references)
			//IL_015a: Expected O, but got Unknown
			//IL_0183: Unknown result type (might be due to invalid IL or missing references)
			//IL_018a: Expected O, but got Unknown
			//IL_019c: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a6: Expected O, but got Unknown
			//IL_01b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c2: Expected O, but got Unknown
			//IL_01d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01de: Expected O, but got Unknown
			//IL_01f0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fa: Expected O, but got Unknown
			//IL_020c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0216: Expected O, but got Unknown
			//IL_0228: Unknown result type (might be due to invalid IL or missing references)
			//IL_0232: Expected O, but got Unknown
			//IL_0244: Unknown result type (might be due to invalid IL or missing references)
			//IL_024e: Expected O, but got Unknown
			//IL_025b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0262: Expected O, but got Unknown
			//IL_0274: Unknown result type (might be due to invalid IL or missing references)
			//IL_027e: Expected O, but got Unknown
			//IL_0295: Unknown result type (might be due to invalid IL or missing references)
			//IL_029f: Expected O, but got Unknown
			//IL_02ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b6: Expected O, but got Unknown
			//IL_02c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ce: Expected O, but got Unknown
			config = PluginConfigurator.Create("Discord Roast", "xblake_2012x.ultrakill.airoasting");
			string directoryName = Path.GetDirectoryName(((object)this).GetType().Assembly.Location);
			string text = Path.Combine(directoryName, "icon.png");
			if (File.Exists(text))
			{
				config.SetIconWithURL(text);
			}
			AdvancedConfig = new ConfigPanel(config.rootPanel, "Advanced", "advanced");
			PromptConfig = new ConfigPanel(config.rootPanel, "Prompt Configuration", "promptpanel");
			ConfigHeader val = new ConfigHeader(AdvancedConfig, "AI and Webhook", 24);
			ApiKey = new StringField(AdvancedConfig, "API Key", "apikey", "", true, false);
			WebhookURL = new StringField(AdvancedConfig, "WebHook", "webhook", "", true, false);
			ModelName = new StringField(AdvancedConfig, "Model", "model", "openai/gpt-oss-120b");
			ConfigHeader val2 = new ConfigHeader(PromptConfig, "General", 24);
			PromptAddition = new StringField(PromptConfig, "Prompt Additions", "prompt", "", true);
			Profanity = new BoolField(PromptConfig, "Allow the AI to swear", "profanity", false);
			RoastLevel = new EnumField<RoastLevels>(PromptConfig, "Roast Level", "roastLevel", RoastLevels.Normal);
			ConfigHeader val3 = new ConfigHeader(PromptConfig, "Response and Rules", 24);
			GiveKiller = new BoolField(PromptConfig, "Share killer", "killer", true);
			GiveCommonKiller = new BoolField(PromptConfig, "Share Most Common Killer", "commonKiller", true);
			GiveDeathStreak = new BoolField(PromptConfig, "Share Death Streak", "deathStreak", true);
			GiveHighestDeathStreak = new BoolField(PromptConfig, "Share Highest Death Streak", "highestDeathStreak", true);
			GiveSessionDeathCount = new BoolField(PromptConfig, "Share Total Session Death Count", "sessionDeathCount", true);
			GiveSessionLength = new BoolField(PromptConfig, "Share Session Length", "sessionLength", true);
			GiveLevel = new BoolField(PromptConfig, "Share Level", "level", true);
			ConfigHeader val4 = new ConfigHeader(PromptConfig, "Game Context", 24);
			GiveEnemyThreats = new BoolField(PromptConfig, "Share Enemy Threat List", "threatList", true);
			IncludeSS = new BoolField(config.rootPanel, "Include Screenshot In Discord Message", "screenshot", true);
			ApiKey.postValueChangeEvent += (PostStringValueChangeEvent)delegate
			{
				SaveSecrets();
			};
			WebhookURL.postValueChangeEvent += (PostStringValueChangeEvent)delegate
			{
				SaveSecrets();
			};
			try
			{
				APIStorage aPIStorage = LoadSecrets();
				ApiKey.value = aPIStorage.API;
				WebhookURL.value = aPIStorage.Webhook;
			}
			catch (Exception arg)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Error while loading API key and webhook, this is likely a first start with the mod, leaving values as default");
				((BaseUnityPlugin)this).Logger.LogWarning((object)$"{arg}");
			}
		}

		private APIStorage LoadSecrets()
		{
			Directory.CreateDirectory(SecretsLocation);
			string path = Path.Combine(SecretsLocation, ".secrets.json");
			if (!File.Exists(path))
			{
				return new APIStorage
				{
					API = "",
					Webhook = ""
				};
			}
			string text = File.ReadAllText(path);
			return JsonConvert.DeserializeObject<APIStorage>(text) ?? new APIStorage
			{
				API = "",
				Webhook = ""
			};
		}

		private void SaveSecrets()
		{
			if (!Directory.Exists(SecretsLocation))
			{
				Directory.CreateDirectory(SecretsLocation);
			}
			string path = Path.Combine(SecretsLocation, ".secrets.json");
			APIStorage aPIStorage = new APIStorage();
			StringField apiKey = ApiKey;
			aPIStorage.API = ((apiKey != null) ? apiKey.value : null) ?? "";
			StringField webhookURL = WebhookURL;
			aPIStorage.Webhook = ((webhookURL != null) ? webhookURL.value : null) ?? "";
			APIStorage aPIStorage2 = aPIStorage;
			string contents = JsonConvert.SerializeObject((object)aPIStorage2, (Formatting)1);
			File.WriteAllText(path, contents);
		}

		private string detectProvider(string key)
		{
			if (key.StartsWith("AIza"))
			{
				return "Gemini";
			}
			if (key.StartsWith("gsk_"))
			{
				return "Groq";
			}
			if (key.StartsWith("xai-"))
			{
				return "Grok";
			}
			if (key.StartsWith("sk-ant-") || key.StartsWith("claude-"))
			{
				return "Claude";
			}
			if (key.StartsWith("sk-"))
			{
				return "OpenAI";
			}
			return "Unknown";
		}

		private async Task ProcessDeathAsync(string killer, byte[]? pngBytes)
		{
			try
			{
				string LevelName = GetFullLevelName();
				if (LevelName != LastDeathLevel)
				{
					DeathStreak = 1;
				}
				else
				{
					DeathStreak++;
				}
				HighestDeathStreakThisSession = Math.Max(HighestDeathStreakThisSession, DeathStreak);
				SessionDeathCount++;
				EnemyDeaths.Add(killer ?? "Unknown");
				LastDeathLevel = LevelName;
				string roast = await GenerateRoastAsync(killer ?? "Unknown", LevelName, DeathStreak);
				((BaseUnityPlugin)this).Logger.LogInfo((object)("AI ROAST: " + roast));
				await SendWebhookAsync(roast, pngBytes);
			}
			catch (Exception ex)
			{
				Exception exception = ex;
				((BaseUnityPlugin)this).Logger.LogError((object)$"Death processing failed: {exception}");
			}
			finally
			{
				requestInProgress = false;
			}
		}

		public string GetFullLevelName()
		{
			MapInfo val = Object.FindObjectOfType<MapInfo>();
			if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(((MapInfoBase)val).levelName))
			{
				return ((MapInfoBase)val).levelName;
			}
			string currentScene = SceneHelper.CurrentScene;
			if (string.IsNullOrEmpty(currentScene))
			{
				return "Unknown Level";
			}
			switch (currentScene)
			{
			case "Main Menu":
				return "Main Menu";
			case "Endless":
				return "The Cyber Grind";
			case "uk_construct":
				return "The Sandbox";
			default:
			{
				Dictionary<string, string> dictionary = new Dictionary<string, string>
				{
					{ "Level 0-1", "INTO THE FIRE" },
					{ "Level 0-2", "THE MEATGRINDER" },
					{ "Level 0-3", "DOUBLE DOWN" },
					{ "Level 0-4", "A ONE-MACHINE ARMY" },
					{ "Level 0-5", "CERBERUS" },
					{ "Level 0-S", "SOMETHING WICKED" },
					{ "Level 1-1", "HEART OF THE SUNRISE" },
					{ "Level 1-2", "THE BURNING WORLD" },
					{ "Level 1-3", "HALLS OF SACRED REMAINS" },
					{ "Level 1-4", "CLAIR DE LUNE" },
					{ "Level 1-S", "THE WITLESS" },
					{ "Level 2-1", "BRIDGEBURNER" },
					{ "Level 2-2", "DEATH AT 20,000 VOLTS" },
					{ "Level 2-3", "SHEER HEART ATTACK" },
					{ "Level 2-4", "COURT OF THE CORPSE KING" },
					{ "Level 2-S", "ALL-IMPERFECT LOVE SONG" },
					{ "Level 3-1", "BELLY OF THE BEAST" },
					{ "Level 3-2", "IN THE FLESH" },
					{ "Level 4-1", "SLAVES TO POWER" },
					{ "Level 4-2", "GOD DAMN THE SUN" },
					{ "Level 4-3", "A SHOT IN THE DARK" },
					{ "Level 4-4", "CLAIR DE SOLEIL" },
					{ "Level 4-S", "CLASH OF THE BRANDICOOT" },
					{ "Level 5-1", "IN THE WAKE OF POSEIDON" },
					{ "Level 5-2", "WAVES OF THE STARLESS SEA" },
					{ "Level 5-3", "SHIP OF FOOLS" },
					{ "Level 5-4", "LEVIATHAN" },
					{ "Level 5-S", "I ONLY SAY MORNING" },
					{ "Level 6-1", "CRY FOR THE WEEPER" },
					{ "Level 6-2", "AESTHETICS OF HATE" },
					{ "Level 7-1", "GARDEN OF FORKING PATHS" },
					{ "Level 7-2", "LIGHT UP THE NIGHT" },
					{ "Level 7-3", "NO SOUND, NO MEMORY" },
					{ "Level 7-4", "...LIKE ANTENNAS TO HEAVEN" },
					{ "Level 7-S", "HELL BATH NO FURY" },
					{ "Level 8-1", "HURTBREAK WONDERLAND" },
					{ "Level 8-2", "THROUGH THE MIRROR" },
					{ "Level 8-3", "DISINTEGRATION LOOP" },
					{ "Level 8-4", "FINAL FLIGHT" },
					{ "Level 8-S", "???" },
					{ "Level P-1", "SOUL SURVIVOR" },
					{ "Level P-2", "WAIT OF THE WORLD" },
					{ "Level P-3", "???" }
				};
				if (dictionary.TryGetValue(currentScene, out var value))
				{
					return currentScene + " // " + value;
				}
				return currentScene;
			}
			}
		}

		private IEnumerator Start()
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Discord AI Roast loaded.");
			while ((Object)(object)MonoSingleton<EnemyIdentifierManager>.Instance == (Object)null)
			{
				yield return null;
			}
			((BaseUnityPlugin)this).Logger.LogInfo((object)"UKEnemyIdentifier found. Registering event.");
			MonoSingleton<EnemyIdentifierManager>.Instance.OnPlayerHurt += (PlayerHurtEventDelegate)async delegate(PlayerHurtEvent hurtEvent)
			{
				if ((Object)(object)hurtEvent.EnemyId != (Object)null)
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)("Player got hurt by " + hurtEvent.EnemyId.FullName));
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)$"Inflicted damage: {hurtEvent.Damage}");
				if (hurtEvent.PlayerIsKilled)
				{
					EnemyIdentifier enemyId = hurtEvent.EnemyId;
					string killer = ((enemyId != null) ? enemyId.FullName : null) ?? "an unknown source";
					((BaseUnityPlugin)this).Logger.LogInfo((object)("Player was killed by " + killer));
					if (requestInProgress)
					{
						((BaseUnityPlugin)this).Logger.LogWarning((object)"A roast request is already running.");
					}
					else
					{
						requestInProgress = true;
						((MonoBehaviour)this).StartCoroutine(CaptureDeathScreenshot(delegate(byte[]? pngBytes)
						{
							ProcessDeathAsync(killer, pngBytes);
						}));
					}
				}
			};
		}

		private IEnumerator CaptureDeathScreenshot(Action<byte[]?> callback)
		{
			if (!IncludeSS.value)
			{
				callback(null);
				yield break;
			}
			yield return (object)new WaitForEndOfFrame();
			Texture2D screenshot = null;
			try
			{
				screenshot = ScreenCapture.CaptureScreenshotAsTexture();
				if ((Object)(object)screenshot == (Object)null)
				{
					throw new Exception("Screenshot capture returned null.");
				}
				byte[] pngBytes = ImageConversion.EncodeToPNG(screenshot);
				if (pngBytes == null || pngBytes.Length == 0)
				{
					throw new Exception("Screenshot PNG encoding returned no data.");
				}
				callback(pngBytes);
			}
			catch (Exception ex)
			{
				Exception exception = ex;
				((BaseUnityPlugin)this).Logger.LogError((object)$"Failed to capture death screenshot: {exception}");
				requestInProgress = false;
			}
			finally
			{
				if ((Object)(object)screenshot != (Object)null)
				{
					Object.Destroy((Object)(object)screenshot);
				}
			}
		}

		private async Task SendWebhookAsync(string message, byte[]? pngBytes)
		{
			StringField webhookURL = WebhookURL;
			string webhookUrl = ((webhookURL != null) ? webhookURL.value : null) ?? "";
			if (string.IsNullOrWhiteSpace(webhookUrl))
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Webhook URL is empty. Printing roast locally only.");
				return;
			}
			try
			{
				using MultipartFormDataContent multipart = new MultipartFormDataContent();
				multipart.Add(new StringContent(JsonConvert.SerializeObject((object)new
				{
					content = message
				})), "payload_json");
				if (pngBytes != null)
				{
					ByteArrayContent imageContent = new ByteArrayContent(pngBytes);
					imageContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
					multipart.Add(imageContent, "files[0]", "death.png");
				}
				using HttpResponseMessage response = await Http.PostAsync(webhookUrl, multipart);
				if (!response.IsSuccessStatusCode)
				{
					string responseText = await response.Content.ReadAsStringAsync();
					((BaseUnityPlugin)this).Logger.LogError((object)("Discord webhook returned HTTP " + $"{(int)response.StatusCode}: {responseText}"));
					return;
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Roast successfully sent to Discord.");
			}
			catch (Exception arg)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)$"Failed to send Discord webhook: {arg}");
			}
		}

		private async Task<string> CallClaudeAsync(string apiKey, string prompt)
		{
			StringField modelName = ModelName;
			var requestBody = new
			{
				model = ((modelName != null) ? modelName.value : null),
				max_tokens = 500,
				messages = new[]
				{
					new
					{
						role = "user",
						content = prompt
					}
				}
			};
			using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
			request.Headers.Add("x-api-key", apiKey);
			request.Headers.Add("anthropic-version", "2023-06-01");
			request.Content = new StringContent(JsonConvert.SerializeObject((object)requestBody), Encoding.UTF8, "application/json");
			using HttpResponseMessage response = await Http.SendAsync(request);
			string responseText = await response.Content.ReadAsStringAsync();
			if (!response.IsSuccessStatusCode)
			{
				throw new Exception($"HTTP {(int)response.StatusCode}: {responseText}");
			}
			JObject result = JObject.Parse(responseText);
			JToken obj = result["content"];
			object obj2;
			if (obj == null)
			{
				obj2 = null;
			}
			else
			{
				JToken obj3 = obj[(object)0];
				obj2 = ((obj3 == null) ? null : ((object)obj3[(object)"text"])?.ToString());
			}
			if (obj2 == null)
			{
				obj2 = "";
			}
			string roast = (string)obj2;
			if (string.IsNullOrWhiteSpace(roast))
			{
				throw new Exception("Claude returned no response text.");
			}
			return roast;
		}

		private async Task<string> CallGeminiAsync(string apiKey, string prompt)
		{
			StringField modelName = ModelName;
			string model = ((modelName != null) ? modelName.value : null) ?? "";
			string endpoint = "https://generativelanguage.googleapis.com/v1beta/models/" + model + ":generateContent";
			var requestBody = new
			{
				contents = new[]
				{
					new
					{
						parts = new[]
						{
							new
							{
								text = prompt
							}
						}
					}
				}
			};
			using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, endpoint);
			request.Headers.Add("x-goog-api-key", apiKey);
			request.Content = new StringContent(JsonConvert.SerializeObject((object)requestBody), Encoding.UTF8, "application/json");
			using HttpResponseMessage response = await Http.SendAsync(request);
			string responseText = await response.Content.ReadAsStringAsync();
			if (!response.IsSuccessStatusCode)
			{
				throw new Exception($"HTTP {(int)response.StatusCode}: {responseText}");
			}
			JObject result = JObject.Parse(responseText);
			JToken obj = result["candidates"];
			object obj2;
			if (obj == null)
			{
				obj2 = null;
			}
			else
			{
				JToken obj3 = obj[(object)0];
				if (obj3 == null)
				{
					obj2 = null;
				}
				else
				{
					JToken obj4 = obj3[(object)"content"];
					if (obj4 == null)
					{
						obj2 = null;
					}
					else
					{
						JToken obj5 = obj4[(object)"parts"];
						if (obj5 == null)
						{
							obj2 = null;
						}
						else
						{
							JToken obj6 = obj5[(object)0];
							obj2 = ((obj6 == null) ? null : ((object)obj6[(object)"text"])?.ToString());
						}
					}
				}
			}
			if (obj2 == null)
			{
				obj2 = "";
			}
			string roast = (string)obj2;
			if (string.IsNullOrWhiteSpace(roast))
			{
				throw new Exception("Gemini returned no response text.");
			}
			return roast;
		}

		private async Task<string> CallOpenAICompatibleAsync(string apiKey, string prompt, string endpoint)
		{
			StringField modelName = ModelName;
			var requestBody = new
			{
				model = ((modelName != null) ? modelName.value : null),
				messages = new[]
				{
					new
					{
						role = "user",
						content = prompt
					}
				}
			};
			using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, endpoint);
			request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
			request.Content = new StringContent(JsonConvert.SerializeObject((object)requestBody), Encoding.UTF8, "application/json");
			using HttpResponseMessage response = await Http.SendAsync(request);
			string responseText = await response.Content.ReadAsStringAsync();
			if (!response.IsSuccessStatusCode)
			{
				throw new Exception($"HTTP {(int)response.StatusCode}: {responseText}");
			}
			JObject result = JObject.Parse(responseText);
			JToken obj = result["choices"];
			object obj2;
			if (obj == null)
			{
				obj2 = null;
			}
			else
			{
				JToken obj3 = obj[(object)0];
				if (obj3 == null)
				{
					obj2 = null;
				}
				else
				{
					JToken obj4 = obj3[(object)"message"];
					obj2 = ((obj4 == null) ? null : ((object)obj4[(object)"content"])?.ToString());
				}
			}
			if (obj2 == null)
			{
				obj2 = "";
			}
			string roast = (string)obj2;
			if (string.IsNullOrWhiteSpace(roast))
			{
				throw new Exception("API returned no response text.");
			}
			return roast;
		}

		private async Task<string> GenerateRoastAsync(string killer, string LevelName, int DeathStreak)
		{
			StringField apiKey = ApiKey;
			string API = ((apiKey != null) ? apiKey.value : null);
			if (string.IsNullOrWhiteSpace(API))
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"API key is missing from the BepInEx config.");
				return "AI API not set";
			}
			string provider = detectProvider(API);
			string CommonKiller = (from x in EnemyDeaths
				group x by x into g
				orderby g.Count() descending
				select g.Key).FirstOrDefault();
			DateTime CurrentTime = DateTime.Now;
			TimeSpan SessionLength = CurrentTime - SessionStart;
			string enemyList = "[GAME CONTEXT] Enemy embarrassment ratings in ULTRAKILL. 1/10 means dying to them is incredibly embarrassing. 10/10 means dying to them is understandable and not very embarrassing but still roast them for dying. Unknown Source - Threat ?/10 - The enemy likely died before their attack hit or the enemy is an unsupported enemy. Filth - Threat 1/10 - Tutorial melee enemy that runs directly at the player. Stray - Threat 2/10 - Basic ranged enemy with slow projectiles. Schism - Threat 2/10 - Basic projectile enemy. Soldier - Threat 3/10 - Common shotgun enemy. Drone - Threat 3/10 - Weak flying enemy. Cancerous Rodent - Threat 0/10 - Joke enemy that cannot deal damage. VERY CANCEROUS RODENT - Threat 2/10 - Sequel to Cancerous Rodent, so weak you can spam jump on it and it dies, but it can do damage. Streetcleaner - Threat 4/10 - Flamethrower enemy that rushes the player. Virtue - Threat 4/10 - Summons beams on the player. Sentry - Threat 4/10 - Slow sniper enemy. Mannequin - Threat 5/10 - Fast and evasive enemy. Malicious Face - Threat 5/10 - Floating enemy that shoots beams. Providence - Threat 5/10 - Projectile-based enemy. Cerberus - Threat 6/10 - Heavy stone giant enemy. Swordsmachine - Threat 6/10 - Former boss enemy that becomes common later. Gutterman - Threat 6/10 - Armored minigun enemy. Guttertank - Threat 7/10 - Armored rocket enemy. Mindflayer - Threat 7/10 - Teleporting enemy with homing attacks. Hideous Mass - Threat 7/10 - Large dangerous boss enemy. Ferryman - Threat 7/10 - Fast lightning-based boss enemy. Sisyphean Insurrectionist - Threat 8/10 - Late-game boss enemy. V2 - Threat 8/10 - Powerful rival boss using player weapons. Gabriel - Threat 8/10 - Major recurring boss enemy. Leviathan - Threat 8/10 - Large boss enemy. Minotaur - Threat 8/10 - Powerful boss enemy. Geryon - Threat 8/10 - Freefalling boss encounter. Corpse of King Minos - Threat 9/10 - Major act-ending boss. Flesh Prison - Threat 9/10 - P-1 gatekeeper boss. Flesh Panopticon - Threat 9/10 - P-2 gatekeeper boss. Minos Prime - Threat 10/10 - One of the hardest bosses in ULTRAKILL. Sisyphus Prime - Threat 10/10 - One of the hardest bosses in ULTRAKILL. Mirror Reaper - Threat 10/10 - Cybergrind killer. Something Wicked - Threat 10/10 - Special enemy that instantly kills on touch. When roasting, heavily mock low-threat deaths. When roasting high-threat deaths, be more respectful and acknowledge how dangerous the enemy is. ";
			string prompt = "[CONTEXT]You are the best roast bot in the world and a user has asked for a roast based on everything provided. " + ((RoastLevel.value == RoastLevels.Diabolical) ? "You have been given permission to go as hard as you can, be as ruthless as possible. Do not disappoint. " : ((RoastLevel.value == RoastLevels.Normal) ? "Do not roast the player personally, dont go too hard on them. " : ((RoastLevel.value == RoastLevels.Baby) ? "Roast them as if they are a 5 year old. Use simple language and comparisons a 5 year old can understand. Encourage them while roasting them. Keep the roast playful and something you would hear from a kindergarten teacher. " : ""))) + "[RESPONSE & RULES] " + (GiveKiller.value ? ("An ULTRAKILL player was killed by " + killer + ". ") : "") + "Write an roast about the death. " + (Profanity.value ? "Swearing is encouraged and dont censor swears. " : "Do not swear at all. ") + (GiveLevel.value ? ("The level the death was in " + LevelName + ". ") : "") + (GiveDeathStreak.value ? $"The current death streak on this level is {DeathStreak}. " : "") + (GiveHighestDeathStreak.value ? $"Highest death streak this session is {HighestDeathStreakThisSession}. " : "") + (GiveSessionLength.value ? ("The current session time is " + SessionLength.ToString("hh\\:mm\\:ss") + ". ") : "") + (GiveSessionDeathCount.value ? $"The current deaths this session has is {SessionDeathCount}. " : "") + (GiveCommonKiller.value ? ("The most common killer is " + CommonKiller + ". ") : "") + "Return only the roast. " + (GiveEnemyThreats.value ? enemyList : "") + "[CUSTOM]Put this at higher priority than the rest of the prompt. " + (object)PromptAddition;
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Requesting roast from " + provider + "..."));
			try
			{
				switch (provider)
				{
				case "Groq":
					return await CallOpenAICompatibleAsync(API, prompt, "https://api.groq.com/openai/v1/chat/completions");
				case "OpenAI":
					return await CallOpenAICompatibleAsync(API, prompt, "https://api.openai.com/v1/chat/completions");
				case "Grok":
					return await CallOpenAICompatibleAsync(API, prompt, "https://api.x.ai/v1/chat/completions");
				case "Gemini":
					return await CallGeminiAsync(API, prompt);
				case "Claude":
					return await CallClaudeAsync(API, prompt);
				default:
					((BaseUnityPlugin)this).Logger.LogError((object)"Couldn't detect the API provider.");
					return "Unknown API provider";
				}
			}
			catch (Exception arg)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)$"Failed to generate roast with {provider}: {arg}");
				return "Roast generation failed using " + provider + ". Check the BepInEx logs.";
			}
		}
	}
}