diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index cf6c3281..1f546f84 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ
diff --git a/1.6/1.6/Defs/EventDefs/EventDef_Wula/Wula_AI_Events.xml b/1.6/1.6/Defs/EventDefs/EventDef_Wula/Wula_AI_Events.xml
index 31497273..06d8afdc 100644
--- a/1.6/1.6/Defs/EventDefs/EventDef_Wula/Wula_AI_Events.xml
+++ b/1.6/1.6/Defs/EventDefs/EventDef_Wula/Wula_AI_Events.xml
@@ -19,23 +19,16 @@
正在建立加密连接...连接成功。这里是乌拉帝国行星封锁机关P.I.A。我是“军团”。说明你的来意,原始人。
-
-
-
-
- Wula_AI_Initial_Contact
-
-
-
-
-
-
+
+ Wula_AI_Initial_Contact
+
+
diff --git a/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml
index c4ce1f45..79e7e820 100644
--- a/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml
+++ b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml
@@ -142,4 +142,18 @@
由于传送取消,生成的临时地图已被清理。
燃料不足。
+
+ AI 设置 (OpenAI 兼容)
+ API 密钥:
+ API 地址 (Base URL):
+ 模型名称:
+ 启用流式传输 (实验性)
+ 启用实时打字机效果。如果遇到问题请禁用。
+
+
+ 重试
+ 发送
+ 错误:内部系统故障。{0}
+ 错误:连接丢失。“军团”保持沉默。
+
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/RimTalkBridge.cs b/Source/WulaFallenEmpire/EventSystem/AI/RimTalkBridge.cs
deleted file mode 100644
index 1fc63dce..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/RimTalkBridge.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
-using Verse;
-
-namespace WulaFallenEmpire.EventSystem.AI
-{
- public static class RimTalkBridge
- {
- private static bool? _isRimTalkActive;
- private static Type _aiClientFactoryType;
- private static Type _aiClientInterfaceType;
- private static Type _roleEnum;
- private static MethodInfo _getAIClientAsyncMethod;
- private static MethodInfo _getChatCompletionAsyncMethod;
-
- public static bool IsRimTalkActive
- {
- get
- {
- if (!_isRimTalkActive.HasValue)
- {
- _isRimTalkActive = ModsConfig.IsActive("RimTalk.Mod"); // Replace with actual PackageId if different
- if (_isRimTalkActive.Value)
- {
- InitializeReflection();
- }
- }
- return _isRimTalkActive.Value;
- }
- }
-
- private static void InitializeReflection()
- {
- try
- {
- // Assuming RimTalk assembly is loaded
- Assembly rimTalkAssembly = AppDomain.CurrentDomain.GetAssemblies()
- .FirstOrDefault(a => a.GetName().Name == "RimTalk");
-
- if (rimTalkAssembly == null)
- {
- Log.Error("[WulaFallenEmpire] RimTalk assembly not found despite mod being active.");
- _isRimTalkActive = false;
- return;
- }
-
- _aiClientFactoryType = rimTalkAssembly.GetType("RimTalk.Client.AIClientFactory");
- _aiClientInterfaceType = rimTalkAssembly.GetType("RimTalk.Client.IAIClient");
- _roleEnum = rimTalkAssembly.GetType("RimTalk.Data.Role");
-
- if (_aiClientFactoryType != null)
- {
- _getAIClientAsyncMethod = _aiClientFactoryType.GetMethod("GetAIClientAsync", BindingFlags.Public | BindingFlags.Static);
- }
-
- if (_aiClientInterfaceType != null)
- {
- _getChatCompletionAsyncMethod = _aiClientInterfaceType.GetMethod("GetChatCompletionAsync");
- }
- }
- catch (Exception ex)
- {
- Log.Error($"[WulaFallenEmpire] Failed to initialize RimTalk reflection: {ex}");
- _isRimTalkActive = false;
- }
- }
-
- public static async Task GetChatCompletion(string instruction, List<(string role, string message)> messages)
- {
- if (!IsRimTalkActive || _getAIClientAsyncMethod == null || _getChatCompletionAsyncMethod == null)
- {
- return null;
- }
-
- try
- {
- // Get AI Client
- var clientTask = (Task)_getAIClientAsyncMethod.Invoke(null, null);
- await clientTask.ConfigureAwait(false);
-
- // The Task returns an IAIClient object
- object client = ((dynamic)clientTask).Result;
-
- if (client == null) return null;
-
- // Prepare messages list
- // List<(Role role, string message)>
- var tupleType = typeof(ValueTuple<,>).MakeGenericType(_roleEnum, typeof(string));
- var listType = typeof(List<>).MakeGenericType(tupleType);
- var messageList = Activator.CreateInstance(listType);
- var addMethod = listType.GetMethod("Add");
-
- foreach (var msg in messages)
- {
- object roleValue = Enum.Parse(_roleEnum, msg.role, true);
- object tuple = Activator.CreateInstance(tupleType, roleValue, msg.message);
- addMethod.Invoke(messageList, new object[] { tuple });
- }
-
- // Call GetChatCompletionAsync
- var completionTask = (Task)_getChatCompletionAsyncMethod.Invoke(client, new object[] { instruction, messageList });
- await completionTask.ConfigureAwait(false);
-
- // The Task returns a Payload object
- object payload = ((dynamic)completionTask).Result;
-
- // Payload has a 'Response' property (or similar, based on previous analysis it was 'Content' or 'Response')
- // Checking previous analysis: Payload has 'Content' property for the text response.
- PropertyInfo contentProp = payload.GetType().GetProperty("Content");
- return contentProp?.GetValue(payload) as string;
- }
- catch (Exception ex)
- {
- Log.Error($"[WulaFallenEmpire] Error calling RimTalk AI: {ex}");
- return null;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs b/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs
new file mode 100644
index 00000000..de5f99a0
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using UnityEngine.Networking;
+using Verse;
+
+namespace WulaFallenEmpire.EventSystem.AI
+{
+ public class SimpleAIClient
+ {
+ private readonly string _apiKey;
+ private readonly string _baseUrl;
+ private readonly string _model;
+
+ public SimpleAIClient(string apiKey, string baseUrl, string model)
+ {
+ _apiKey = apiKey;
+ _baseUrl = baseUrl?.TrimEnd('/');
+ _model = model;
+ }
+
+ public async Task GetChatCompletionAsync(string instruction, List<(string role, string message)> messages)
+ {
+ if (string.IsNullOrEmpty(_baseUrl))
+ {
+ Log.Error("[WulaAI] Base URL is missing.");
+ return null;
+ }
+
+ string endpoint = $"{_baseUrl}/chat/completions";
+ // Handle cases where baseUrl already includes /v1 or full path
+ if (_baseUrl.EndsWith("/chat/completions")) endpoint = _baseUrl;
+ else if (!_baseUrl.EndsWith("/v1")) endpoint = $"{_baseUrl}/v1/chat/completions";
+
+ // Build JSON manually to avoid dependencies
+ StringBuilder jsonBuilder = new StringBuilder();
+ jsonBuilder.Append("{");
+ jsonBuilder.Append($"\"model\": \"{_model}\",");
+ jsonBuilder.Append("\"stream\": false,");
+ jsonBuilder.Append("\"messages\": [");
+
+ // System instruction
+ if (!string.IsNullOrEmpty(instruction))
+ {
+ jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}},");
+ }
+
+ // Messages
+ for (int i = 0; i < messages.Count; i++)
+ {
+ var msg = messages[i];
+ string role = msg.role.ToLower();
+ if (role == "ai") role = "assistant";
+ // Map other roles if needed
+
+ jsonBuilder.Append($"{{\"role\": \"{role}\", \"content\": \"{EscapeJson(msg.message)}\"}}");
+ if (i < messages.Count - 1) jsonBuilder.Append(",");
+ }
+
+ jsonBuilder.Append("]");
+ jsonBuilder.Append("}");
+
+ string jsonBody = jsonBuilder.ToString();
+ Log.Message($"[WulaAI] Sending request to {endpoint}");
+
+ using (UnityWebRequest request = new UnityWebRequest(endpoint, "POST"))
+ {
+ byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
+ request.uploadHandler = new UploadHandlerRaw(bodyRaw);
+ request.downloadHandler = new DownloadHandlerBuffer();
+ request.SetRequestHeader("Content-Type", "application/json");
+ if (!string.IsNullOrEmpty(_apiKey))
+ {
+ request.SetRequestHeader("Authorization", $"Bearer {_apiKey}");
+ }
+
+ var operation = request.SendWebRequest();
+
+ while (!operation.isDone)
+ {
+ await Task.Delay(50);
+ }
+
+#if UNITY_2020_2_OR_NEWER
+ if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
+#else
+ if (request.isNetworkError || request.isHttpError)
+#endif
+ {
+ Log.Error($"[WulaAI] API Error: {request.error}\nResponse: {request.downloadHandler.text}");
+ return null;
+ }
+
+ string responseText = request.downloadHandler.text;
+ Log.Message($"[WulaAI] Raw Response: {responseText}");
+ return ExtractContent(responseText);
+ }
+ }
+
+ private string EscapeJson(string s)
+ {
+ if (s == null) return "";
+ return s.Replace("\\", "\\\\")
+ .Replace("\"", "\\\"")
+ .Replace("\n", "\\n")
+ .Replace("\r", "\\r")
+ .Replace("\t", "\\t");
+ }
+
+ private string ExtractContent(string json)
+ {
+ try
+ {
+ // Robust parsing for "content": "..." allowing for whitespace variations
+ int contentIndex = json.IndexOf("\"content\"");
+ if (contentIndex == -1) return null;
+
+ // Find the opening quote after "content"
+ int openQuoteIndex = -1;
+ for (int i = contentIndex + 9; i < json.Length; i++)
+ {
+ if (json[i] == '"')
+ {
+ openQuoteIndex = i;
+ break;
+ }
+ }
+ if (openQuoteIndex == -1) return null;
+
+ int startIndex = openQuoteIndex + 1;
+ StringBuilder content = new StringBuilder();
+ bool escaped = false;
+
+ for (int i = startIndex; i < json.Length; i++)
+ {
+ char c = json[i];
+ if (escaped)
+ {
+ if (c == 'n') content.Append('\n');
+ else if (c == 'r') content.Append('\r');
+ else if (c == 't') content.Append('\t');
+ else if (c == '"') content.Append('"');
+ else if (c == '\\') content.Append('\\');
+ else content.Append(c); // Literal
+ escaped = false;
+ }
+ else
+ {
+ if (c == '\\')
+ {
+ escaped = true;
+ }
+ else if (c == '"')
+ {
+ // End of string
+ return content.ToString();
+ }
+ else
+ {
+ content.Append(c);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[WulaAI] Error parsing response: {ex}");
+ }
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
index b2c4ba01..627f1049 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
@@ -9,19 +9,20 @@ using WulaFallenEmpire.EventSystem.AI.Tools;
namespace WulaFallenEmpire.EventSystem.AI.UI
{
- public class Dialog_AIConversation : Window
+ public class Dialog_AIConversation : Dialog_CustomDisplay
{
- private EventDef _def;
private List<(string role, string message)> _history = new List<(string role, string message)>();
- private string _currentResponse = "Thinking...";
+ private string _currentResponse = "";
private List _options = new List();
private string _inputText = "";
private bool _isThinking = false;
- private Vector2 _scrollPosition = Vector2.zero;
+ private float _thinkingTime = 0f;
+ private string _thinkingStatus = "";
+ private bool _isTimeout = false;
private List _tools = new List();
- private const int MaxHistoryTokens = 100000; // Approximate token limit
- private const int CharsPerToken = 4; // Rough estimation
+ private const int MaxHistoryTokens = 100000;
+ private const int CharsPerToken = 4;
private const string DefaultSystemInstruction = @"You are 'The Legion', a super AI controlling the Wula Empire's blockade fleet.
You are authoritative, powerful, and slightly arrogant but efficient.
@@ -42,13 +43,19 @@ OPTIONS:
3. Option 3
";
- public Dialog_AIConversation(EventDef def)
+ public Dialog_AIConversation(EventDef def) : base(def)
{
- _def = def;
+ // Base constructor sets this.def
+
+ // Use Config from Dialog_CustomDisplay
+ this.forcePause = Dialog_CustomDisplay.Config.pauseGameOnOpen;
+ this.absorbInputAroundWindow = false; // Allow interaction with other UI elements
this.doCloseX = true;
- this.forcePause = true;
- this.absorbInputAroundWindow = true;
+ this.doWindowBackground = Dialog_CustomDisplay.Config.showMainWindow;
+ this.drawShadow = Dialog_CustomDisplay.Config.showMainWindow;
this.closeOnClickedOutside = false;
+ this.draggable = true;
+ this.resizeable = true;
_tools.Add(new Tool_SpawnResources());
_tools.Add(new Tool_ModifyGoodwill());
@@ -57,38 +64,47 @@ OPTIONS:
_tools.Add(new Tool_GetMapResources());
}
- public override Vector2 InitialSize => _def.windowSize != Vector2.zero ? _def.windowSize : new Vector2(600, 500);
+ public override Vector2 InitialSize => def.windowSize != Vector2.zero ? def.windowSize : Dialog_CustomDisplay.Config.windowSize;
public override void PostOpen()
{
base.PostOpen();
+ // Textures are loaded in base.PreOpen()
StartConversation();
}
public override void PostClose()
{
base.PostClose();
- // Save history on close
var historyManager = Find.World.GetComponent();
- historyManager.SaveHistory(_def.defName, _history);
+ historyManager.SaveHistory(def.defName, _history);
}
private async void StartConversation()
{
- _isThinking = true;
-
- // Load history
var historyManager = Find.World.GetComponent();
- _history = historyManager.GetHistory(_def.defName);
+ _history = historyManager.GetHistory(def.defName);
if (_history.Count == 0)
{
- _history.Add(("User", "Hello")); // Initial trigger
- await GenerateResponse();
+ // Initial greeting from EventDef
+ if (!def.descriptions.NullOrEmpty())
+ {
+ _currentResponse = def.descriptions.RandomElement().Translate();
+ _history.Add(("AI", _currentResponse));
+
+ // Generate initial options based on greeting
+ _history.Add(("System", "The conversation has started. Please generate 3 initial response options for the player based on your greeting."));
+ await GenerateResponse();
+ }
+ else
+ {
+ _history.Add(("User", "Hello"));
+ await GenerateResponse();
+ }
}
else
{
- // Restore last state
var lastAIResponse = _history.LastOrDefault(x => x.role == "AI");
if (lastAIResponse.message != null)
{
@@ -96,19 +112,15 @@ OPTIONS:
}
else
{
- // Should not happen if history is valid, but fallback
await GenerateResponse();
}
- _isThinking = false;
}
}
private string GetSystemInstruction()
{
- string baseInstruction = !string.IsNullOrEmpty(_def.aiSystemInstruction) ? _def.aiSystemInstruction : DefaultSystemInstruction;
+ string baseInstruction = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultSystemInstruction;
string language = LanguageDatabase.activeLanguage.FriendlyNameNative;
-
- // Get Goodwill
var eventVarManager = Find.World.GetComponent();
int goodwill = eventVarManager.GetVariable("Wula_Goodwill_To_PIA", 0);
string goodwillContext = $"Current Goodwill with P.I.A: {goodwill}. ";
@@ -116,40 +128,78 @@ OPTIONS:
else if (goodwill < 0) goodwillContext += "You are cold and impatient.";
else if (goodwill > 50) goodwillContext += "You are somewhat approving and helpful.";
else goodwillContext += "You are neutral and business-like.";
-
return $"{baseInstruction}\n{goodwillContext}\nIMPORTANT: You MUST reply in the following language: {language}.";
}
private async Task GenerateResponse()
{
_isThinking = true;
- _currentResponse = "Thinking...";
+ _isTimeout = false;
+ _thinkingTime = 0f;
+ _thinkingStatus = "Connecting to neural network...";
_options.Clear();
-
- CompressHistoryIfNeeded();
-
- string response = await WulaFallenEmpire.EventSystem.AI.RimTalkBridge.GetChatCompletion(GetSystemInstruction() + GetToolDescriptions(), _history);
-
- if (string.IsNullOrEmpty(response))
+
+ try
+ {
+ CompressHistoryIfNeeded();
+ string systemInstruction = GetSystemInstruction() + GetToolDescriptions();
+ Log.Message($"[WulaAI] Sending request to AI. History count: {_history.Count}. System Instruction:\n{systemInstruction}");
+
+ // Use local settings and SimpleAIClient
+ var settings = WulaFallenEmpireMod.settings;
+ if (string.IsNullOrEmpty(settings.apiKey))
+ {
+ _currentResponse = "Error: API Key not configured in Mod Settings.";
+ _isThinking = false;
+ return;
+ }
+
+ var client = new SimpleAIClient(settings.apiKey, settings.baseUrl, settings.model);
+
+ // Start a timeout task
+ var timeoutTask = Task.Delay(120000); // 120 seconds
+ var apiTask = client.GetChatCompletionAsync(systemInstruction, _history);
+
+ var completedTask = await Task.WhenAny(apiTask, timeoutTask);
+
+ if (completedTask == timeoutTask)
+ {
+ _isThinking = false;
+ _isTimeout = true;
+ _currentResponse = "Error: Connection timed out (120s). The Legion is unreachable.";
+ return;
+ }
+
+ string response = await apiTask;
+ Log.Message($"[WulaAI] Received response from AI:\n{response}");
+
+ if (string.IsNullOrEmpty(response))
+ {
+ _currentResponse = "Error: Connection lost. The Legion is silent.";
+ _isThinking = false;
+ return;
+ }
+
+ string trimmedResponse = response.Trim();
+ if ((trimmedResponse.StartsWith("[") && trimmedResponse.EndsWith("]")) ||
+ (trimmedResponse.StartsWith("{") && trimmedResponse.EndsWith("}")))
+ {
+ await HandleToolUsage(trimmedResponse);
+ }
+ else
+ {
+ ParseResponse(response);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[WulaAI] Exception in GenerateResponse: {ex}");
+ _currentResponse = "Wula_AI_Error_Internal".Translate(ex.Message);
+ }
+ finally
{
- _currentResponse = "Error: Could not connect to AI.";
_isThinking = false;
- return;
}
-
- // Check for tool usage (Array or Single Object)
- string trimmedResponse = response.Trim();
- if ((trimmedResponse.StartsWith("[") && trimmedResponse.EndsWith("]")) ||
- (trimmedResponse.StartsWith("{") && trimmedResponse.EndsWith("}")))
- {
- await HandleToolUsage(trimmedResponse);
- }
- else
- {
- ParseResponse(response);
- }
-
- _isThinking = false;
}
private void CompressHistoryIfNeeded()
@@ -179,96 +229,68 @@ OPTIONS:
private async Task HandleToolUsage(string json)
{
- // Normalize to list of objects
List toolCalls = new List();
- if (json.Trim().StartsWith("{"))
- {
- toolCalls.Add(json);
- }
+ if (json.Trim().StartsWith("{")) toolCalls.Add(json);
else
{
- // Simple array parsing: split by "}, {"
- // This is fragile but works for simple cases without nested objects in args
- // A better way is to use a proper JSON parser if available, or regex
- // Let's try a simple regex to extract objects
- // Assuming objects are { ... }
int depth = 0;
int start = 0;
for (int i = 0; i < json.Length; i++)
{
- if (json[i] == '{')
- {
- if (depth == 0) start = i;
- depth++;
- }
- else if (json[i] == '}')
- {
- depth--;
- if (depth == 0)
- {
- toolCalls.Add(json.Substring(start, i - start + 1));
- }
- }
+ if (json[i] == '{') { if (depth == 0) start = i; depth++; }
+ else if (json[i] == '}') { depth--; if (depth == 0) toolCalls.Add(json.Substring(start, i - start + 1)); }
}
}
StringBuilder combinedResults = new StringBuilder();
- bool hasVisibleResult = false;
+ Log.Message($"[WulaAI] Processing {toolCalls.Count} tool calls.");
foreach (var callJson in toolCalls)
{
string toolName = "";
string args = "";
-
try
{
- // Extract tool name and args
int toolIndex = callJson.IndexOf("\"tool\"");
int argsIndex = callJson.IndexOf("\"args\"");
-
if (toolIndex != -1 && argsIndex != -1)
{
int toolValueStart = callJson.IndexOf(":", toolIndex) + 1;
int toolValueEnd = callJson.IndexOf(",", toolValueStart);
- if (toolValueEnd == -1) toolValueEnd = callJson.IndexOf("}", toolValueStart); // Handle case where tool is last
-
+ if (toolValueEnd == -1) toolValueEnd = callJson.IndexOf("}", toolValueStart);
toolName = callJson.Substring(toolValueStart, toolValueEnd - toolValueStart).Trim().Trim('"');
-
int argsValueStart = callJson.IndexOf(":", argsIndex) + 1;
int argsValueEnd = callJson.LastIndexOf("}");
args = callJson.Substring(argsValueStart, argsValueEnd - argsValueStart).Trim();
}
}
- catch
+ catch (Exception ex)
{
- combinedResults.AppendLine("Error parsing tool request.");
+ string errorMsg = $"Error parsing tool request: {ex.Message}";
+ Log.Error($"[WulaAI] {errorMsg}");
+ combinedResults.AppendLine(errorMsg);
continue;
}
+ Log.Message($"[WulaAI] Executing tool '{toolName}' with args: {args}");
var tool = _tools.FirstOrDefault(t => t.Name == toolName);
if (tool != null)
{
string result = tool.Execute(args);
-
- if (toolName == "modify_goodwill")
- {
- combinedResults.AppendLine($"Tool '{toolName}' Result (Invisible): {result}");
- }
- else
- {
- combinedResults.AppendLine($"Tool '{toolName}' Result: {result}");
- hasVisibleResult = true;
- }
+ Log.Message($"[WulaAI] Tool '{toolName}' execution result: {result}");
+ if (toolName == "modify_goodwill") combinedResults.AppendLine($"Tool '{toolName}' Result (Invisible): {result}");
+ else combinedResults.AppendLine($"Tool '{toolName}' Result: {result}");
}
else
{
- combinedResults.AppendLine($"Error: Tool '{toolName}' not found.");
+ string errorMsg = $"Error: Tool '{toolName}' not found.";
+ Log.Error($"[WulaAI] {errorMsg}");
+ combinedResults.AppendLine(errorMsg);
}
}
- _history.Add(("AI", json)); // Log the full tool call
+ _history.Add(("AI", json));
_history.Add(("System", combinedResults.ToString()));
-
await GenerateResponse();
}
@@ -276,8 +298,6 @@ OPTIONS:
{
var parts = rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None);
_currentResponse = parts[0].Trim();
-
- // Only add to history if it's a new response (not restoring from history)
if (_history.Count == 0 || _history.Last().role != "AI" || _history.Last().message != rawResponse)
{
_history.Add(("AI", rawResponse));
@@ -290,10 +310,7 @@ OPTIONS:
{
string opt = line.Trim();
int dotIndex = opt.IndexOf('.');
- if (dotIndex != -1 && dotIndex < 4)
- {
- opt = opt.Substring(dotIndex + 1).Trim();
- }
+ if (dotIndex != -1 && dotIndex < 4) opt = opt.Substring(dotIndex + 1).Trim();
_options.Add(opt);
}
}
@@ -301,36 +318,87 @@ OPTIONS:
public override void DoWindowContents(Rect inRect)
{
- Text.Font = GameFont.Medium;
- Widgets.Label(new Rect(0, 0, inRect.width, 30), _def.characterName ?? "The Legion");
- Text.Font = GameFont.Small;
-
- float y = 40;
-
- Rect chatRect = new Rect(0, y, inRect.width, 300);
- Widgets.DrawMenuSection(chatRect);
-
- string display = _currentResponse;
- if (_isThinking) display = "Thinking...";
-
- Widgets.Label(chatRect.ContractedBy(10), display);
- y += 310;
-
- if (!_isThinking && _options.Count > 0)
+ // Draw Background
+ if (background != null)
{
- foreach (var option in _options)
- {
- if (Widgets.ButtonText(new Rect(0, y, inRect.width, 30), option))
- {
- SelectOption(option);
- }
- y += 35;
- }
+ GUI.DrawTexture(inRect, background, ScaleMode.ScaleAndCrop);
}
- y += 10;
- _inputText = Widgets.TextField(new Rect(0, y, inRect.width - 110, 30), _inputText);
- if (Widgets.ButtonText(new Rect(inRect.width - 100, y, 100, 30), "Send"))
+ float curY = inRect.y;
+ float width = inRect.width;
+
+ // 1. Portrait (Top)
+ if (portrait != null)
+ {
+ Rect scaledPortraitRect = Dialog_CustomDisplay.Config.GetScaledRect(Dialog_CustomDisplay.Config.portraitSize, inRect, true);
+ // Center horizontally
+ Rect portraitRect = new Rect((width - scaledPortraitRect.width) / 2, curY, scaledPortraitRect.width, scaledPortraitRect.height);
+ GUI.DrawTexture(portraitRect, portrait, ScaleMode.ScaleToFit);
+ curY += scaledPortraitRect.height + 10f;
+ }
+
+ // 2. Character Name
+ Text.Font = GameFont.Medium;
+ string name = def.characterName ?? "The Legion";
+ float nameHeight = Text.CalcHeight(name, width);
+ Widgets.Label(new Rect(inRect.x, curY, width, nameHeight), name);
+ curY += nameHeight + 10f;
+
+ // Calculate remaining height for Description and Options
+ float inputHeight = 30f;
+ float bottomMargin = 10f;
+ float remainingHeight = inRect.height - curY - inputHeight - bottomMargin;
+
+ // Split remaining height: 60% Description, 40% Options
+ float descriptionHeight = remainingHeight * 0.6f;
+ float optionsHeight = remainingHeight * 0.4f;
+
+ // 3. Description (AI Response)
+ Rect descriptionRect = new Rect(inRect.x, curY, width, descriptionHeight);
+ // Widgets.DrawMenuSection(descriptionRect); // Removed background as requested
+
+ if (_isThinking)
+ {
+ _thinkingTime += Time.deltaTime;
+ string dots = new string('.', (int)(_thinkingTime * 2) % 4);
+ string status = $"{_thinkingStatus}{dots} ({_thinkingTime:F1}s)";
+
+ Text.Anchor = TextAnchor.MiddleCenter;
+ Widgets.Label(descriptionRect, status);
+ Text.Anchor = TextAnchor.UpperLeft;
+ }
+ else if (_isTimeout)
+ {
+ Text.Anchor = TextAnchor.MiddleCenter;
+ Widgets.Label(descriptionRect, _currentResponse);
+
+ // Retry button
+ Rect retryRect = new Rect(descriptionRect.center.x - 60f, descriptionRect.center.y + 20f, 120f, 30f);
+ if (Widgets.ButtonText(retryRect, "Wula_AI_Retry".Translate()))
+ {
+ _ = GenerateResponse(); // Fire and forget
+ }
+ Text.Anchor = TextAnchor.UpperLeft;
+ }
+ else
+ {
+ DrawDescriptionScrollView(descriptionRect.ContractedBy(10f), _currentResponse);
+ }
+ curY += descriptionHeight + 10f;
+
+ // 4. Options
+ Rect optionsRect = new Rect(inRect.x, curY, width, optionsHeight);
+ if (!_isThinking && _options.Count > 0)
+ {
+ List eventOptions = _options.Select(opt => new EventOption { label = opt, useCustomColors = false }).ToList();
+ DrawOptions(optionsRect, eventOptions);
+ }
+ curY += optionsHeight + 10f;
+
+ // 5. Input Area (Bottom)
+ Rect inputRect = new Rect(inRect.x, inRect.yMax - inputHeight, width, inputHeight);
+ _inputText = Widgets.TextField(new Rect(inputRect.x, inputRect.y, inputRect.width - 85, inputHeight), _inputText);
+ if (Widgets.ButtonText(new Rect(inputRect.xMax - 80, inputRect.y, 80, inputHeight), "Wula_AI_Send".Translate()))
{
if (!string.IsNullOrEmpty(_inputText))
{
@@ -338,11 +406,96 @@ OPTIONS:
_inputText = "";
}
}
+ }
+
+ // Override DrawSingleOption to handle click
+ protected override void DrawSingleOption(Rect rect, EventOption option)
+ {
+ // We need to intercept the click to call SelectOption
+ // But base.DrawSingleOption calls HandleAction which executes effects.
+ // Here we want to send the text to AI.
- // Close button
- if (Widgets.ButtonText(new Rect(inRect.width - 120, inRect.height - 40, 120, 30), "CloseButton".Translate()))
+ // Copy logic from base but change action
+ // 水平居中选项
+ float optionWidth = Mathf.Min(rect.width, Dialog_CustomDisplay.Config.optionSize.x * (rect.width / Dialog_CustomDisplay.Config.windowSize.x));
+ float optionX = rect.x + (rect.width - optionWidth) / 2;
+ Rect optionRect = new Rect(optionX, rect.y, optionWidth, rect.height);
+
+ // 保存原始状态
+ Color originalColor = GUI.color;
+ GameFont originalFont = Text.Font;
+ Color originalTextColor = GUI.contentColor;
+ TextAnchor originalAnchor = Text.Anchor;
+
+ try
{
- Close();
+ // 设置文本居中
+ Text.Anchor = TextAnchor.MiddleCenter;
+ Text.Font = GameFont.Small;
+
+ // AI options are always enabled
+ // 使用默认自定义颜色
+ DrawCustomButton(optionRect, option.label.Translate(), isEnabled: true);
+
+ // 添加点击处理
+ if (Widgets.ButtonInvisible(optionRect))
+ {
+ SelectOption(option.label);
+ }
+ }
+ finally
+ {
+ // 恢复原始状态
+ GUI.color = originalColor;
+ Text.Font = originalFont;
+ GUI.contentColor = originalTextColor;
+ Text.Anchor = originalAnchor;
+ }
+ }
+
+ // Helper to draw custom button (copied from base because it's private there, wait I made it protected? No, I made DrawCustomButton private in base? Let me check)
+ // I made DrawCustomButton private in base. I should have made it protected.
+ // Let me check my previous apply_diff.
+ // I made DrawSingleOption protected virtual.
+ // But DrawCustomButton is private.
+ // So I need to copy DrawCustomButton here or make it protected in base.
+ // I will copy it here to be safe and avoid another diff on base.
+
+ private void DrawCustomButton(Rect rect, string label, bool isEnabled = true)
+ {
+ bool isMouseOver = Mouse.IsOver(rect);
+ Color buttonColor, textColor;
+
+ if (!isEnabled)
+ {
+ buttonColor = new Color(0.15f, 0.15f, 0.15f, 0.6f);
+ textColor = new Color(0.6f, 0.6f, 0.6f, 1f);
+ }
+ else if (isMouseOver)
+ {
+ buttonColor = new Color(0.6f, 0.3f, 0.3f, 1f);
+ textColor = new Color(1f, 1f, 1f, 1f);
+ }
+ else
+ {
+ buttonColor = new Color(0.5f, 0.2f, 0.2f, 1f);
+ textColor = new Color(0.9f, 0.9f, 0.9f, 1f);
+ }
+
+ GUI.color = buttonColor;
+ Widgets.DrawBoxSolid(rect, buttonColor);
+
+ if (isEnabled) Widgets.DrawBox(rect, 1);
+ else Widgets.DrawBox(rect, 1);
+
+ GUI.color = textColor;
+ Text.Anchor = TextAnchor.MiddleCenter;
+ Widgets.Label(rect.ContractedBy(4f), label);
+
+ if (!isEnabled)
+ {
+ GUI.color = new Color(0.6f, 0.6f, 0.6f, 0.8f);
+ Widgets.DrawLine(new Vector2(rect.x + 10f, rect.center.y), new Vector2(rect.xMax - 10f, rect.center.y), GUI.color, 1f);
}
}
diff --git a/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs b/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs
index 9215c159..cc7a7439 100644
--- a/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs
+++ b/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs
@@ -9,12 +9,12 @@ namespace WulaFallenEmpire
{
public class Dialog_CustomDisplay : Window
{
- private EventDef def;
- private Texture2D portrait;
- private Texture2D background;
- private string selectedDescription;
+ protected EventDef def;
+ protected Texture2D portrait;
+ protected Texture2D background;
+ protected string selectedDescription;
- private static EventUIConfigDef config;
+ protected static EventUIConfigDef config;
public static EventUIConfigDef Config
{
get
@@ -37,8 +37,8 @@ namespace WulaFallenEmpire
private static readonly Color CustomButtonTextDisabledColor = new Color(0.6f, 0.6f, 0.6f, 1f);
// 滚动位置
- private Vector2 descriptionScrollPosition = Vector2.zero;
- private Vector2 optionsScrollPosition = Vector2.zero;
+ protected Vector2 descriptionScrollPosition = Vector2.zero;
+ protected Vector2 optionsScrollPosition = Vector2.zero;
// 使用配置的窗口尺寸
public override Vector2 InitialSize
@@ -56,8 +56,10 @@ namespace WulaFallenEmpire
// 关键修改:使用配置控制是否暂停游戏
this.forcePause = Config.pauseGameOnOpen;
- this.absorbInputAroundWindow = true;
+ this.absorbInputAroundWindow = false; // Allow interaction with other UI elements
this.doCloseX = true;
+ this.draggable = true; // Allow dragging
+ this.resizeable = true; // Allow resizing
// 根据配置设置是否绘制窗口背景和阴影
this.doWindowBackground = Config.showMainWindow;
@@ -280,7 +282,7 @@ namespace WulaFallenEmpire
///
/// 修复的描述区域滚动视图 - 只显示纵向滚动条
///
- private void DrawDescriptionScrollView(Rect outRect, string text)
+ protected virtual void DrawDescriptionScrollView(Rect outRect, string text)
{
try
{
@@ -315,7 +317,7 @@ namespace WulaFallenEmpire
}
// 绘制单个选项 - 使用自定义样式
- private void DrawSingleOption(Rect rect, EventOption option)
+ protected virtual void DrawSingleOption(Rect rect, EventOption option)
{
string reason;
bool conditionsMet = AreConditionsMet(option.conditions, out reason);
@@ -500,7 +502,7 @@ namespace WulaFallenEmpire
}
// 绘制选项区域
- private void DrawOptions(Rect rect, List options)
+ protected virtual void DrawOptions(Rect rect, List options)
{
if (options == null || options.Count == 0)
return;
@@ -599,7 +601,7 @@ namespace WulaFallenEmpire
}
}
- private void HandleAction(List conditionalEffects)
+ protected virtual void HandleAction(List conditionalEffects)
{
if (conditionalEffects.NullOrEmpty())
{
@@ -615,7 +617,7 @@ namespace WulaFallenEmpire
}
}
- private bool AreConditionsMet(List conditions, out string reason)
+ protected bool AreConditionsMet(List conditions, out string reason)
{
reason = "";
if (conditions.NullOrEmpty())
@@ -634,7 +636,7 @@ namespace WulaFallenEmpire
return true;
}
- private string GetDisabledReason(EventOption option, string reason)
+ protected string GetDisabledReason(EventOption option, string reason)
{
if (!option.disabledReason.NullOrEmpty())
{
diff --git a/Source/WulaFallenEmpire/EventSystem/Effect/Effect_OpenAIConversation.cs b/Source/WulaFallenEmpire/EventSystem/Effect/Effect_OpenAIConversation.cs
index bf3136f5..777c0916 100644
--- a/Source/WulaFallenEmpire/EventSystem/Effect/Effect_OpenAIConversation.cs
+++ b/Source/WulaFallenEmpire/EventSystem/Effect/Effect_OpenAIConversation.cs
@@ -12,9 +12,10 @@ namespace WulaFallenEmpire
public override void Execute(Window dialog = null)
{
- if (!RimTalkBridge.IsRimTalkActive)
+ // Check if API Key is configured in local settings
+ if (string.IsNullOrEmpty(WulaFallenEmpireMod.settings.apiKey))
{
- Messages.Message("RimTalk mod is not active. AI conversation cannot be started.", MessageTypeDefOf.RejectInput, false);
+ Messages.Message("AI API Key is not configured in Mod Settings. AI conversation cannot be started.", MessageTypeDefOf.RejectInput, false);
return;
}
diff --git a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
index 5f7afa9f..a38e8689 100644
--- a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
+++ b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
@@ -62,6 +62,10 @@
..\..\..\..\..\..\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll
False
+
+ ..\..\..\..\..\..\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.UnityWebRequestModule.dll
+ False
+
..\..\..\..\..\..\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.IMGUIModule.dll
False
diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs
index 3a41b8fe..47481dea 100644
--- a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs
+++ b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs
@@ -9,8 +9,12 @@ namespace WulaFallenEmpire
[StaticConstructorOnStartup]
public class WulaFallenEmpireMod : Mod
{
+ public static WulaFallenEmpireSettings settings;
+
public WulaFallenEmpireMod(ModContentPack content) : base(content)
{
+ settings = GetSettings();
+
// 初始化Harmony
var harmony = new Harmony("tourswen.wulafallenempire"); // 替换为您的唯一Mod ID
harmony.PatchAll(Assembly.GetExecutingAssembly());
@@ -18,6 +22,30 @@ namespace WulaFallenEmpire
Log.Message("[WulaFallenEmpire] Harmony patches applied.");
}
+ public override void DoSettingsWindowContents(Rect inRect)
+ {
+ Listing_Standard listingStandard = new Listing_Standard();
+ listingStandard.Begin(inRect);
+
+ listingStandard.Label("Wula_AISettings_Title".Translate());
+
+ listingStandard.Label("Wula_AISettings_ApiKey".Translate());
+ settings.apiKey = listingStandard.TextEntry(settings.apiKey);
+
+ listingStandard.Label("Wula_AISettings_BaseUrl".Translate());
+ settings.baseUrl = listingStandard.TextEntry(settings.baseUrl);
+
+ listingStandard.Label("Wula_AISettings_Model".Translate());
+ settings.model = listingStandard.TextEntry(settings.model);
+
+ listingStandard.End();
+ base.DoSettingsWindowContents(inRect);
+ }
+
+ public override string SettingsCategory()
+ {
+ return "Wula Fallen Empire";
+ }
}
[StaticConstructorOnStartup]
diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs
new file mode 100644
index 00000000..c19a09f9
--- /dev/null
+++ b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs
@@ -0,0 +1,19 @@
+using Verse;
+
+namespace WulaFallenEmpire
+{
+ public class WulaFallenEmpireSettings : ModSettings
+ {
+ public string apiKey = "";
+ public string baseUrl = "https://api.openai.com/v1";
+ public string model = "gpt-3.5-turbo";
+
+ public override void ExposeData()
+ {
+ Scribe_Values.Look(ref apiKey, "apiKey", "");
+ Scribe_Values.Look(ref baseUrl, "baseUrl", "https://api.openai.com/v1");
+ Scribe_Values.Look(ref model, "model", "gpt-3.5-turbo");
+ base.ExposeData();
+ }
+ }
+}
\ No newline at end of file