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));