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.";
}
}
}
}