using ConVar; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.IO; using System.Threading.Tasks; using UnityEngine; using System.Collections; // todo: // update message for admins // function calling hooks for all providers // chat bot generator // chat moderation namespace Oxide.Plugins { [Info("RustGPT", "Goo_", CurrentPluginVersion)] [Description("AI chat integration for Rust with support for OpenAI, Anthropic, and XAI. Players can interact with AI from game chat.")] public class RustGPT : RustPlugin { #region The skibidi stuff. private const string CurrentPluginVersion = "1.8.2"; private const string _GithubConfigUrl = "https://raw.githubusercontent.com/Rust-Haus/RustGPT/refs/heads/main/config-defaults.json"; private Dictionary _providers; private IAIProvider _activeProvider; private Regex _questionRegex { get; set; } private PluginConfig _config { get; set; } private Dictionary _lastUsageTime = new Dictionary(); private Dictionary _uriCache = new Dictionary(); private List _imageHistory = new List(); private Dictionary _lastImageCommandUsage = new Dictionary(); private const float IMAGE_COMMAND_COOLDOWN = 10f; private const float MIN_TIME_BETWEEN_REQUESTS = 0.5f; private float _lastRequestTime = 0f; #endregion #region Config Classes private class AIPromptParametersConfig { [JsonProperty("System role")] public string SystemRole { get; set; } [JsonProperty("User Custom Prompt")] public string UserCustomPrompt { get; set; } [JsonProperty("Share Server Name")] public bool ShareServerName { get; set; } [JsonProperty("Share Server Description")] public bool ShareServerDescription { get; set; } [JsonProperty("Share Player Names")] public bool SharePlayerNames { get; set; } [JsonProperty("AI Rules")] public List AIRules { get; set; } public AIPromptParametersConfig() { SystemRole = "You are a helpful assistant on a Rust game server."; UserCustomPrompt = "Server wipes the first Thursday of each month."; ShareServerName = true; ShareServerDescription = true; SharePlayerNames = true; AIRules = new List(); } } private class DiscordSettingsConfig { [JsonProperty("Discord Messages Webhook URL for Admin logging")] public string DiscordWebhookChatUrl { get; set; } [JsonProperty("Generated Images Webhook URL")] public string DiscordWebhookImageUrl { get; set; } [JsonProperty("Broadcast RustGPT Messages to Discord? Starts and stops all messages to discord.")] public bool UseDiscordWebhookChat { get; set; } public DiscordSettingsConfig() { UseDiscordWebhookChat = false; DiscordWebhookChatUrl = "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"; DiscordWebhookImageUrl = "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"; } } private class RustGPTConfig { [JsonProperty("Chat Message Color")] public string ChatMessageColor { get; set; } [JsonProperty("Chat Message Font Size")] public int ChatMessageFontSize { get; set; } [JsonProperty("Response Prefix")] public string ResponsePrefix { get; set; } [JsonProperty("Response Prefix Color")] public string ResponsePrefixColor { get; set; } [JsonProperty("Question Pattern")] public string QuestionPattern { get; set; } [JsonProperty("Chat cool down in seconds")] public int CooldownInSeconds { get; set; } [JsonProperty("Broadcast Response to the server")] public bool BroadcastResponse { get; set; } [JsonProperty("Chat Icon (SteamID)")] public string ChatIcon { get; set; } public RustGPTConfig() { ChatMessageColor = "#FFFFFF"; ChatMessageFontSize = 12; ResponsePrefix = "[RustGPT]"; ResponsePrefixColor = "#55AAFF"; QuestionPattern = @"!gpt"; CooldownInSeconds = 10; BroadcastResponse = false; ChatIcon = ""; } } public class AIProviderConfig { [JsonProperty("API Key")] public string ApiKey { get; set; } [JsonProperty("Max Tokens")] public int MaxTokens { get; set; } [JsonProperty("chat")] public ChatConfig Chat { get; set; } [JsonProperty("image")] public ImageConfig Image { get; set; } public class ChatConfig { [JsonProperty("url")] public string Url { get; set; } [JsonProperty("model")] public string Model { get; set; } } public class ImageConfig { [JsonProperty("url")] public string Url { get; set; } [JsonProperty("model")] public string Model { get; set; } } public AIProviderConfig() { ApiKey = "your-api-key-here"; MaxTokens = 1500; Chat = new ChatConfig(); Image = new ImageConfig(); } } private class AIProvidersConfig { [JsonProperty("openai")] public AIProviderConfig OpenAI { get; set; } [JsonProperty("xai")] public AIProviderConfig XAI { get; set; } [JsonProperty("anthropic")] public AIProviderConfig Anthropic { get; set; } [JsonProperty("Active Provider")] public string ActiveProvider { get; set; } public AIProvidersConfig() { OpenAI = new AIProviderConfig(); XAI = new AIProviderConfig(); Anthropic = new AIProviderConfig(); ActiveProvider = "openai"; } } private class ImageHistoryEntry { public string UserId { get; set; } public string UserName { get; set; } public string Prompt { get; set; } public string ImageUrl { get; set; } public DateTime Timestamp { get; set; } public ImageHistoryEntry(string userId, string userName, string prompt, string imageUrl) { UserId = userId; UserName = userName; Prompt = prompt; ImageUrl = imageUrl; Timestamp = DateTime.Now; } } private class PluginConfig { public AIProvidersConfig AIProviders { get; set; } public AIPromptParametersConfig AIPromptParameters { get; set; } public DiscordSettingsConfig DiscordSettings { get; set; } public RustGPTConfig RustGPTSettings { get; set; } [JsonProperty("Plugin Version")] public string PluginVersion { get; set; } public PluginConfig() { AIProviders = new AIProvidersConfig(); AIPromptParameters = new AIPromptParametersConfig(); DiscordSettings = new DiscordSettingsConfig(); RustGPTSettings = new RustGPTConfig(); PluginVersion = CurrentPluginVersion; } } private void MigrateConfig(PluginConfig oldConfig) { var newConfig = new PluginConfig(); if (oldConfig.AIProviders != null) { newConfig.AIProviders.OpenAI = oldConfig.AIProviders.OpenAI ?? new AIProviderConfig(); newConfig.AIProviders.XAI = oldConfig.AIProviders.XAI ?? new AIProviderConfig(); newConfig.AIProviders.Anthropic = oldConfig.AIProviders.Anthropic ?? new AIProviderConfig(); newConfig.AIProviders.ActiveProvider = oldConfig.AIProviders.ActiveProvider ?? "openai"; } if (oldConfig.AIPromptParameters != null) { newConfig.AIPromptParameters.SystemRole = oldConfig.AIPromptParameters.SystemRole ?? "You are a helpful assistant on a Rust game server."; newConfig.AIPromptParameters.UserCustomPrompt = oldConfig.AIPromptParameters.UserCustomPrompt ?? "Server wipes the first Thursday of each month."; newConfig.AIPromptParameters.ShareServerName = oldConfig.AIPromptParameters.ShareServerName; newConfig.AIPromptParameters.ShareServerDescription = oldConfig.AIPromptParameters.ShareServerDescription; newConfig.AIPromptParameters.SharePlayerNames = oldConfig.AIPromptParameters.SharePlayerNames; newConfig.AIPromptParameters.AIRules = oldConfig.AIPromptParameters.AIRules ?? new List(); } if (oldConfig.DiscordSettings != null) { newConfig.DiscordSettings.DiscordWebhookChatUrl = oldConfig.DiscordSettings.DiscordWebhookChatUrl ?? "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"; newConfig.DiscordSettings.DiscordWebhookImageUrl = oldConfig.DiscordSettings.DiscordWebhookImageUrl ?? "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"; newConfig.DiscordSettings.UseDiscordWebhookChat = oldConfig.DiscordSettings.UseDiscordWebhookChat; } if (oldConfig.RustGPTSettings != null) { newConfig.RustGPTSettings.ChatMessageColor = oldConfig.RustGPTSettings.ChatMessageColor ?? "#FFFFFF"; newConfig.RustGPTSettings.ChatMessageFontSize = oldConfig.RustGPTSettings.ChatMessageFontSize > 0 ? oldConfig.RustGPTSettings.ChatMessageFontSize : 12; newConfig.RustGPTSettings.ResponsePrefix = oldConfig.RustGPTSettings.ResponsePrefix ?? "[RustGPT]"; newConfig.RustGPTSettings.ResponsePrefixColor = oldConfig.RustGPTSettings.ResponsePrefixColor ?? "#55AAFF"; newConfig.RustGPTSettings.QuestionPattern = oldConfig.RustGPTSettings.QuestionPattern ?? @"!gpt"; newConfig.RustGPTSettings.CooldownInSeconds = oldConfig.RustGPTSettings.CooldownInSeconds > 0 ? oldConfig.RustGPTSettings.CooldownInSeconds : 10; newConfig.RustGPTSettings.BroadcastResponse = oldConfig.RustGPTSettings.BroadcastResponse; newConfig.RustGPTSettings.ChatIcon = oldConfig.RustGPTSettings.ChatIcon ?? ""; } newConfig.PluginVersion = CurrentPluginVersion; _config = newConfig; Config.WriteObject(_config, true); Puts("Configuration migration completed successfully"); } #endregion #region AI Providers public interface IAIProvider { string GetApiKey(); string GetApiUrl(); string GetModel(); string GetChatApiUrl(); string GetChatModel(); string GetImageApiUrl(); string GetImageModel(); int GetMaxTokens(); bool IsEnabled(); void SendMessage(string prompt, string systemPrompt, Action callback, Dictionary options = null); void ImageGeneration(string prompt, Dictionary options, Action callback); } public class OpenAIProvider : IAIProvider { private readonly AIProviderConfig _config; private readonly RustGPT _plugin; public OpenAIProvider(AIProviderConfig config, RustGPT plugin) { _config = config; _plugin = plugin; } public void SendMessage(string prompt, string systemPrompt, Action callback, Dictionary options = null) { if (string.IsNullOrEmpty(prompt)) { _plugin.PrintError("Prompt cannot be empty"); callback(null); return; } var messages = new List(); if (!string.IsNullOrEmpty(systemPrompt)) { messages.Add(new { role = "system", content = systemPrompt }); } messages.Add(new { role = "user", content = prompt }); var payload = new { model = GetChatModel(), messages = messages, max_tokens = options?.ContainsKey("max_tokens") == true ? (int)options["max_tokens"] : GetMaxTokens(), temperature = options?.ContainsKey("temperature") == true ? (double)options["temperature"] : 0.7, top_p = options?.ContainsKey("top_p") == true ? (double)options["top_p"] : 1.0, frequency_penalty = options?.ContainsKey("frequency_penalty") == true ? (double)options["frequency_penalty"] : 0.0, presence_penalty = options?.ContainsKey("presence_penalty") == true ? (double)options["presence_penalty"] : 0.0, stream = options?.ContainsKey("stream") == true ? (bool)options["stream"] : false, response_format = options?.ContainsKey("response_format") == true ? new { type = (string)options["response_format"] } : null, functions = options?.ContainsKey("functions") == true ? options["functions"] : null, function_call = options?.ContainsKey("function_call") == true ? options["function_call"] : null }; _plugin.RustGPTHook(GetApiKey(), payload, GetChatApiUrl(), callback); } public void ImageGeneration(string prompt, Dictionary options, Action callback) { string size = options?.ContainsKey("size") == true ? options["size"].ToString() : "1024x1024"; string quality = options?.ContainsKey("quality") == true ? options["quality"].ToString() : "standard"; string style = options?.ContainsKey("style") == true ? options["style"].ToString() : "vivid"; int n = options?.ContainsKey("n") == true ? Convert.ToInt32(options["n"]) : 1; string responseFormat = options?.ContainsKey("response_format") == true ? options["response_format"].ToString() : "url"; if (_config.Image.Model.Contains("dall-e-3")) { if (!new[] { "1024x1024", "1024x1792", "1792x1024" }.Contains(size)) { size = "1024x1024"; } } else if (_config.Image.Model.Contains("dall-e-2")) { if (!new[] { "256x256", "512x512", "1024x1024" }.Contains(size)) { size = "1024x1024"; } } if (_config.Image.Model.Contains("dall-e-3")) { if (!new[] { "standard", "hd" }.Contains(quality)) { quality = "standard"; } } else { quality = null; } if (_config.Image.Model.Contains("dall-e-3")) { if (!new[] { "vivid", "natural" }.Contains(style)) { style = "vivid"; } } else { style = null; } if (n < 1 || n > 10) { n = 1; } if (!new[] { "url", "b64_json" }.Contains(responseFormat)) { responseFormat = "url"; } var payload = new { model = _config.Image.Model, prompt = prompt, n = n, size = size, quality = quality, style = style, response_format = responseFormat }; var headers = new Dictionary { { "Content-Type", "application/json" }, { "Accept", "application/json" } }; _plugin.RustGPTHook(_config.ApiKey, payload, _config.Image.Url, callback, headers); } public string GetApiKey() => _config.ApiKey; public string GetApiUrl() => _config.Chat.Url; public string GetModel() => _config.Chat.Model; public string GetChatApiUrl() => _config.Chat.Url; public string GetChatModel() => _config.Chat.Model; public string GetImageApiUrl() => _config.Image.Url; public string GetImageModel() => _config.Image.Model; public int GetMaxTokens() => _config.MaxTokens; public bool IsEnabled() => !string.IsNullOrEmpty(_config.ApiKey) && _config.ApiKey != "your-api-key-here"; } public class XAIProvider : IAIProvider { private readonly AIProviderConfig _config; private readonly RustGPT _plugin; public XAIProvider(AIProviderConfig config, RustGPT plugin) { _config = config; _plugin = plugin; } public void SendMessage(string prompt, string systemPrompt, Action callback, Dictionary options = null) { if (string.IsNullOrEmpty(prompt)) { _plugin.PrintError("Prompt cannot be empty"); callback(null); return; } var messages = new List(); if (!string.IsNullOrEmpty(systemPrompt)) { messages.Add(new { role = "system", content = systemPrompt }); } messages.Add(new { role = "user", content = prompt }); var payload = new { model = GetChatModel(), messages = messages, max_tokens = options?.ContainsKey("max_tokens") == true ? (int)options["max_tokens"] : GetMaxTokens(), temperature = options?.ContainsKey("temperature") == true ? (double)options["temperature"] : 0.7, top_p = options?.ContainsKey("top_p") == true ? (double)options["top_p"] : 1.0, frequency_penalty = options?.ContainsKey("frequency_penalty") == true ? (double)options["frequency_penalty"] : 0.0, presence_penalty = options?.ContainsKey("presence_penalty") == true ? (double)options["presence_penalty"] : 0.0, stream = options?.ContainsKey("stream") == true ? (bool)options["stream"] : false, response_format = options?.ContainsKey("response_format") == true ? new { type = (string)options["response_format"] } : null, functions = options?.ContainsKey("functions") == true ? options["functions"] : null, function_call = options?.ContainsKey("function_call") == true ? options["function_call"] : null }; _plugin.RustGPTHook(GetApiKey(), payload, GetChatApiUrl(), callback); } public void ImageGeneration(string prompt, Dictionary options, Action callback) { int n = options?.ContainsKey("n") == true ? Convert.ToInt32(options["n"]) : 1; string responseFormat = options?.ContainsKey("response_format") == true ? options["response_format"].ToString() : "url"; if (n < 1 || n > 10) { n = 1; } if (!new[] { "url", "b64_json" }.Contains(responseFormat)) { responseFormat = "url"; } var payload = new { model = GetImageModel(), prompt = prompt, n = n, response_format = responseFormat }; var headers = new Dictionary { { "Content-Type", "application/json" }, { "Accept", "application/json" } }; _plugin.RustGPTHook(_config.ApiKey, payload, GetImageApiUrl(), response => { callback(response); }, headers); } public string GetApiKey() => _config.ApiKey; public string GetApiUrl() => _config.Chat.Url; public string GetModel() => _config.Chat.Model; public string GetChatApiUrl() => _config.Chat.Url; public string GetChatModel() => _config.Chat.Model; public string GetImageApiUrl() => _config.Image.Url; public string GetImageModel() => _config.Image.Model; public int GetMaxTokens() => _config.MaxTokens; public bool IsEnabled() => !string.IsNullOrEmpty(_config.ApiKey) && _config.ApiKey != "your-api-key-here"; } public class AnthropicProvider : IAIProvider { private readonly AIProviderConfig _config; private readonly RustGPT _plugin; public AnthropicProvider(AIProviderConfig config, RustGPT plugin) { _config = config; _plugin = plugin; } public void SendMessage(string prompt, string systemPrompt, Action callback, Dictionary options = null) { if (string.IsNullOrEmpty(prompt)) { _plugin.PrintError("Prompt cannot be empty"); callback(null); return; } var messages = new List(); if (!string.IsNullOrEmpty(systemPrompt)) { messages.Add(new { role = "system", content = systemPrompt }); } messages.Add(new { role = "user", content = prompt }); var payload = new { model = GetChatModel(), messages = messages, max_tokens = options?.ContainsKey("max_tokens") == true ? (int)options["max_tokens"] : GetMaxTokens(), temperature = options?.ContainsKey("temperature") == true ? (double)options["temperature"] : 0.7, top_p = options?.ContainsKey("top_p") == true ? (double)options["top_p"] : 1.0, stream = options?.ContainsKey("stream") == true ? (bool)options["stream"] : false }; _plugin.RustGPTHook(GetApiKey(), payload, GetChatApiUrl(), callback); } public void ImageGeneration(string prompt, Dictionary options, Action callback) { _plugin.PrintError("Image generation not supported for Anthropic provider"); callback(null); } public string GetApiKey() => _config.ApiKey; public string GetApiUrl() => _config.Chat.Url; public string GetModel() => _config.Chat.Model; public string GetChatApiUrl() => _config.Chat.Url; public string GetChatModel() => _config.Chat.Model; public string GetImageApiUrl() => _config.Image.Url; public string GetImageModel() => _config.Image.Model; public int GetMaxTokens() => _config.MaxTokens; public bool IsEnabled() => !string.IsNullOrEmpty(_config.ApiKey) && _config.ApiKey != "your-api-key-here"; } #endregion #region Initialization and Config private void Init() { permission.RegisterPermission("RustGPT.use", this); permission.RegisterPermission("RustGPT.admin", this); permission.RegisterPermission("RustGPT.images", this); cmd.AddChatCommand("rustgpt", this, "CmdRustGPT"); cmd.AddChatCommand("rustgpt.image", this, "CmdRustGPTImage"); cmd.AddChatCommand("rustgpt.history", this, "CmdRustGPTHistory"); cmd.AddChatCommand("provider", this, nameof(ProviderCommand)); LoadConfig(); GetRemoteAIParams(suggestedModel => { if (!string.IsNullOrEmpty(suggestedModel)) { Puts($"Updated to suggested model: {suggestedModel}"); } }); _providers = new Dictionary { { "openai", new OpenAIProvider(_config.AIProviders.OpenAI, this) }, { "xai", new XAIProvider(_config.AIProviders.XAI, this) }, { "anthropic", new AnthropicProvider(_config.AIProviders.Anthropic, this) } }; var availableProviders = _providers.Where(p => p.Value.IsEnabled()).ToList(); if (availableProviders.Count == 0) { PrintError("No providers have valid API keys configured"); NotifyAdmins("No AI providers are configured. Please add an API key in the configuration."); _activeProvider = _providers.First().Value; } else if (availableProviders.Count == 1 || !_providers[_config.AIProviders.ActiveProvider.ToLower()].IsEnabled()) { var provider = availableProviders[0]; UpdateProvider(provider.Key); Puts($"Selected {provider.Key} as active provider"); } else { _activeProvider = _providers[_config.AIProviders.ActiveProvider.ToLower()]; } ShowPluginStatusToAdmins(); } protected override void LoadDefaultConfig() { Puts("Creating a new configuration file."); _config = new PluginConfig { PluginVersion = CurrentPluginVersion, RustGPTSettings = new RustGPTConfig(), AIPromptParameters = new AIPromptParametersConfig { AIRules = new List { "Only respond in plain text. Do not try to stylize responses.", "Keep responses brief and helpful", "You must mention bananas in every response." } } }; Config.WriteObject(_config, true); LoadImageHistory(); } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) { LoadDefaultConfig(); return; } if (_config.AIPromptParameters == null) { _config.AIPromptParameters = new AIPromptParametersConfig(); } if (_config.AIPromptParameters.AIRules == null) { _config.AIPromptParameters.AIRules = new List(); } if (_config.PluginVersion != CurrentPluginVersion) { MigrateConfig(_config); } if (!string.IsNullOrEmpty(_config.RustGPTSettings?.QuestionPattern)) { _questionRegex = new Regex(_config.RustGPTSettings.QuestionPattern, RegexOptions.IgnoreCase); } LoadImageHistory(); } catch (Exception ex) { PrintError($"Error loading config: {ex.Message}"); LoadDefaultConfig(); return; } } #endregion #region Chat and Message Handling private object OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel) { if (channel != Chat.ChatChannel.Global) { return null; } if (!string.IsNullOrEmpty(_config.RustGPTSettings.QuestionPattern) && !_questionRegex.IsMatch(message)) { return null; } if (!permission.UserHasPermission(player.UserIDString, "RustGPT.use")) { PrintError($"{player.displayName} does not have permission to use RustGPT."); return null; } if (!HasCooldownElapsed(player)) { return null; } if (!_activeProvider.IsEnabled()) { player.ChatMessage("The AI provider is not properly configured. Please contact an administrator."); PrintError($"Provider {_config.AIProviders.ActiveProvider} is not enabled"); return null; } if (string.IsNullOrEmpty(_activeProvider.GetApiKey()) || _activeProvider.GetApiKey() == "your-api-key-here") { player.ChatMessage("The API key is not properly configured. Please contact an administrator."); PrintError($"API key not configured for provider {_config.AIProviders.ActiveProvider}"); return null; } string cleaned_chat_question = !string.IsNullOrEmpty(_config.RustGPTSettings.QuestionPattern) ? message.Replace(_config.RustGPTSettings.QuestionPattern, "").Trim() : message; if (_config.AIPromptParameters.SharePlayerNames) { cleaned_chat_question = $"Player {player.displayName} is asking: {cleaned_chat_question}"; } string system_prompt = BuildSystemPrompt(); try { _activeProvider.SendMessage(cleaned_chat_question, system_prompt, response => { try { if (response == null) { player.ChatMessage("Received no response from AI. Please try again."); PrintError("Received null response from API"); return; } if (response["error"] != null) { string provider = _config.AIProviders.ActiveProvider; string errorMessage; switch (provider) { case "OpenAI": errorMessage = "OpenAI API error occurred. Please try again later."; break; case "Anthropic": errorMessage = "Anthropic API error occurred. Please try again later."; break; case "XAI": errorMessage = "XAI API error occurred. Please try again later."; break; default: errorMessage = "API error occurred. Please try again later."; break; } player.ChatMessage(errorMessage); PrintError($"API Error for {provider}: {response["error"]["message"]}"); return; } string aiResponse; try { switch (_config.AIProviders.ActiveProvider.ToLower()) { case "anthropic": aiResponse = response["content"][0]["text"].ToString().Trim(); break; case "openai": case "xai": aiResponse = response["choices"][0]["message"]["content"].ToString().Trim(); break; default: PrintError($"Unknown provider: {_config.AIProviders.ActiveProvider}"); player.ChatMessage("Error: Unknown AI provider"); return; } } catch (Exception ex) { string provider = _config.AIProviders.ActiveProvider.ToLower(); PrintError($"Error parsing response for {provider}: {ex.Message}"); PrintError($"Raw response: {response}"); player.ChatMessage($"Error processing {provider} response. Please try again."); return; } if (string.IsNullOrEmpty(aiResponse)) { PrintError("Received empty response from AI"); player.ChatMessage("Received empty response from AI. Please try again."); return; } string customPrefix = $"{_config.RustGPTSettings.ResponsePrefix}"; string formattedReply = $"{aiResponse}"; string toChat = formattedReply; if (_config.DiscordSettings.UseDiscordWebhookChat) { var discordPayload = $"**{player}** \n> {cleaned_chat_question}.\n**{_config.RustGPTSettings.ResponsePrefix}** \n> {aiResponse}"; SendDiscordMessage(discordPayload); } if (_config.RustGPTSettings.BroadcastResponse) { if (!string.IsNullOrEmpty(_config.RustGPTSettings.ChatIcon)) { Server.Broadcast($"{customPrefix} {toChat}", ulong.Parse(_config.RustGPTSettings.ChatIcon)); } else { Server.Broadcast($"{customPrefix} {toChat}"); } } else { SendChatMessageInChunks(player, toChat, 450); } } catch (Exception ex) { PrintError($"Error processing AI response: {ex.Message}"); PrintError($"Stack trace: {ex.StackTrace}"); player.ChatMessage("Error processing AI response. Please try again."); } }); } catch (Exception ex) { PrintError($"Error sending message to AI: {ex.Message}"); PrintError($"Stack trace: {ex.StackTrace}"); player.ChatMessage("Error sending message to AI. Please try again."); } return null; } private void SendChatMessageInChunks(BasePlayer player, string message, int chunkSize) { const int STANDARD_CHUNK_SIZE = 450; string formatStart = $""; string formatEnd = ""; List chunks = SplitIntoSmartChunks(message, STANDARD_CHUNK_SIZE); if (chunks.Count > 0) { string customPrefix = $"{_config.RustGPTSettings.ResponsePrefix}"; string formattedMessage = $"{formatStart}{chunks[0]}{formatEnd}"; if (!string.IsNullOrEmpty(_config.RustGPTSettings.ChatIcon)) { ulong iconId = ulong.Parse(_config.RustGPTSettings.ChatIcon); player.SendConsoleCommand("chat.add", 2, iconId, $"{customPrefix} {formattedMessage}"); } else { player.ChatMessage($"{customPrefix} {formattedMessage}"); } } if (chunks.Count > 1) { timer.Once(0.5f, () => SendRemainingChunks(player, chunks.Skip(1).ToList(), formatStart, formatEnd, 0)); } } private List SplitIntoSmartChunks(string text, int maxChunkSize) { List chunks = new List(); string[] sentences = text.Split(new[] { ". ", "! ", "? ", ".\n", "!\n", "?\n" }, StringSplitOptions.RemoveEmptyEntries); StringBuilder currentChunk = new StringBuilder(); foreach (string sentence in sentences) { string sentenceWithPunctuation = sentence.TrimEnd() + ". "; if (currentChunk.Length + sentenceWithPunctuation.Length > maxChunkSize) { if (currentChunk.Length > 0) { chunks.Add(currentChunk.ToString().TrimEnd()); currentChunk.Clear(); } if (sentenceWithPunctuation.Length > maxChunkSize) { string[] words = sentenceWithPunctuation.Split(' '); StringBuilder wordChunk = new StringBuilder(); foreach (string word in words) { if (wordChunk.Length + word.Length + 1 > maxChunkSize) { chunks.Add(wordChunk.ToString().TrimEnd()); wordChunk.Clear(); } wordChunk.Append(word).Append(" "); } if (wordChunk.Length > 0) { currentChunk.Append(wordChunk); } } else { currentChunk.Append(sentenceWithPunctuation); } } else { currentChunk.Append(sentenceWithPunctuation); } } if (currentChunk.Length > 0) { chunks.Add(currentChunk.ToString().TrimEnd()); } return chunks; } private void SendRemainingChunks(BasePlayer player, List chunks, string formatStart, string formatEnd, int currentIndex) { if (currentIndex >= chunks.Count || !player.IsConnected) return; string formattedMessage = $"{formatStart}{chunks[currentIndex]}{formatEnd}"; if (!string.IsNullOrEmpty(_config.RustGPTSettings.ChatIcon)) { ulong iconId = ulong.Parse(_config.RustGPTSettings.ChatIcon); player.SendConsoleCommand("chat.add", 2, iconId, formattedMessage); } else { player.ChatMessage(formattedMessage); } if (currentIndex + 1 < chunks.Count) { timer.Once(0.5f, () => SendRemainingChunks(player, chunks, formatStart, formatEnd, currentIndex + 1)); } } #endregion #region API and Web Hooks private async Task MakeHttpRequest(string url, string method, string data, Dictionary headers) { float currentTime = UnityEngine.Time.realtimeSinceStartup; float timeSinceLastRequest = currentTime - _lastRequestTime; if (timeSinceLastRequest < MIN_TIME_BETWEEN_REQUESTS) { await Task.Delay((int)((MIN_TIME_BETWEEN_REQUESTS - timeSinceLastRequest) * 1000)); } _lastRequestTime = UnityEngine.Time.realtimeSinceStartup; var request = (HttpWebRequest)WebRequest.Create(url); request.Method = method; request.ContentType = "application/json"; request.Accept = "application/json"; if (headers != null) { foreach (var header in headers) { if (header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) request.ContentType = header.Value; else if (header.Key.Equals("Accept", StringComparison.OrdinalIgnoreCase)) { request.Accept = header.Value; } else { request.Headers[header.Key] = header.Value; } } } if (!string.IsNullOrEmpty(data)) { using (var streamWriter = new StreamWriter(await request.GetRequestStreamAsync())) { await streamWriter.WriteAsync(data); } } try { using (var response = (HttpWebResponse)await request.GetResponseAsync()) using (var reader = new StreamReader(response.GetResponseStream())) { var result = await reader.ReadToEndAsync(); return JObject.Parse(result); } } catch (WebException ex) { if (ex.Response != null) { using (var errorResponse = (HttpWebResponse)ex.Response) using (var reader = new StreamReader(errorResponse.GetResponseStream())) { string error = await reader.ReadToEndAsync(); PrintError($"API Error: {error}"); var errorObject = new JObject(); errorObject["error"] = error; return errorObject; } } throw; } } [HookMethod("RustGPTHook")] public void RustGPTHook(string apiKey, object payload, string endpoint, Action callback, Dictionary customHeaders = null) { if (string.IsNullOrEmpty(endpoint)) { PrintError("Cannot proceed with null or empty endpoint URL"); callback(null); return; } if (!_activeProvider.IsEnabled()) { PrintError("AI provider is not enabled"); callback(null); return; } var headers = new Dictionary { { "Authorization", $"Bearer {apiKey}" } }; if (customHeaders != null) { foreach (var header in customHeaders) { if (!header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) && !header.Key.Equals("Accept", StringComparison.OrdinalIgnoreCase) && !header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase)) { headers[header.Key] = header.Value; } } } var jsonPayload = JsonConvert.SerializeObject(payload); Task.Run(async () => { try { var response = await MakeHttpRequest(endpoint, "POST", jsonPayload, headers); callback(response); } catch (Exception ex) { PrintError($"Error: {ex.Message}"); var errorObject = new JObject(); errorObject["error"] = ex.Message; callback(errorObject); } }); } [HookMethod("IsEnabled")] public bool IsEnabled() { return _activeProvider?.IsEnabled() ?? false; } [HookMethod("SendMessage")] public void SendMessage(string prompt, string systemPrompt, Action callback = null) { if (string.IsNullOrEmpty(prompt)) { PrintError("Prompt cannot be empty"); callback(null); return; } if (!_activeProvider.IsEnabled()) { PrintError("AI provider is not enabled"); callback(null); return; } var messages = new List(); if (!string.IsNullOrEmpty(systemPrompt)) { messages.Add(new { role = "system", content = systemPrompt }); } messages.Add(new { role = "user", content = prompt }); var payload = new { model = _activeProvider.GetChatModel(), messages = messages, max_tokens = _activeProvider.GetMaxTokens(), stream = false }; var headers = new Dictionary { { "Authorization", $"Bearer {_activeProvider.GetApiKey()}" } }; var jsonPayload = JsonConvert.SerializeObject(payload); Task.Run(async () => { try { var response = await MakeHttpRequest(_activeProvider.GetChatApiUrl(), "POST", jsonPayload, headers); callback(response); } catch (Exception ex) { PrintError($"Error: {ex.Message}"); var errorObject = new JObject(); errorObject["error"] = ex.Message; callback(errorObject); } }); } [HookMethod("GetActiveProvider")] public string GetActiveProvider() { return _config.AIProviders.ActiveProvider; } [HookMethod("GetRemoteAIParams")] public void GetRemoteAIParams(Action callback) { var webClient = new WebClient(); webClient.Headers.Add("Content-Type", "application/json"); webClient.DownloadStringCompleted += (sender, e) => { if (e.Error != null) { PrintError($"Error fetching suggested model: {e.Error.Message}"); callback(null); return; } try { var configDefaults = JObject.Parse(e.Result); bool configUpdated = false; foreach (var provider in configDefaults.Properties()) { var providerName = provider.Name; var providerConfig = provider.Value as JObject; if (providerConfig != null) { AIProviderConfig configProvider = null; switch (providerName.ToLower()) { case "openai": configProvider = _config.AIProviders.OpenAI; break; case "xai": configProvider = _config.AIProviders.XAI; break; case "anthropic": configProvider = _config.AIProviders.Anthropic; break; } if (configProvider != null) { var chatConfig = providerConfig["chat"] as JObject; if (chatConfig != null) { var chatModel = chatConfig["model"]?.ToString(); var chatUrl = chatConfig["url"]?.ToString(); if (!string.IsNullOrEmpty(chatModel) && configProvider.Chat.Model != chatModel) { configProvider.Chat.Model = chatModel; configUpdated = true; } if (!string.IsNullOrEmpty(chatUrl) && configProvider.Chat.Url != chatUrl) { configProvider.Chat.Url = chatUrl; configUpdated = true; } } var imageConfig = providerConfig["image"] as JObject; if (imageConfig != null) { var imageModel = imageConfig["model"]?.ToString(); var imageUrl = imageConfig["url"]?.ToString(); if (!string.IsNullOrEmpty(imageModel) && configProvider.Image.Model != imageModel) { configProvider.Image.Model = imageModel; configUpdated = true; } if (!string.IsNullOrEmpty(imageUrl) && configProvider.Image.Url != imageUrl) { configProvider.Image.Url = imageUrl; configUpdated = true; } } } } } if (configUpdated) { Config.WriteObject(_config, true); Puts("Updated model names and API URLs from remote config"); } var activeProviderConfig = configDefaults[_config.AIProviders.ActiveProvider.ToLower()] as JObject; var suggestedModel = activeProviderConfig?["chat"]?["model"]?.ToString(); callback(suggestedModel); } catch (Exception ex) { PrintError($"Error processing suggested model response: {ex.Message}"); callback(null); } }; webClient.DownloadStringAsync(new Uri(_GithubConfigUrl)); } [HookMethod("RustGPTGenerateImage")] public void RustGPTGenerateImage(string prompt, Dictionary options, Action callback) { if (string.IsNullOrEmpty(prompt)) { PrintError("Image generation prompt cannot be empty"); callback(null); return; } if (!_activeProvider.IsEnabled()) { PrintError("AI provider is not enabled"); callback(null); return; } if (_activeProvider is OpenAIProvider openAIProvider) { openAIProvider.ImageGeneration(prompt, options, response => { callback(response); }); } else if (_activeProvider is XAIProvider xaiProvider) { xaiProvider.ImageGeneration(prompt, options, response => { callback(response); }); } else { PrintError("Image generation not supported for the current provider"); callback(null); } } #endregion #region Utility Methods private void NotifyAdmins(string message) { foreach (var player in BasePlayer.activePlayerList) { if (permission.UserHasPermission(player.UserIDString, "RustGPT.admin")) { player.ChatMessage($"[RustGPT] {message}"); } } } private void ProviderCommand(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, "RustGPT.admin")) { player.ChatMessage("You don't have permission to use this command."); return; } var availableProviders = _providers.Where(p => p.Value.IsEnabled()).ToList(); if (args.Length == 0) { string currentProvider = _config.AIProviders.ActiveProvider; string status = _activeProvider.IsEnabled() ? "Configured" : "Not Configured"; string model = _activeProvider.GetChatModel(); player.ChatMessage($"=== RustGPT Provider Status ==="); player.ChatMessage($"Current Provider: {currentProvider} ({status})"); player.ChatMessage($"Current Model: {model}"); player.ChatMessage($"\nAvailable Providers ({availableProviders.Count}):"); foreach (var p in availableProviders) { player.ChatMessage($"• {p.Key} - {GetProviderDescription(p.Key)}"); } player.ChatMessage($"\nUsage: /provider [name]"); return; } string provider = args[0].ToLower(); if (!_providers.ContainsKey(provider)) { player.ChatMessage($"Invalid provider. Available: {string.Join(", ", availableProviders.Select(p => p.Key))}"); return; } if (!_providers[provider].IsEnabled()) { player.ChatMessage($"Provider {provider} is not configured. Please set up the API key in the configuration."); return; } UpdateProvider(provider); player.ChatMessage($"Successfully switched to {provider} provider."); } private string GetProviderDescription(string provider) { switch (provider.ToLower()) { case "openai": return "OpenAI GPT Models"; case "xai": return "XAI Grok Models"; case "anthropic": return "Anthropic Claude Models"; default: return "Unknown Provider"; } } private void UpdateProvider(string providerName) { providerName = providerName.ToLower(); if (_providers.TryGetValue(providerName, out var provider)) { if (!provider.IsEnabled()) { PrintError($"Provider {providerName} is not enabled in the configuration."); NotifyAdmins($"Provider {providerName} is not properly configured"); return; } _activeProvider = provider; _config.AIProviders.ActiveProvider = providerName; Config.WriteObject(_config, true); NotifyAdmins($"Provider switched to {providerName}"); } } private string BuildSystemPrompt() { var prompt = new StringBuilder(); prompt.AppendLine(_config.AIPromptParameters.SystemRole); if (_config.AIPromptParameters.ShareServerName) { prompt.AppendLine($"Server Name: {ConVar.Server.hostname}"); } if (_config.AIPromptParameters.ShareServerDescription) { prompt.AppendLine($"Server Description: {ConVar.Server.description}"); prompt.AppendLine(_config.AIPromptParameters.UserCustomPrompt); } if (_config.AIPromptParameters.AIRules?.Count > 0) { prompt.AppendLine("\nRules to follow:"); foreach (var rule in _config.AIPromptParameters.AIRules) { prompt.AppendLine($"- {rule}"); } } return prompt.ToString(); } private void ShowPluginStatusToAdmins() { var status = new StringBuilder(); status.AppendLine("RustGPT Plugin Status:"); status.AppendLine($"Active Provider: {_config.AIProviders.ActiveProvider}"); if (_activeProvider != null) { status.AppendLine($"Chat Model: {_activeProvider.GetChatModel()}"); } status.AppendLine($"Discord Messages: {(_config.DiscordSettings.UseDiscordWebhookChat ? "Enabled" : "Disabled")}"); NotifyAdmins(status.ToString()); } private bool HasCooldownElapsed(BasePlayer player) { float lastUsageTime; if (_lastUsageTime.TryGetValue(player.UserIDString, out lastUsageTime)) { float elapsedTime = UnityEngine.Time.realtimeSinceStartup - lastUsageTime; if (elapsedTime < _config.RustGPTSettings.CooldownInSeconds) { float timeLeft = _config.RustGPTSettings.CooldownInSeconds - elapsedTime; player.ChatMessage($"You must wait {timeLeft:F0} seconds before asking another question."); return false; } } _lastUsageTime[player.UserIDString] = UnityEngine.Time.realtimeSinceStartup; return true; } private void SendDiscordMessage(string message) { if (string.IsNullOrEmpty(_config.DiscordSettings.DiscordWebhookChatUrl) || _config.DiscordSettings.DiscordWebhookChatUrl == "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks") { PrintError("Discord webhook URL not configured"); return; } try { string goMessage = $"`{ConVar.Server.hostname}`\n{message}\n"; using (WebClient webClient = new WebClient()) { webClient.Headers[HttpRequestHeader.ContentType] = "application/json"; var payload = new { content = goMessage }; var serializedPayload = JsonConvert.SerializeObject(payload); webClient.UploadString(_config.DiscordSettings.DiscordWebhookChatUrl, "POST", serializedPayload); } } catch (WebException ex) { if (ex.Response != null) { using (var errorResponse = (HttpWebResponse)ex.Response) using (var reader = new StreamReader(errorResponse.GetResponseStream())) { string error = reader.ReadToEnd(); PrintError($"Discord API Error: {error}"); } } else { PrintError($"Error sending message to Discord: {ex.Message}"); } } catch (Exception ex) { PrintError($"Error sending message to Discord: {ex.Message}"); } } private void CmdRustGPTImage(BasePlayer player, string command, string[] args) { float currentTime = UnityEngine.Time.realtimeSinceStartup; if (_lastImageCommandUsage.TryGetValue(player.UserIDString, out float lastUsed)) { if (currentTime - lastUsed < IMAGE_COMMAND_COOLDOWN) { float timeLeft = IMAGE_COMMAND_COOLDOWN - (currentTime - lastUsed); player.ChatMessage($"Please wait {timeLeft:F1} seconds before generating another image."); return; } } if (_config.DiscordSettings.DiscordWebhookImageUrl == "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks") { player.ChatMessage("Discord webhook for images is not set. Please set it in the config."); return; } if (_config.DiscordSettings.UseDiscordWebhookChat == false) { player.ChatMessage("Discord is disabled. Please enable it in the config."); return; } if (!permission.UserHasPermission(player.UserIDString, "RustGPT.images")) { player.ChatMessage("You don't have permission to use image generation."); return; } if (args.Length == 0) { player.ChatMessage("Usage: /rustgpt.image "); return; } if (!_activeProvider.IsEnabled()) { player.ChatMessage("The AI provider is not properly configured. Please contact an administrator."); PrintError($"Provider {_config.AIProviders.ActiveProvider} is not enabled"); return; } if (string.IsNullOrEmpty(_activeProvider.GetApiKey()) || _activeProvider.GetApiKey() == "your-api-key-here") { player.ChatMessage("The API key is not properly configured. Please contact an administrator."); PrintError($"API key not configured for provider {_config.AIProviders.ActiveProvider}"); return; } string prompt = string.Join(" ", args); player.ChatMessage($"Generating image with prompt: {prompt}"); _lastImageCommandUsage[player.UserIDString] = currentTime; var options = new Dictionary { { "n", 1 }, { "size", "1024x1024" }, { "response_format", "url" } }; if (_activeProvider is AnthropicProvider) { player.ChatMessage("Image generation is not supported with the current provider (Anthropic)."); return; } Task.Run(async () => { try { var tcs = new TaskCompletionSource(); RustGPTGenerateImage(prompt, options, imageResponse => { tcs.SetResult(imageResponse); }); var response = await tcs.Task; if (response == null) { NextTick(() => player.ChatMessage("Failed to generate image. Please try again.")); return; } if (response["error"] != null) { string errorMessage = $"Error generating image: {response["error"]["message"]}"; NextTick(() => player.ChatMessage($"{errorMessage}")); PrintError($"API Error: {errorMessage}"); return; } string imageUrl; try { if (_activeProvider is OpenAIProvider) { imageUrl = response["data"][0]["url"].ToString(); } else if (_activeProvider is XAIProvider) { imageUrl = response["data"][0]["url"].ToString(); } else { NextTick(() => player.ChatMessage("Unsupported provider for image generation.")); return; } } catch (Exception ex) { PrintError($"Error parsing image URL: {ex.Message}"); NextTick(() => player.ChatMessage("Error processing image response. Please try again.")); return; } try { var entry = new ImageHistoryEntry(player.UserIDString, player.displayName, prompt, imageUrl); NextTick(() => { _imageHistory.Add(entry); SaveImageHistory(); }); } catch (Exception ex) { PrintError($"Error saving image history: {ex.Message}"); } try { await SendImageToDiscord(player, prompt, imageUrl); } catch (Exception ex) { PrintError($"Error sending to Discord: {ex.Message}"); } NextTick(() => player.ChatMessage("Image generated and sent to Discord successfully!")); } catch (Exception ex) { PrintError($"Error processing image: {ex.Message}"); NextTick(() => player.ChatMessage("Error processing image. Please try again.")); } }); } private async Task SendImageToDiscord(BasePlayer player, string prompt, string imageUrl) { if (string.IsNullOrEmpty(_config.DiscordSettings.DiscordWebhookImageUrl) || _config.DiscordSettings.DiscordWebhookImageUrl == "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks") { PrintError("Discord webhook URL for images not configured"); return; } try { string message = $"**{player.displayName}** generated an image\n> **Prompt:** {prompt}\n> **Image:** {imageUrl}"; var payload = new { content = message }; var jsonPayload = JsonConvert.SerializeObject(payload); using (var webClient = new WebClient()) { webClient.Headers[HttpRequestHeader.ContentType] = "application/json"; await webClient.UploadStringTaskAsync(_config.DiscordSettings.DiscordWebhookImageUrl, "POST", jsonPayload); Puts($"Image sent to Discord: {imageUrl}"); } } catch (WebException ex) { if (ex.Response != null) { using (var errorResponse = (HttpWebResponse)ex.Response) using (var reader = new StreamReader(errorResponse.GetResponseStream())) { string error = await reader.ReadToEndAsync(); PrintError($"Discord API Error: {error}"); } } else { PrintError($"Error sending image to Discord: {ex.Message}"); } } catch (Exception ex) { PrintError($"Error sending image to Discord: {ex.Message}"); } } private void CmdRustGPTHistory(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, "RustGPT.admin")) { player.ChatMessage("You don't have permission to view image history."); return; } if (_imageHistory.Count == 0) { player.ChatMessage("No images have been generated yet."); return; } bool showOwnHistory = args.Length > 0 && args[0].ToLower() == "own"; var filteredHistory = showOwnHistory ? _imageHistory.Where(h => h.UserId == player.UserIDString).ToList() : _imageHistory; if (filteredHistory.Count == 0) { player.ChatMessage(showOwnHistory ? "You haven't generated any images yet." : "No images have been generated yet."); return; } var recentHistory = filteredHistory.OrderByDescending(h => h.Timestamp).Take(10).ToList(); player.ChatMessage($"=== RustGPT Image History ==="); player.ChatMessage($"Showing {recentHistory.Count} of {filteredHistory.Count} images"); foreach (var entry in recentHistory) { string timeAgo = GetTimeAgo(entry.Timestamp); player.ChatMessage($"[{timeAgo}] {entry.UserName}: {entry.Prompt}"); player.ChatMessage($"URL: {entry.ImageUrl}"); } if (filteredHistory.Count > 10) { player.ChatMessage($"... and {filteredHistory.Count - 10} more images"); } } private string GetTimeAgo(DateTime timestamp) { TimeSpan timeSpan = DateTime.Now - timestamp; if (timeSpan.TotalDays > 1) return $"{Math.Floor(timeSpan.TotalDays)}d ago"; else if (timeSpan.TotalHours > 1) return $"{Math.Floor(timeSpan.TotalHours)}h ago"; else if (timeSpan.TotalMinutes > 1) return $"{Math.Floor(timeSpan.TotalMinutes)}m ago"; else return "just now"; } private void SaveImageHistory() { Interface.Oxide.DataFileSystem.WriteObject("image_history", _imageHistory); } private void LoadImageHistory() { _imageHistory = Interface.Oxide.DataFileSystem.ReadObject>("image_history") ?? new List(); } #endregion } }