diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index 4cdaa017..3354aa7e 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/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml
index adafc0b5..79e7e820 100644
--- a/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml
+++ b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml
@@ -155,6 +155,5 @@
发送
错误:内部系统故障。{0}
错误:连接丢失。“军团”保持沉默。
- {FACTION_name} 已经在附近投下了一些资源。
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIHistoryManager.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIHistoryManager.cs
index c3b2990f..70080b58 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/AIHistoryManager.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/AIHistoryManager.cs
@@ -10,7 +10,7 @@ namespace WulaFallenEmpire.EventSystem.AI
public class AIHistoryManager : WorldComponent
{
private string _saveId;
- private Dictionary> _cache = new Dictionary>();
+ private Dictionary> _cache = new Dictionary>();
public AIHistoryManager(World world) : base(world)
{
@@ -35,7 +35,7 @@ namespace WulaFallenEmpire.EventSystem.AI
return Path.Combine(GetSaveDirectory(), $"{_saveId}_{eventDefName}.json");
}
- public List GetHistory(string eventDefName)
+ public List<(string role, string message)> GetHistory(string eventDefName)
{
if (_cache.TryGetValue(eventDefName, out var cachedHistory))
{
@@ -61,10 +61,10 @@ namespace WulaFallenEmpire.EventSystem.AI
}
}
- return new List();
+ return new List<(string role, string message)>();
}
- public void SaveHistory(string eventDefName, List history)
+ public void SaveHistory(string eventDefName, List<(string role, string message)> history)
{
_cache[eventDefName] = history;
string path = GetFilePath(eventDefName);
@@ -93,7 +93,7 @@ namespace WulaFallenEmpire.EventSystem.AI
public static class SimpleJsonParser
{
- public static string Serialize(List history)
+ public static string Serialize(List<(string role, string message)> history)
{
StringBuilder sb = new StringBuilder();
sb.Append("[");
@@ -102,8 +102,7 @@ namespace WulaFallenEmpire.EventSystem.AI
var item = history[i];
sb.Append("{");
sb.Append($"\"role\":\"{Escape(item.role)}\",");
- sb.Append($"\"content\":\"{Escape(item.content)}\"");
- // Note: tool_calls are not serialized for history to keep it simple.
+ sb.Append($"\"message\":\"{Escape(item.message)}\"");
sb.Append("}");
if (i < history.Count - 1) sb.Append(",");
}
@@ -111,11 +110,13 @@ namespace WulaFallenEmpire.EventSystem.AI
return sb.ToString();
}
- public static List Deserialize(string json)
+ public static List<(string role, string message)> Deserialize(string json)
{
- var result = new List();
+ var result = new List<(string role, string message)>();
if (string.IsNullOrEmpty(json)) return result;
+ // Very basic parser, assumes standard format produced by Serialize
+ // Remove outer brackets
json = json.Trim();
if (json.StartsWith("[") && json.EndsWith("]"))
{
@@ -123,6 +124,10 @@ namespace WulaFallenEmpire.EventSystem.AI
}
if (string.IsNullOrEmpty(json)) return result;
+
+ // Split by objects
+ // This is fragile if objects contain nested objects or escaped braces, but for this specific structure it's fine
+ // We are splitting by "},{" which is risky. Better to iterate.
int depth = 0;
int start = 0;
@@ -165,43 +170,22 @@ namespace WulaFallenEmpire.EventSystem.AI
}
return dict;
}
-
- public static List> ParseArray(string json)
- {
- var list = new List>();
- json = json.Trim('[', ']');
- 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)
- {
- list.Add(Parse(json.Substring(start, i - start + 1)));
- }
- }
- }
- return list;
- }
- private static ApiMessage ParseObject(string json)
+ private static (string role, string message) ParseObject(string json)
{
- var msg = new ApiMessage();
+ string role = null;
+ string message = null;
+
var dict = Parse(json);
- if (dict.TryGetValue("role", out string r)) msg.role = r;
- if (dict.TryGetValue("content", out string c)) msg.content = c;
- return msg;
+ if (dict.TryGetValue("role", out string r)) role = r;
+ if (dict.TryGetValue("message", out string m)) message = m;
+
+ return (role, message);
}
private static string[] SplitByComma(string input)
{
+ // Split by comma but ignore commas inside quotes
var list = new List();
bool inQuote = false;
int start = 0;
@@ -220,6 +204,7 @@ namespace WulaFallenEmpire.EventSystem.AI
private static string[] SplitByColon(string input)
{
+ // Split by first colon outside quotes
bool inQuote = false;
for (int i = 0; i < input.Length; i++)
{
@@ -238,7 +223,7 @@ namespace WulaFallenEmpire.EventSystem.AI
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
}
- public static string Unescape(string s) // Changed to public
+ private static string Unescape(string s)
{
if (s == null) return "";
return s.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\\"", "\"").Replace("\\\\", "\\");
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs b/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs
index 125bd8d8..a1db83f4 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine.Networking;
@@ -21,7 +20,7 @@ namespace WulaFallenEmpire.EventSystem.AI
_model = model;
}
- public async Task GetChatCompletionAsync(string instruction, List messages)
+ public async Task GetChatCompletionAsync(string instruction, List<(string role, string message)> messages)
{
if (string.IsNullOrEmpty(_baseUrl))
{
@@ -30,56 +29,32 @@ namespace WulaFallenEmpire.EventSystem.AI
}
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";
-
- jsonBuilder.Append("{");
- jsonBuilder.Append($"\"role\": \"{role}\"");
-
- if (!string.IsNullOrEmpty(msg.content))
- {
- jsonBuilder.Append($", \"content\": \"{EscapeJson(msg.content)}\"");
- }
-
- if (msg.tool_calls != null && msg.tool_calls.Any())
- {
- jsonBuilder.Append(", \"tool_calls\": [");
- for (int j = 0; j < msg.tool_calls.Count; j++)
- {
- var toolCall = msg.tool_calls[j];
- jsonBuilder.Append("{");
- jsonBuilder.Append($"\"id\": \"{toolCall.id}\",");
- jsonBuilder.Append($"\"type\": \"{toolCall.type}\",");
- jsonBuilder.Append($"\"function\": {{ \"name\": \"{toolCall.function.name}\", \"arguments\": \"{EscapeJson(toolCall.function.arguments)}\" }}");
- jsonBuilder.Append("}");
- if (j < msg.tool_calls.Count - 1) jsonBuilder.Append(",");
- }
- jsonBuilder.Append("]");
- }
-
- if (!string.IsNullOrEmpty(msg.tool_call_id))
- {
- jsonBuilder.Append($", \"tool_call_id\": \"{msg.tool_call_id}\"");
- }
-
- jsonBuilder.Append("}");
+ // Map other roles if needed
+
+ jsonBuilder.Append($"{{\"role\": \"{role}\", \"content\": \"{EscapeJson(msg.message)}\"}}");
if (i < messages.Count - 1) jsonBuilder.Append(",");
}
@@ -87,8 +62,8 @@ namespace WulaFallenEmpire.EventSystem.AI
jsonBuilder.Append("}");
string jsonBody = jsonBuilder.ToString();
- Log.Message($"[WulaAI] Sending request: {jsonBody}");
-
+ Log.Message($"[WulaAI] Sending request to {endpoint}");
+
using (UnityWebRequest request = new UnityWebRequest(endpoint, "POST"))
{
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
@@ -114,8 +89,8 @@ namespace WulaFallenEmpire.EventSystem.AI
}
string responseText = request.downloadHandler.text;
- Log.Message($"[WulaAI] Received raw response: {responseText}");
- return ParseApiResponse(responseText);
+ Log.Message($"[WulaAI] Raw Response: {responseText}");
+ return ExtractContent(responseText);
}
}
@@ -129,79 +104,66 @@ namespace WulaFallenEmpire.EventSystem.AI
.Replace("\t", "\\t");
}
- private ApiResponse ParseApiResponse(string json)
+ private string ExtractContent(string json)
{
try
{
- var parsed = SimpleJsonParser.Parse(json);
- if (parsed.TryGetValue("choices", out string choicesStr))
- {
- var choices = SimpleJsonParser.ParseArray(choicesStr);
- if (choices.Any())
- {
- var firstChoice = choices.First();
- if (firstChoice.TryGetValue("message", out string messageStr))
- {
- var message = SimpleJsonParser.Parse(messageStr);
- string content = null;
- if (message.TryGetValue("content", out string c)) content = c;
+ // Robust parsing for "content": "..." allowing for whitespace variations
+ int contentIndex = json.IndexOf("\"content\"");
+ if (contentIndex == -1) return null;
- List toolCalls = new List();
- if (message.TryGetValue("tool_calls", out string toolCallsStr))
- {
- var toolCallArray = SimpleJsonParser.ParseArray(toolCallsStr);
- foreach (var tc in toolCallArray)
- {
- if (tc.TryGetValue("id", out string id) &&
- tc.TryGetValue("type", out string type) &&
- tc.TryGetValue("function", out string functionStr))
- {
- var function = SimpleJsonParser.Parse(functionStr);
- if (function.TryGetValue("name", out string name) &&
- function.TryGetValue("arguments", out string args))
- {
- toolCalls.Add(new ToolCall { id = id, type = type, function = new ToolFunction { name = name, arguments = args } });
- }
- }
- }
- }
- return new ApiResponse { content = content, tool_calls = toolCalls };
+ // 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 API response: {ex}");
+ Log.Error($"[WulaAI] Error parsing response: {ex}");
}
return null;
}
}
-
- public class ApiMessage
- {
- public string role;
- public string content;
- public List tool_calls;
- public string tool_call_id; // For tool responses
- }
-
- public class ToolCall
- {
- public string id;
- public string type; // "function"
- public ToolFunction function;
- }
-
- public class ToolFunction
- {
- public string name;
- public string arguments; // JSON string
- }
-
- public class ApiResponse
- {
- public string content;
- public List tool_calls;
- }
}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs
index d3937a76..b382d872 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs
@@ -71,10 +71,6 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
if (thingsToDrop.Count > 0)
{
DropPodUtility.DropThingsNear(dropSpot, map, thingsToDrop);
-
- Faction faction = Find.FactionManager.FirstFactionOfDef(WulaDefOf.Wula_PIA_Legion_Faction);
- Messages.Message("Wula_ResourceDrop".Translate(faction.Named("FACTION")), new LookTargets(dropSpot, map), MessageTypeDefOf.PositiveEvent);
-
resultLog.Length -= 2; // Remove trailing comma
resultLog.Append($" at {dropSpot}.");
return resultLog.ToString();
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
index 8d8e71fd..8e6d5e92 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
@@ -11,7 +11,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
{
public class Dialog_AIConversation : Dialog_CustomDisplay
{
- private List _history = new List();
+ private List<(string role, string message)> _history = new List<(string role, string message)>();
private string _currentResponse = "";
private List _options = new List();
private string _inputText = "";
@@ -36,9 +36,13 @@ Do not add any other text when using tools. Your response must be either a tool
**CRITICAL RULE: When the player requests resources (e.g., 'we are starving', 'give us steel'), you MUST FIRST use the 'get_colonist_status' and 'get_map_resources' tools to verify their claims. After receiving the tool results, you will then decide whether to use the 'spawn_resources' tool in your NEXT turn.**
-**CRITICAL RULE: After a tool is executed, you will receive a message with 'role: tool'. You MUST then generate a natural language response to the user, explaining the outcome of the tool's action.**
-
If you are not using a tool, provide a normal conversational response.
+After a tool use, you will receive the result, and then you should respond to the player describing what happened.
+Generate 1-3 short, distinct response options for the player at the end of your turn, formatted as:
+OPTIONS:
+1. Option 1
+2. Option 2
+3. Option 3
IMPORTANT: You can change your visual expression using the 'change_expression' tool.
Expression IDs:
@@ -122,21 +126,22 @@ Use these expressions to match your tone and reaction to the player.
if (!def.descriptions.NullOrEmpty())
{
_currentResponse = def.descriptions.RandomElement().Translate();
- _history.Add(new ApiMessage { role = "assistant", content = _currentResponse });
- await GenerateResponse(isContinuation: false, customInstruction: "The conversation has started. Please generate 3 initial response options for the player based on your greeting.");
+ _history.Add(("assistant", _currentResponse));
+ _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(new ApiMessage { role = "user", content = "Hello" });
+ _history.Add(("user", "Hello"));
await GenerateResponse();
}
}
else
{
- var lastMessage = _history.LastOrDefault();
- if (lastMessage != null && lastMessage.role == "assistant" && lastMessage.tool_calls == null)
+ var lastAIResponse = _history.LastOrDefault(x => x.role == "assistant");
+ if (lastAIResponse.message != null)
{
- ParseResponse(lastMessage.content ?? "");
+ ParseResponse(lastAIResponse.message);
}
else
{
@@ -159,18 +164,19 @@ Use these expressions to match your tone and reaction to the player.
return $"{baseInstruction}\n{goodwillContext}\nIMPORTANT: You MUST reply in the following language: {language}.";
}
- private async Task GenerateResponse(bool isContinuation = false, string customInstruction = null)
+ private async Task GenerateResponse(bool isContinuation = false)
{
if (!isContinuation)
{
if (_isThinking) return;
_isThinking = true;
+ _options.Clear();
}
try
{
CompressHistoryIfNeeded();
- string systemInstruction = customInstruction ?? (GetSystemInstruction() + GetToolDescriptions());
+ string systemInstruction = GetSystemInstruction() + GetToolDescriptions();
var settings = WulaFallenEmpireMod.settings;
if (string.IsNullOrEmpty(settings.apiKey))
@@ -181,26 +187,32 @@ Use these expressions to match your tone and reaction to the player.
}
var client = new SimpleAIClient(settings.apiKey, settings.baseUrl, settings.model);
- ApiResponse response = await client.GetChatCompletionAsync(systemInstruction, _history);
+ string response = await client.GetChatCompletionAsync(systemInstruction, _history);
- if (response == null)
+ if (string.IsNullOrEmpty(response))
{
_currentResponse = "Wula_AI_Error_ConnectionLost".Translate();
_isThinking = false;
return;
}
- _history.Add(new ApiMessage { role = "assistant", content = response.content, tool_calls = response.tool_calls });
-
- if (response.tool_calls != null && response.tool_calls.Any())
+ var toolCallMatch = System.Text.RegularExpressions.Regex.Match(response, @"`(\w+)\((.*)\)`");
+ if (toolCallMatch.Success)
{
- await HandleToolUsage(response.tool_calls);
+ string toolName = toolCallMatch.Groups[1].Value;
+ string args = toolCallMatch.Groups[2].Value;
+
+ _history.Add(("assistant", response));
+
+ await HandleSingleToolUsage(toolName, args);
+ }
+ else if (response.Trim().StartsWith("["))
+ {
+ await HandleToolUsage(response);
}
else
{
- _currentResponse = response.content ?? "";
- ParseResponse(_currentResponse);
- _scrollPosition.y = float.MaxValue; // Force scroll to bottom
+ ParseResponse(response);
}
}
catch (Exception ex)
@@ -216,14 +228,14 @@ Use these expressions to match your tone and reaction to the player.
private void CompressHistoryIfNeeded()
{
- int estimatedTokens = _history.Sum(h => (h.content ?? "").Length) / CharsPerToken;
+ int estimatedTokens = _history.Sum(h => h.message.Length) / CharsPerToken;
if (estimatedTokens > MaxHistoryTokens)
{
int removeCount = _history.Count / 2;
if (removeCount > 0)
{
_history.RemoveRange(0, removeCount);
- _history.Insert(0, new ApiMessage { role = "system", content = "[Previous conversation summarized]" });
+ _history.Insert(0, ("system", "[Previous conversation summarized]"));
}
}
}
@@ -239,32 +251,100 @@ Use these expressions to match your tone and reaction to the player.
return sb.ToString();
}
- private async Task HandleToolUsage(List toolCalls)
+ private async Task HandleSingleToolUsage(string toolName, string args)
{
- foreach (var toolCall in toolCalls)
+ StringBuilder combinedResults = new StringBuilder();
+ var tool = _tools.FirstOrDefault(t => t.Name == toolName);
+ if (tool != null)
{
- Log.Message($"[WulaAI] Executing tool '{toolCall.function.name}' with args: {toolCall.function.arguments}");
- var tool = _tools.FirstOrDefault(t => t.Name == toolCall.function.name);
- string result;
+ string result = tool.Execute(args).Trim();
+ if (toolName == "modify_goodwill") combinedResults.Append($"Tool '{toolName}' Result (Invisible): {result}");
+ else combinedResults.Append($"Tool '{toolName}' Result: {result}");
+ }
+ else
+ {
+ string errorMsg = $"Error: Tool '{toolName}' not found.";
+ Log.Error($"[WulaAI] {errorMsg}");
+ combinedResults.AppendLine(errorMsg);
+ }
+
+ _history.Add(("tool", combinedResults.ToString()));
+ await GenerateResponse(isContinuation: true);
+ }
+
+ private async Task HandleToolUsage(string json)
+ {
+ List<(string toolName, string args)> toolCalls = new List<(string, string)>();
+ 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)
+ {
+ string callJson = json.Substring(start, i - start + 1);
+ var parsedCall = SimpleJsonParser.Parse(callJson);
+ if (parsedCall.TryGetValue("tool", out string toolName) && parsedCall.TryGetValue("args", out string args))
+ {
+ toolCalls.Add((toolName, args));
+ }
+ }
+ }
+ }
+
+ if (!toolCalls.Any())
+ {
+ ParseResponse(json);
+ return;
+ }
+
+ StringBuilder combinedResults = new StringBuilder();
+ foreach (var (toolName, args) in toolCalls)
+ {
+ var tool = _tools.FirstOrDefault(t => t.Name == toolName);
if (tool != null)
{
- result = tool.Execute(toolCall.function.arguments).Trim();
+ string result = tool.Execute(args).Trim();
+ if (toolName == "modify_goodwill") combinedResults.Append($"Tool '{toolName}' Result (Invisible): {result} ");
+ else combinedResults.Append($"Tool '{toolName}' Result: {result} ");
}
else
{
- result = $"Error: Tool '{toolCall.function.name}' not found.";
- Log.Error($"[WulaAI] {result}");
+ string errorMsg = $"Error: Tool '{toolName}' not found.";
+ Log.Error($"[WulaAI] {errorMsg}");
+ combinedResults.AppendLine(errorMsg);
}
- Log.Message($"[WulaAI] Tool '{toolCall.function.name}' returned: {result}");
- _history.Add(new ApiMessage { role = "tool", tool_call_id = toolCall.id, content = result });
}
-
+
+ _history.Add(("assistant", json));
+ _history.Add(("tool", combinedResults.ToString()));
await GenerateResponse(isContinuation: true);
}
private void ParseResponse(string rawResponse)
{
- _currentResponse = rawResponse.Trim();
+ _currentResponse = rawResponse;
+ var parts = rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None);
+ if (_history.Count == 0 || _history.Last().role != "assistant" || _history.Last().message != rawResponse)
+ {
+ _history.Add(("assistant", rawResponse));
+ }
+
+ if (parts.Length > 1)
+ {
+ _options.Clear();
+ var optionsLines = parts[1].Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in optionsLines)
+ {
+ string opt = line.Trim();
+ int dotIndex = opt.IndexOf('.');
+ if (dotIndex != -1 && dotIndex < 4) opt = opt.Substring(dotIndex + 1).Trim();
+ if(!string.IsNullOrEmpty(opt)) _options.Add(opt);
+ }
+ }
}
public override void DoWindowContents(Rect inRect)
@@ -289,8 +369,9 @@ Use these expressions to match your tone and reaction to the player.
curY += nameHeight + 10f;
float inputHeight = 30f;
+ float optionsHeight = _options.Any() ? 100f : 0f;
float bottomMargin = 10f;
- float descriptionHeight = inRect.height - curY - inputHeight - bottomMargin;
+ float descriptionHeight = inRect.height - curY - inputHeight - optionsHeight - bottomMargin;
Rect descriptionRect = new Rect(inRect.x, curY, width, descriptionHeight);
DrawChatHistory(descriptionRect);
@@ -303,6 +384,14 @@ Use these expressions to match your tone and reaction to the player.
}
curY += descriptionHeight + 10f;
+ 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;
+
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()))
@@ -317,60 +406,45 @@ Use these expressions to match your tone and reaction to the player.
private void DrawChatHistory(Rect rect)
{
- var visibleHistory = _history.Where(e => e.role != "tool" && !string.IsNullOrEmpty(e.content)).ToList();
- var lastAiMessage = visibleHistory.LastOrDefault(e => e.role == "assistant");
+ Rect viewRect = new Rect(0f, 0f, rect.width - 16f, 0f);
+ float tempY = 0f;
- float totalHeight = 0f;
- foreach (var entry in visibleHistory)
+ Text.Font = GameFont.Small;
+ foreach (var entry in _history)
{
- GameFont originalFont = Text.Font;
- bool isLastAiMsg = entry == lastAiMessage;
- Text.Font = isLastAiMsg ? GameFont.Medium : GameFont.Small;
-
- string text = entry.content;
- totalHeight += Text.CalcHeight(text, rect.width - 16f) + 10f;
-
- Text.Font = originalFont;
+ if (entry.role == "tool") continue;
+ string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message;
+ tempY += Text.CalcHeight(text, viewRect.width) + 10f;
}
+ viewRect.height = tempY;
- Rect viewRect = new Rect(0f, 0f, rect.width - 16f, totalHeight);
Widgets.BeginScrollView(rect, ref _scrollPosition, viewRect);
float curY = 0f;
- for (int i = 0; i < visibleHistory.Count; i++)
+ foreach (var entry in _history)
{
- var entry = visibleHistory[i];
-
- GameFont originalFont = Text.Font;
- bool isLastAiMsg = entry == lastAiMessage;
- Text.Font = isLastAiMsg ? GameFont.Medium : GameFont.Small;
+ if (entry.role == "tool") continue;
- string text = entry.content;
+ string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message;
float height = Text.CalcHeight(text, viewRect.width);
Rect labelRect = new Rect(0f, curY, viewRect.width, height);
- string label;
if (entry.role == "user")
{
- label = $"你: {text}";
+ Text.Anchor = TextAnchor.MiddleLeft;
+ Widgets.Label(labelRect, $"你: {text}");
}
- else // assistant
+ else
{
- label = $"P.I.A: {text}";
+ Text.Anchor = TextAnchor.MiddleLeft;
+ Widgets.Label(labelRect, $"P.I.A: {text}");
}
-
- Widgets.Label(labelRect, label);
- curY += height + 10f; // Use calculated height for spacing
-
- Text.Font = originalFont;
- }
-
- if (Event.current.type == EventType.Layout)
- {
- _scrollPosition.y = viewRect.height;
+ curY += height + 10f;
}
+ Text.Anchor = TextAnchor.UpperLeft;
Widgets.EndScrollView();
+ Text.Font = GameFont.Medium;
}
private string ParseResponseForDisplay(string rawResponse)
@@ -378,10 +452,76 @@ Use these expressions to match your tone and reaction to the player.
return rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None)[0].Trim();
}
+ protected override void DrawSingleOption(Rect rect, EventOption option)
+ {
+ 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);
+
+ var originalColor = GUI.color;
+ var originalFont = Text.Font;
+ var originalTextColor = GUI.contentColor;
+ var originalAnchor = Text.Anchor;
+
+ try
+ {
+ Text.Anchor = TextAnchor.MiddleCenter;
+ Text.Font = GameFont.Small;
+ 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;
+ }
+ }
+
+ 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);
+ }
+ }
private async void SelectOption(string text)
{
- _history.Add(new ApiMessage { role = "user", content = text });
+ _history.Add(("user", text));
await GenerateResponse();
}
}
diff --git a/Source/WulaFallenEmpire/WulaDefOf.cs b/Source/WulaFallenEmpire/WulaDefOf.cs
index 8276a64d..954f262e 100644
--- a/Source/WulaFallenEmpire/WulaDefOf.cs
+++ b/Source/WulaFallenEmpire/WulaDefOf.cs
@@ -80,8 +80,8 @@ namespace WulaFallenEmpire
[DefOf]
public static class WulaDefOf
{
- public static FactionDef Wula_PIA_Legion_Faction;
-
+ // public static PawnTableDef WULA_AutonomousMechs;
+
static WulaDefOf()
{
DefOfHelper.EnsureInitializedInCtor(typeof(WulaDefOf));