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_]+)(?:>.*?\1>|/>)", "", 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);
}