diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index 816f0d25..ef3b71c0 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/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml b/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml deleted file mode 100644 index b16cecbb..00000000 --- a/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - {FACTION_name}已经在附近投下了一些资源。 - - \ No newline at end of file diff --git a/Source/WulaFallenEmpire/3516260226.code-workspace b/Source/WulaFallenEmpire/3516260226.code-workspace index 192501ad..67ab2782 100644 --- a/Source/WulaFallenEmpire/3516260226.code-workspace +++ b/Source/WulaFallenEmpire/3516260226.code-workspace @@ -18,6 +18,9 @@ }, { "path": "../../../../../../../../Users/Kalo/Downloads/RimTalk-main" + }, + { + "path": "../../../../../../../../Users/Kalo/Downloads/openai_token-main" } ], "settings": {} diff --git a/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs b/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs index 3288bb22..32604963 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs @@ -12,6 +12,7 @@ namespace WulaFallenEmpire.EventSystem.AI private readonly string _apiKey; private readonly string _baseUrl; private readonly string _model; + private const int MaxLogChars = 2000; public SimpleAIClient(string apiKey, string baseUrl, string model) { @@ -41,28 +42,36 @@ namespace WulaFallenEmpire.EventSystem.AI jsonBuilder.Append("\"messages\": ["); // System instruction + bool firstMessage = true; if (!string.IsNullOrEmpty(instruction)) { - jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}},"); + jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}}"); + firstMessage = false; } // Messages for (int i = 0; i < messages.Count; i++) { var msg = messages[i]; - string role = msg.role.ToLower(); + string role = (msg.role ?? "user").ToLowerInvariant(); if (role == "ai") role = "assistant"; - // Map other roles if needed + else if (role == "tool") role = "system"; // Internal-only role; map to supported role for Chat Completions APIs. + else if (role != "system" && role != "user" && role != "assistant") role = "user"; + if (!firstMessage) jsonBuilder.Append(","); jsonBuilder.Append($"{{\"role\": \"{role}\", \"content\": \"{EscapeJson(msg.message)}\"}}"); - if (i < messages.Count - 1) jsonBuilder.Append(","); + firstMessage = false; } jsonBuilder.Append("]"); jsonBuilder.Append("}"); string jsonBody = jsonBuilder.ToString(); - Log.Message($"[WulaAI] Sending request to {endpoint}:\n{jsonBody}"); + if (Prefs.DevMode) + { + Log.Message($"[WulaAI] Sending request to {endpoint} (model={_model}, messages={messages?.Count ?? 0})"); + Log.Message($"[WulaAI] Request body (truncated):\n{TruncateForLog(jsonBody)}"); + } using (UnityWebRequest request = new UnityWebRequest(endpoint, "POST")) { @@ -84,24 +93,57 @@ namespace WulaFallenEmpire.EventSystem.AI if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError) { - Log.Error($"[WulaAI] API Error: {request.error}\nResponse: {request.downloadHandler.text}"); + Log.Error($"[WulaAI] API Error: {request.error}\nResponse (truncated): {TruncateForLog(request.downloadHandler.text)}"); return null; } string responseText = request.downloadHandler.text; - Log.Message($"[WulaAI] Raw Response: {responseText}"); + if (Prefs.DevMode) + { + Log.Message($"[WulaAI] Raw Response (truncated): {TruncateForLog(responseText)}"); + } return ExtractContent(responseText); } } + private static string TruncateForLog(string s) + { + if (string.IsNullOrEmpty(s)) return s; + if (s.Length <= MaxLogChars) return s; + return s.Substring(0, MaxLogChars) + $"... (truncated, total {s.Length} chars)"; + } + private string EscapeJson(string s) { if (s == null) return ""; - return s.Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t"); + + StringBuilder sb = new StringBuilder(s.Length + 16); + for (int i = 0; i < s.Length; i++) + { + char c = s[i]; + switch (c) + { + case '\\': sb.Append("\\\\"); break; + case '"': sb.Append("\\\""); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + case '\b': sb.Append("\\b"); break; + case '\f': sb.Append("\\f"); break; + default: + if (c < 0x20) + { + sb.Append("\\u"); + sb.Append(((int)c).ToString("x4")); + } + else + { + sb.Append(c); + } + break; + } + } + return sb.ToString(); } private string ExtractContent(string json) @@ -116,13 +158,13 @@ namespace WulaFallenEmpire.EventSystem.AI string[] lines = json.Split(new[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); foreach (string line in lines) { - string trimmedLine = line.Trim(); - if (trimmedLine == "data: [DONE]") continue; - if (trimmedLine.StartsWith("data: ")) - { - string dataJson = trimmedLine.Substring(6); + string trimmedLine = line.Trim(); + if (trimmedLine == "data: [DONE]") continue; + if (trimmedLine.StartsWith("data: ")) + { + string dataJson = trimmedLine.Substring(6); // Extract content from this chunk - string chunkContent = ExtractJsonValue(dataJson, "content"); + string chunkContent = TryExtractAssistantContent(dataJson) ?? ExtractJsonValue(dataJson, "content"); if (!string.IsNullOrEmpty(chunkContent)) { fullContent.Append(chunkContent); @@ -134,7 +176,7 @@ namespace WulaFallenEmpire.EventSystem.AI else { // Standard non-stream format - return ExtractJsonValue(json, "content"); + return TryExtractAssistantContent(json) ?? ExtractJsonValue(json, "content"); } } catch (Exception ex) @@ -144,12 +186,109 @@ namespace WulaFallenEmpire.EventSystem.AI return null; } - private string ExtractJsonValue(string json, string key) + private static string TryExtractAssistantContent(string json) + { + if (string.IsNullOrWhiteSpace(json)) return null; + + int choicesIndex = json.IndexOf("\"choices\"", StringComparison.Ordinal); + if (choicesIndex == -1) return null; + + string firstChoiceJson = TryExtractFirstChoiceObject(json, choicesIndex); + if (string.IsNullOrEmpty(firstChoiceJson)) return null; + + int messageIndex = firstChoiceJson.IndexOf("\"message\"", StringComparison.Ordinal); + if (messageIndex != -1) + { + return ExtractJsonValue(firstChoiceJson, "content", messageIndex); + } + + int deltaIndex = firstChoiceJson.IndexOf("\"delta\"", StringComparison.Ordinal); + if (deltaIndex != -1) + { + return ExtractJsonValue(firstChoiceJson, "content", deltaIndex); + } + + return ExtractJsonValue(firstChoiceJson, "text", 0); + } + + private static string TryExtractFirstChoiceObject(string json, int choicesKeyIndex) + { + int arrayStart = json.IndexOf('[', choicesKeyIndex); + if (arrayStart == -1) return null; + + int objStart = json.IndexOf('{', arrayStart); + if (objStart == -1) return null; + + int objEnd = FindMatchingBrace(json, objStart); + if (objEnd == -1) return null; + + return json.Substring(objStart, objEnd - objStart + 1); + } + + private static int FindMatchingBrace(string json, int startIndex) + { + int depth = 0; + bool inString = false; + bool escaped = false; + + for (int i = startIndex; i < json.Length; i++) + { + char c = json[i]; + if (inString) + { + if (escaped) + { + escaped = false; + continue; + } + + if (c == '\\') + { + escaped = true; + continue; + } + + if (c == '"') + { + inString = false; + } + + continue; + } + + if (c == '"') + { + inString = true; + continue; + } + + if (c == '{') + { + depth++; + continue; + } + + if (c == '}') + { + depth--; + if (depth == 0) return i; + } + } + + return -1; + } + + private static string ExtractJsonValue(string json, string key) { // Simple parser to find "key": "value" // This is not a full JSON parser and assumes standard formatting + return ExtractJsonValue(json, key, 0); + } + + private static string ExtractJsonValue(string json, string key, int startIndex) + { string keyPattern = $"\"{key}\""; - int keyIndex = json.IndexOf(keyPattern); + int keyIndex = json.IndexOf(keyPattern, startIndex, StringComparison.Ordinal); if (keyIndex == -1) return null; // Find the colon after the key @@ -196,4 +335,4 @@ namespace WulaFallenEmpire.EventSystem.AI return null; } } -} \ 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 74d37fc3..c14dccc1 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using RimWorld; @@ -90,16 +91,24 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools if (itemsToSpawn.Count == 0) { - return "Error: No valid items found in request. Usage: ......"; + string msg = "Error: No valid items found in request. Usage: ......"; + Messages.Message(msg, MessageTypeDefOf.RejectInput); + return msg; } - Map map = Find.CurrentMap; + Map map = GetTargetMap(); if (map == null) { - return "Error: No active map."; + string msg = "Error: No active map."; + Messages.Message(msg, MessageTypeDefOf.RejectInput); + return msg; } IntVec3 dropSpot = DropCellFinder.TradeDropSpot(map); + if (!dropSpot.IsValid || !dropSpot.InBounds(map)) + { + dropSpot = DropCellFinder.RandomDropSpot(map); + } List thingsToDrop = new List(); StringBuilder resultLog = new StringBuilder(); resultLog.Append("Success: Dropped "); @@ -114,27 +123,60 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools if (thingsToDrop.Count > 0) { - DropPodUtility.DropThingsNear(dropSpot, map, thingsToDrop); - - Faction faction = Find.FactionManager.FirstFactionOfDef(FactionDef.Named("Wula_PIA_Legion_Faction")); - if (faction != null) + // If the conversation window pauses the game, incoming drop pods may not "land" until unpaused. + // To keep this tool reliable, place items immediately when paused; otherwise, use drop pods. + bool isPaused = Find.TickManager != null && Find.TickManager.Paused; + if (isPaused) { - Messages.Message("Wula_ResourceDrop".Translate(faction.def.defName.Named("FACTION_name")), new LookTargets(dropSpot, map), MessageTypeDefOf.PositiveEvent); + foreach (var thing in thingsToDrop) + { + GenPlace.TryPlaceThing(thing, dropSpot, map, ThingPlaceMode.Near); + } + } + else + { + DropPodUtility.DropThingsNear(dropSpot, map, thingsToDrop); } + Faction faction = Find.FactionManager.FirstFactionOfDef(FactionDef.Named("Wula_PIA_Legion_Faction")); + string letterText = faction != null + ? "Wula_ResourceDrop".Translate(faction.def.defName.Named("FACTION_name")) + : "Wula_ResourceDrop".Translate("Unknown".Named("FACTION_name")); + Messages.Message(letterText, new LookTargets(dropSpot, map), MessageTypeDefOf.PositiveEvent); + resultLog.Length -= 2; // Remove trailing comma - resultLog.Append($" at {dropSpot}."); + resultLog.Append($" at {dropSpot}. {(isPaused ? "(placed immediately because game is paused)" : "(drop pods inbound)")}"); return resultLog.ToString(); } else { - return "Error: Failed to create items."; + string msg = "Error: Failed to create items."; + Messages.Message(msg, MessageTypeDefOf.RejectInput); + return msg; } } catch (Exception ex) { - return $"Error: {ex.Message}"; + string msg = $"Error: {ex.Message}"; + Messages.Message(msg, MessageTypeDefOf.RejectInput); + return msg; } } + + private static Map GetTargetMap() + { + Map map = Find.CurrentMap; + if (map != null) return map; + + if (Find.Maps != null) + { + Map homeMap = Find.Maps.FirstOrDefault(m => m != null && m.IsPlayerHome); + if (homeMap != null) return homeMap; + + if (Find.Maps.Count > 0) return Find.Maps[0]; + } + + 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 cd7cd94d..fac1a280 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs @@ -228,6 +228,19 @@ When the player requests any form of resources, you MUST follow this multi-turn StartConversation(); } + private void PersistHistory() + { + try + { + var historyManager = Find.World?.GetComponent(); + historyManager?.SaveHistory(def.defName, _history); + } + catch (Exception ex) + { + Log.Error($"[WulaAI] Failed to persist AI history: {ex}"); + } + } + private void LoadPortraits() { for (int i = 1; i <= 6; i++) @@ -281,6 +294,7 @@ When the player requests any form of resources, you MUST follow this multi-turn if (_history.Count == 0) { _history.Add(("user", "Hello")); + PersistHistory(); await GenerateResponse(); } else @@ -379,6 +393,7 @@ When the player requests any form of resources, you MUST follow this multi-turn { _history.RemoveRange(0, removeCount); _history.Insert(0, ("system", "[Previous conversation summarized]")); + PersistHistory(); } } } @@ -422,8 +437,17 @@ When the player requests any form of resources, you MUST follow this multi-turn argsXml = contentMatch.Groups[1].Value; } - Log.Message($"[WulaAI] Executing tool: {toolName} with args: {argsXml}"); + if (Prefs.DevMode) + { + Log.Message($"[WulaAI] Executing tool: {toolName} with args: {argsXml}"); + } + string result = tool.Execute(argsXml).Trim(); + if (Prefs.DevMode && !string.IsNullOrEmpty(result)) + { + string toLog = result.Length <= 2000 ? result : result.Substring(0, 2000) + $"... (truncated, total {result.Length} chars)"; + Log.Message($"[WulaAI] Tool '{toolName}' result: {toLog}"); + } if (toolName == "modify_goodwill") { @@ -436,9 +460,9 @@ When the player requests any form of resources, you MUST follow this multi-turn } _history.Add(("assistant", xml)); - // Use role "user" for tool results to avoid API errors about tool_calls format. - // This is safe and won't break AI logic, as the AI still sees the result clearly. - _history.Add(("user", $"[Tool Results]\n{combinedResults.ToString().Trim()}")); + // Persist tool results with a dedicated role; the API request maps this role to a supported one. + _history.Add(("tool", $"[Tool Results]\n{combinedResults.ToString().Trim()}")); + PersistHistory(); // Check if there is any text content in the response string textContent = Regex.Replace(xml, @"<([a-zA-Z0-9_]+)(?:>.*?|/>)", "", RegexOptions.Singleline).Trim(); @@ -460,6 +484,7 @@ When the player requests any form of resources, you MUST follow this multi-turn { Log.Error($"[WulaAI] Exception in HandleXmlToolUsage: {ex}"); _history.Add(("tool", $"Error processing tool call: {ex.Message}")); + PersistHistory(); await GenerateResponse(isContinuation: true); } } @@ -473,6 +498,7 @@ When the player requests any form of resources, you MUST follow this multi-turn if (_history.Count == 0 || _history.Last().role != "assistant" || _history.Last().message != rawResponse) { _history.Add(("assistant", rawResponse)); + PersistHistory(); } } @@ -563,7 +589,9 @@ When the player requests any form of resources, you MUST follow this multi-turn try { float viewHeight = 0f; - var filteredHistory = _history.Where(e => e.role != "tool" && e.role != "system").ToList(); + var filteredHistory = _history + .Where(e => e.role != "system" && (Prefs.DevMode || e.role != "tool")) + .ToList(); // Pre-calculate height for (int i = 0; i < filteredHistory.Count; i++) { @@ -603,7 +631,7 @@ When the player requests any form of resources, you MUST follow this multi-turn if (entry.role == "user") { Text.Anchor = TextAnchor.MiddleRight; - Widgets.Label(labelRect, $"{text} :你"); + Widgets.Label(labelRect, $"{text}"); } else { @@ -707,6 +735,7 @@ When the player requests any form of resources, you MUST follow this multi-turn private async void SelectOption(string text) { _history.Add(("user", text)); + PersistHistory(); _scrollToBottom = true; await GenerateResponse(); } @@ -714,6 +743,7 @@ When the player requests any form of resources, you MUST follow this multi-turn public override void PostClose() { if (Instance == this) Instance = null; + PersistHistory(); base.PostClose(); HandleAction(def.dismissEffects); }