diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb
index f2addc2a..f31330a9 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb and b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb 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 f93e6a24..ddc8a9e8 100644
--- a/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml
+++ b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml
@@ -156,6 +156,8 @@
API 密钥:
API 地址 (Base URL):
模型名称:
+ 上下文保存长度 (Token 估算上限):
+ 控制 AI 对话历史在超过上限时自动压缩。数值越小越省成本,但 AI 更容易“忘记”。
启用流式传输 (实验性)
启用实时打字机效果。如果遇到问题请禁用。
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapPawns.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapPawns.cs
index 8dd75d30..3efffd79 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapPawns.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapPawns.cs
@@ -10,8 +10,16 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
public class Tool_GetMapPawns : AITool
{
public override string Name => "get_map_pawns";
- public override string Description => "Scans the current map and lists pawns. Supports filtering by relation (friendly/hostile/neutral), type (colonist/animal/mechanoid/humanlike), and status (prisoner/slave/guest/downed).";
- public override string UsageSchema => "string (optional, comma-separated: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed)int (optional, default 50)";
+ public override string Description => "Scans the current map and lists pawns (including corpses). Supports filtering by relation (friendly/hostile/neutral), type (colonist/animal/mech/humanlike), and status (prisoner/slave/guest/wild/downed/dead).";
+ public override string UsageSchema =>
+ "string (optional, comma-separated: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed, dead)true/false (optional, default true)int (optional, default 50)";
+
+ private struct MapPawnEntry
+ {
+ public Pawn Pawn;
+ public bool IsDead;
+ public IntVec3 Position;
+ }
public override string Execute(string args)
{
@@ -21,36 +29,85 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
string filterRaw = null;
if (parsed.TryGetValue("filter", out string f)) filterRaw = f;
+
int maxResults = 50;
if (parsed.TryGetValue("maxResults", out string maxStr) && int.TryParse(maxStr, out int mr))
{
maxResults = Math.Max(1, Math.Min(200, mr));
}
+ bool includeDead = true;
+ if (parsed.TryGetValue("includeDead", out string includeDeadStr) && bool.TryParse(includeDeadStr, out bool parsedIncludeDead))
+ {
+ includeDead = parsedIncludeDead;
+ }
+
Map map = Find.CurrentMap;
if (map == null) return "Error: No active map.";
var filters = ParseFilters(filterRaw);
+ if (filters.Contains("dead")) includeDead = true;
- List pawns = map.mapPawns?.AllPawnsSpawned?.Where(p => p != null).ToList() ?? new List();
- pawns = pawns.Where(p => MatchesFilters(p, filters)).ToList();
+ var entries = new List();
- if (pawns.Count == 0) return "No pawns matched.";
+ var livePawns = map.mapPawns?.AllPawnsSpawned?.Where(p => p != null).ToList() ?? new List();
+ foreach (var pawn in livePawns)
+ {
+ entries.Add(new MapPawnEntry
+ {
+ Pawn = pawn,
+ IsDead = pawn.Dead,
+ Position = pawn.Position
+ });
+ }
- pawns = pawns
- .OrderByDescending(p => IsHostileToPlayer(p))
- .ThenByDescending(p => p.RaceProps?.Humanlike ?? false)
- .ThenBy(p => p.def?.label ?? "")
- .ThenBy(p => p.Name?.ToStringShort ?? "")
+ if (includeDead && map.listerThings != null)
+ {
+ var corpses = map.listerThings.ThingsInGroup(ThingRequestGroup.Corpse);
+ if (corpses != null)
+ {
+ foreach (var thing in corpses)
+ {
+ if (thing is not Corpse corpse) continue;
+ Pawn inner = corpse.InnerPawn;
+ if (inner == null) continue;
+
+ entries.Add(new MapPawnEntry
+ {
+ Pawn = inner,
+ IsDead = true,
+ Position = corpse.Position
+ });
+ }
+ }
+ }
+
+ entries = entries
+ .Where(e => e.Pawn != null)
+ .GroupBy(e => e.Pawn.thingIDNumber)
+ .Select(g => g.First())
+ .Where(e => includeDead || !e.IsDead)
+ .Where(e => MatchesFilters(e, filters))
+ .ToList();
+
+ if (entries.Count == 0) return "No pawns matched.";
+
+ int matched = entries.Count;
+ var selected = entries
+ .OrderByDescending(e => IsHostileToPlayer(e.Pawn))
+ .ThenBy(e => e.IsDead) // living first
+ .ThenByDescending(e => e.Pawn.RaceProps?.Humanlike ?? false)
+ .ThenBy(e => e.Pawn.def?.label ?? "")
+ .ThenBy(e => e.Pawn.Name?.ToStringShort ?? "")
.Take(maxResults)
.ToList();
StringBuilder sb = new StringBuilder();
- sb.AppendLine($"Found {pawns.Count} pawns on map (showing up to {maxResults}):");
+ sb.AppendLine($"Found {matched} matching pawns on map (showing {selected.Count}):");
- foreach (var pawn in pawns)
+ foreach (var entry in selected)
{
- AppendPawnLine(sb, pawn);
+ AppendPawnLine(sb, entry);
}
return sb.ToString().TrimEnd();
@@ -66,7 +123,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
var set = new HashSet(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(filterRaw)) return set;
- var parts = filterRaw.Split(new[] { ',', ',', ';', '、', '|' }, StringSplitOptions.RemoveEmptyEntries);
+ var parts = filterRaw.Split(new[] { ',', '\uFF0C', ';', '\u3001', '|' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
string token = part.Trim().ToLowerInvariant();
@@ -85,16 +142,18 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
else if (token == "访客" || token == "客人") token = "guest";
else if (token == "野生") token = "wild";
else if (token == "倒地" || token == "昏迷") token = "downed";
+ else if (token == "死亡" || token == "尸体") token = "dead";
set.Add(token);
}
return set;
}
- private static bool MatchesFilters(Pawn pawn, HashSet filters)
+ private static bool MatchesFilters(MapPawnEntry entry, HashSet filters)
{
if (filters == null || filters.Count == 0) return true;
+ Pawn pawn = entry.Pawn;
bool anyMatched = false;
foreach (var f in filters)
{
@@ -112,6 +171,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
"guest" => pawn.guest != null && pawn.Faction != null && pawn.Faction != Faction.OfPlayer,
"wild" => pawn.Faction == null && (pawn.RaceProps?.Animal ?? false),
"downed" => pawn.Downed,
+ "dead" => entry.IsDead || pawn.Dead,
_ => false
};
@@ -137,19 +197,20 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
private static bool IsNeutralToPlayer(Pawn pawn)
{
if (pawn == null || Faction.OfPlayer == null) return false;
- if (pawn.Faction == null) return true; // wild/animals etc.
+ if (pawn.Faction == null) return true;
if (pawn.Faction == Faction.OfPlayer) return false;
return !pawn.HostileTo(Faction.OfPlayer);
}
- private static void AppendPawnLine(StringBuilder sb, Pawn pawn)
+ private static void AppendPawnLine(StringBuilder sb, MapPawnEntry entry)
{
+ Pawn pawn = entry.Pawn;
string name = pawn.Name?.ToStringShort ?? pawn.LabelShortCap;
string kind = pawn.def?.label ?? "unknown";
string faction = pawn.Faction?.Name ?? (pawn.RaceProps?.Animal == true ? "Wild" : "None");
string relation = IsHostileToPlayer(pawn) ? "Hostile" : (pawn.Faction == Faction.OfPlayer ? "Player" : "Non-hostile");
- string tags = BuildTags(pawn);
- string pos = pawn.Position.IsValid ? pawn.Position.ToString() : "?";
+ string tags = BuildTags(pawn, entry.IsDead);
+ string pos = entry.Position.IsValid ? entry.Position.ToString() : (pawn.Position.IsValid ? pawn.Position.ToString() : "?");
sb.Append($"- {name} ({kind})");
sb.Append($" faction={faction} relation={relation} pos={pos}");
@@ -157,7 +218,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
sb.AppendLine();
}
- private static string BuildTags(Pawn pawn)
+ private static string BuildTags(Pawn pawn, bool isDead)
{
var tags = new List();
if (pawn.IsFreeColonist) tags.Add("colonist");
@@ -165,6 +226,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
if (pawn.IsSlaveOfColony) tags.Add("slave");
if (pawn.guest != null && pawn.Faction != null && pawn.Faction != Faction.OfPlayer) tags.Add("guest");
if (pawn.Downed) tags.Add("downed");
+ if (isDead || pawn.Dead) tags.Add("dead");
if (pawn.InMentalState) tags.Add("mental");
if (pawn.Drafted) tags.Add("drafted");
if (pawn.RaceProps?.Humanlike ?? false) tags.Add("humanlike");
@@ -174,3 +236,4 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
}
}
}
+
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetRecentNotifications.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetRecentNotifications.cs
new file mode 100644
index 00000000..23b2cf5d
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetRecentNotifications.cs
@@ -0,0 +1,256 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Verse;
+
+namespace WulaFallenEmpire.EventSystem.AI.Tools
+{
+ public class Tool_GetRecentNotifications : AITool
+ {
+ public override string Name => "get_recent_notifications";
+ public override string Description => "Returns the most recent letters and messages, sorted by in-game time from newest to oldest.";
+ public override string UsageSchema =>
+ "int (optional, default 10, max 100)true/false (optional, default true)true/false (optional, default true)";
+
+ private struct NotificationEntry
+ {
+ public int Tick;
+ public string Kind;
+ public string Title;
+ public string Body;
+ }
+
+ public override string Execute(string args)
+ {
+ try
+ {
+ int count = 10;
+ bool includeLetters = true;
+ bool includeMessages = true;
+
+ var parsed = ParseXmlArgs(args);
+ if (parsed.TryGetValue("count", out var countStr) && int.TryParse(countStr, out int parsedCount))
+ {
+ count = parsedCount;
+ }
+
+ if (parsed.TryGetValue("includeLetters", out var incLettersStr) && bool.TryParse(incLettersStr, out bool parsedLetters))
+ {
+ includeLetters = parsedLetters;
+ }
+
+ if (parsed.TryGetValue("includeMessages", out var incMessagesStr) && bool.TryParse(incMessagesStr, out bool parsedMessages))
+ {
+ includeMessages = parsedMessages;
+ }
+
+ count = Math.Max(1, Math.Min(100, count));
+
+ int now = Find.TickManager?.TicksGame ?? 0;
+ var entries = new List();
+
+ if (includeLetters)
+ {
+ entries.AddRange(ReadLetters(now));
+ }
+
+ if (includeMessages)
+ {
+ entries.AddRange(ReadMessages(now));
+ }
+
+ if (entries.Count == 0)
+ {
+ return "No recent letters or messages found.";
+ }
+
+ var selected = entries
+ .OrderByDescending(e => e.Tick)
+ .ThenByDescending(e => e.Kind)
+ .Take(count)
+ .ToList();
+
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine($"Found {selected.Count} recent notifications (newest -> oldest):");
+
+ int idx = 1;
+ foreach (var e in selected)
+ {
+ sb.AppendLine($"{idx}. [{e.Kind}] tick={e.Tick}");
+ if (!string.IsNullOrWhiteSpace(e.Title))
+ {
+ sb.AppendLine($" Title: {TrimForDisplay(e.Title, 140)}");
+ }
+ if (!string.IsNullOrWhiteSpace(e.Body))
+ {
+ sb.AppendLine($" Text: {TrimForDisplay(e.Body, 600)}");
+ }
+ idx++;
+ }
+
+ return sb.ToString().TrimEnd();
+ }
+ catch (Exception ex)
+ {
+ return $"Error: {ex.Message}";
+ }
+ }
+
+ private static IEnumerable ReadLetters(int fallbackNow)
+ {
+ var list = new List();
+
+ object letterStack = Find.LetterStack;
+ if (letterStack == null) return list;
+
+ IEnumerable letters = null;
+ try
+ {
+ letters = GetMemberValue(letterStack, "LettersListForReading", "letters", "lettersList", "lettersListForReading") as IEnumerable;
+ }
+ catch
+ {
+ letters = null;
+ }
+
+ if (letters == null) return list;
+
+ foreach (var letter in letters)
+ {
+ if (letter == null) continue;
+
+ int tick = GetInt(letter, "arrivalTick", "receivedTick", "tick", "ticksGame") ?? fallbackNow;
+ string label = GetString(letter, "label", "Label", "LabelCap");
+ string text = GetString(letter, "text", "Text", "TextString", "LetterText");
+
+ string defName = GetString(GetMemberValue(letter, "def", "Def"), "defName", "DefName");
+ string kind = string.IsNullOrWhiteSpace(defName) ? "Letter" : $"Letter:{defName}";
+
+ list.Add(new NotificationEntry
+ {
+ Tick = tick,
+ Kind = kind,
+ Title = label,
+ Body = text
+ });
+ }
+
+ return list;
+ }
+
+ private static IEnumerable ReadMessages(int fallbackNow)
+ {
+ var list = new List();
+
+ IEnumerable messages = null;
+ try
+ {
+ messages = GetMemberValue(typeof(Messages), "MessagesListForReading", "messagesListForReading", "messages") as IEnumerable;
+ }
+ catch
+ {
+ messages = null;
+ }
+
+ if (messages == null) return list;
+
+ foreach (var message in messages)
+ {
+ if (message == null) continue;
+
+ int tick = GetInt(message, "time", "timeReceived", "receivedTick", "ticks", "tick", "startTick") ?? fallbackNow;
+ string text = GetString(message, "text", "Text", "message", "Message");
+ string typeDef = GetString(GetMemberValue(message, "def", "Def", "type", "Type", "messageType", "MessageType"), "defName", "DefName");
+ string kind = string.IsNullOrWhiteSpace(typeDef) ? "Message" : $"Message:{typeDef}";
+
+ list.Add(new NotificationEntry
+ {
+ Tick = tick,
+ Kind = kind,
+ Title = null,
+ Body = text
+ });
+ }
+
+ return list;
+ }
+
+ private static string TrimForDisplay(string s, int maxChars)
+ {
+ if (string.IsNullOrEmpty(s)) return s;
+ string oneLine = s.Replace("\r", " ").Replace("\n", " ").Trim();
+ if (oneLine.Length <= maxChars) return oneLine;
+ return oneLine.Substring(0, maxChars) + "...";
+ }
+
+ private static object GetMemberValue(object objOrType, params string[] names)
+ {
+ if (objOrType == null || names == null || names.Length == 0) return null;
+
+ Type t = objOrType as Type ?? objOrType.GetType();
+ bool isStatic = objOrType is Type;
+
+ const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static;
+
+ foreach (var name in names)
+ {
+ if (string.IsNullOrWhiteSpace(name)) continue;
+
+ var prop = t.GetProperty(name, Flags);
+ if (prop != null)
+ {
+ try
+ {
+ return prop.GetValue(isStatic ? null : objOrType, null);
+ }
+ catch
+ {
+ }
+ }
+
+ var field = t.GetField(name, Flags);
+ if (field != null)
+ {
+ try
+ {
+ return field.GetValue(isStatic ? null : objOrType);
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static int? GetInt(object obj, params string[] names)
+ {
+ object val = GetMemberValue(obj, names);
+ if (val == null) return null;
+ if (val is int i) return i;
+ if (val is long l)
+ {
+ if (l > int.MaxValue) return int.MaxValue;
+ if (l < int.MinValue) return int.MinValue;
+ return (int)l;
+ }
+ if (val is float f) return (int)f;
+ if (val is double d) return (int)d;
+ if (int.TryParse(val.ToString(), out int parsed)) return parsed;
+ return null;
+ }
+
+ private static string GetString(object obj, params string[] names)
+ {
+ object val = GetMemberValue(obj, names);
+ if (val == null) return null;
+ return val.ToString();
+ }
+ }
+}
+
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
index 3c57d128..518f75eb 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
@@ -21,11 +21,17 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
private bool _scrollToBottom = false;
private List _tools = new List();
private Dictionary _portraits = new Dictionary();
- private const int MaxHistoryTokens = 100000;
+ private const int DefaultMaxHistoryTokens = 100000;
private const int CharsPerToken = 4;
private int _continuationDepth = 0;
private const int MaxContinuationDepth = 6;
+ private static int GetMaxHistoryTokens()
+ {
+ int configured = WulaFallenEmpire.WulaFallenEmpireMod.settings?.maxContextTokens ?? DefaultMaxHistoryTokens;
+ return Math.Max(1000, Math.Min(200000, configured));
+ }
+
// Static instance for tools to access
public static Dialog_AIConversation Instance { get; private set; }
@@ -51,6 +57,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
3. **WORKFLOW**: You must use tools step-by-step to accomplish tasks. Use the output from one tool to inform your next step.
4. **ANTI-HALLUCINATION**: You MUST ONLY call tools from the list below. Do NOT invent tools or parameters. If a task is impossible, explain why without calling a tool.
5. **ENFORCEMENT**: The game will execute multiple info tools in one response, but it will NOT execute an action tool (spawn/bombardment/reinforcements/goodwill/expression) if you also included info tools in the same response. Call action tools in a separate turn after you see the info tool results.
+6. **AFTER TOOL RESULTS**: After you receive tool results, if no further tools are needed, you MUST reply in natural language only (no XML).
====
@@ -151,34 +158,48 @@ Example:
## get_map_resources
-Description: Checks the player's map for specific resources or buildings to verify their inventory.
+ Description: Checks the player's map for specific resources or buildings to verify their inventory.
+ Use this tool when:
+ - The player claims they are lacking a specific resource (e.g., ""we need steel,"" ""we have no food"").
+ - You want to assess the colony's material wealth before making a decision.
+ Parameters:
+ - resourceName: (OPTIONAL) The specific `ThingDef` name of the resource to check (e.g., 'Steel', 'MealSimple'). If omitted, provides a general overview.
+ Usage:
+
+ optional resource name
+
+ Example (checking for Steel):
+
+ Steel
+
+
+## get_recent_notifications
+Description: Gets the most recent letters and messages, sorted by in-game time from newest to oldest.
Use this tool when:
-- The player claims they are lacking a specific resource (e.g., ""we need steel,"" ""we have no food"").
-- You want to assess the colony's material wealth before making a decision.
+- You need recent context about what happened (raids, alerts, rewards, failures) without relying on player memory.
Parameters:
-- resourceName: (OPTIONAL) The specific `ThingDef` name of the resource to check (e.g., 'Steel', 'MealSimple'). If omitted, provides a general overview.
+- count: (OPTIONAL) How many entries to return (default 10, max 100).
+- includeLetters: (OPTIONAL) true/false (default true).
+- includeMessages: (OPTIONAL) true/false (default true).
Usage:
-
- optional resource name
-
-Example (checking for Steel):
-
- Steel
-
+
+ 10
+
## get_map_pawns
-Description: Scans the current map and lists pawns. Supports filtering by relation/type/status.
-Use this tool when:
-- You need to know what pawns are present on the map (raiders, visitors, animals, mechs, colonists).
-- The player claims there are threats or asks about who/what is nearby.
-Parameters:
-- filter: (OPTIONAL) Comma-separated filters: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed.
-- maxResults: (OPTIONAL) Max lines to return (default 50).
-Usage:
-
+ Description: Scans the current map and lists pawns. Supports filtering by relation/type/status.
+ Use this tool when:
+ - You need to know what pawns are present on the map (raiders, visitors, animals, mechs, colonists).
+ - The player claims there are threats or asks about who/what is nearby.
+ Parameters:
+ - filter: (OPTIONAL) Comma-separated filters: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed, dead.
+ - includeDead: (OPTIONAL) true/false, include corpse pawns (default true).
+ - maxResults: (OPTIONAL) Max lines to return (default 50).
+ Usage:
+
hostile, humanlike
50
-
+
## call_bombardment
Description: Calls orbital bombardment support at a specified map coordinate using an AbilityDef's bombardment configuration (e.g., WULA_Firepower_Cannon_Salvo).
@@ -270,13 +291,14 @@ When the player requests any form of resources, you MUST follow this multi-turn
_tools.Add(new Tool_SpawnResources());
_tools.Add(new Tool_ModifyGoodwill());
_tools.Add(new Tool_SendReinforcement());
- _tools.Add(new Tool_GetColonistStatus());
- _tools.Add(new Tool_GetMapResources());
- _tools.Add(new Tool_GetMapPawns());
- _tools.Add(new Tool_CallBombardment());
- _tools.Add(new Tool_ChangeExpression());
- _tools.Add(new Tool_SearchThingDef());
- }
+ _tools.Add(new Tool_GetColonistStatus());
+ _tools.Add(new Tool_GetMapResources());
+ _tools.Add(new Tool_GetRecentNotifications());
+ _tools.Add(new Tool_GetMapPawns());
+ _tools.Add(new Tool_CallBombardment());
+ _tools.Add(new Tool_ChangeExpression());
+ _tools.Add(new Tool_SearchThingDef());
+ }
public override Vector2 InitialSize => def.windowSize != Vector2.zero ? def.windowSize : Dialog_CustomDisplay.Config.windowSize;
@@ -414,6 +436,11 @@ When the player requests any form of resources, you MUST follow this multi-turn
{
CompressHistoryIfNeeded();
string systemInstruction = GetSystemInstruction(); // No longer need to add tool descriptions here
+ if (isContinuation)
+ {
+ systemInstruction += "\n\n# CONTINUATION\nYou have received tool results. If you already have enough information, reply to the player in natural language only (NO XML, NO tool calls). " +
+ "Only call another tool if strictly necessary, and if you do, call ONLY ONE tool in your entire response.";
+ }
var settings = WulaFallenEmpireMod.settings;
if (string.IsNullOrEmpty(settings.apiKey))
@@ -456,7 +483,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
private void CompressHistoryIfNeeded()
{
int estimatedTokens = _history.Sum(h => h.message?.Length ?? 0) / CharsPerToken;
- if (estimatedTokens > MaxHistoryTokens)
+ if (estimatedTokens > GetMaxHistoryTokens())
{
int removeCount = _history.Count / 2;
if (removeCount > 0)
@@ -487,15 +514,19 @@ When the player requests any form of resources, you MUST follow this multi-turn
StringBuilder xmlOnlyBuilder = new StringBuilder();
bool executedAnyInfoTool = false;
bool executedAnyActionTool = false;
+ bool executedAnyCosmeticTool = false;
static bool IsActionToolName(string toolName)
{
- // Action tools cause side effects / state changes and must be handled step-by-step.
return toolName == "spawn_resources" ||
toolName == "modify_goodwill" ||
toolName == "send_reinforcement" ||
- toolName == "call_bombardment" ||
- toolName == "change_expression";
+ toolName == "call_bombardment";
+ }
+
+ static bool IsCosmeticToolName(string toolName)
+ {
+ return toolName == "change_expression";
}
foreach (Match match in matches)
@@ -504,6 +535,8 @@ When the player requests any form of resources, you MUST follow this multi-turn
string toolName = match.Groups[1].Value;
bool isAction = IsActionToolName(toolName);
+ bool isCosmetic = IsCosmeticToolName(toolName);
+ bool isInfo = !isAction && !isCosmetic;
// Enforce step-by-step tool use:
// - Allow batching multiple info tools in one response (read-only queries).
@@ -520,6 +553,21 @@ When the player requests any form of resources, you MUST follow this multi-turn
combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because only one action tool may be executed per turn.");
break;
}
+ if (isInfo && executedAnyActionTool)
+ {
+ combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' and any following tools because info tools must not be mixed with an action tool in the same turn.");
+ break;
+ }
+ if (isCosmetic && executedAnyActionTool)
+ {
+ combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because cosmetic tools must not be mixed with an action tool in the same turn.");
+ break;
+ }
+ if (isCosmetic && executedAnyCosmeticTool)
+ {
+ combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because only one cosmetic tool may be executed per turn.");
+ break;
+ }
if (xmlOnlyBuilder.Length > 0) xmlOnlyBuilder.AppendLine().AppendLine();
xmlOnlyBuilder.Append(toolCallXml);
@@ -563,6 +611,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
}
if (isAction) executedAnyActionTool = true;
+ else if (isCosmetic) executedAnyCosmeticTool = true;
else executedAnyInfoTool = true;
}
diff --git a/Source/WulaFallenEmpire/GlobalWorkTable/Dialog_GlobalStorageTransfer.cs b/Source/WulaFallenEmpire/GlobalWorkTable/Dialog_GlobalStorageTransfer.cs
index fc22d9d4..b586cab0 100644
--- a/Source/WulaFallenEmpire/GlobalWorkTable/Dialog_GlobalStorageTransfer.cs
+++ b/Source/WulaFallenEmpire/GlobalWorkTable/Dialog_GlobalStorageTransfer.cs
@@ -15,9 +15,7 @@ namespace WulaFallenEmpire
private const float TopAreaHeight = 58f;
private readonly Building_GlobalWorkTable table;
- private readonly Pawn negotiator;
private readonly GlobalStorageWorldComponent storage;
- private readonly GlobalStorageTransferTrader trader;
private readonly QuickSearchWidget quickSearchWidget = new QuickSearchWidget();
private Vector2 scrollPosition;
@@ -25,19 +23,12 @@ namespace WulaFallenEmpire
private List tradeables = new List();
- private ITrader prevTrader;
- private Pawn prevNegotiator;
- private TradeDeal prevDeal;
- private bool prevGiftMode;
-
public override Vector2 InitialSize => new Vector2(1024f, UI.screenHeight);
public Dialog_GlobalStorageTransfer(Building_GlobalWorkTable table, Pawn negotiator)
{
this.table = table;
- this.negotiator = negotiator;
storage = Find.World.GetComponent();
- trader = new GlobalStorageTransferTrader(table?.Map, storage);
doCloseX = true;
closeOnAccept = false;
@@ -48,30 +39,9 @@ namespace WulaFallenEmpire
public override void PostOpen()
{
base.PostOpen();
-
- prevTrader = TradeSession.trader;
- prevNegotiator = TradeSession.playerNegotiator;
- prevDeal = TradeSession.deal;
- prevGiftMode = TradeSession.giftMode;
-
- TradeSession.trader = trader;
- TradeSession.playerNegotiator = negotiator;
- TradeSession.deal = null;
- TradeSession.giftMode = false;
-
RebuildTradeables();
}
- public override void PostClose()
- {
- base.PostClose();
-
- TradeSession.trader = prevTrader;
- TradeSession.playerNegotiator = prevNegotiator;
- TradeSession.deal = prevDeal;
- TradeSession.giftMode = prevGiftMode;
- }
-
public override void DoWindowContents(Rect inRect)
{
if (table == null || table.DestroyedOrNull() || table.Map == null || storage == null)
@@ -111,30 +81,150 @@ namespace WulaFallenEmpire
Rect outRect = rect.ContractedBy(5f);
Rect viewRect = new Rect(0f, 0f, outRect.width - 16f, viewHeight);
- Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect);
float curY = 0f;
int drawnIndex = 0;
- for (int i = 0; i < tradeables.Count; i++)
+ Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect);
+ try
{
- Tradeable trad = tradeables[i];
- if (trad == null || trad.ThingDef == null) continue;
+ for (int i = 0; i < tradeables.Count; i++)
+ {
+ Tradeable trad = tradeables[i];
+ if (trad == null) continue;
- if (!quickSearchWidget.filter.Matches(trad.ThingDef))
- continue;
+ PruneTradeableThingLists(trad);
+ if (!TryGetAnyThing(trad, out Thing anyThing)) continue;
- Rect rowRect = new Rect(0f, curY, viewRect.width, RowHeight);
- TradeUI.DrawTradeableRow(rowRect, trad, drawnIndex);
- curY += RowHeight;
- drawnIndex++;
+ ThingDef def = anyThing?.def;
+ if (def == null) continue;
+
+ if (!quickSearchWidget.filter.Matches(def))
+ continue;
+
+ Rect rowRect = new Rect(0f, curY, viewRect.width, RowHeight);
+ DrawStorageTransferRow(rowRect, trad, drawnIndex);
+ curY += RowHeight;
+ drawnIndex++;
+ }
+
+ if (Event.current.type == EventType.Layout)
+ {
+ viewHeight = Mathf.Max(curY, outRect.height);
+ }
+ }
+ finally
+ {
+ GenUI.ResetLabelAlign();
+ Widgets.EndScrollView();
+ }
+ }
+
+ private static bool TryGetAnyThing(Tradeable trad, out Thing anyThing)
+ {
+ anyThing = null;
+ if (trad == null) return false;
+
+ if (TryGetAnyThingFromList(trad.thingsColony, out anyThing)) return true;
+ if (TryGetAnyThingFromList(trad.thingsTrader, out anyThing)) return true;
+
+ return false;
+ }
+
+ private static bool TryGetAnyThingFromList(List things, out Thing anyThing)
+ {
+ anyThing = null;
+ if (things == null || things.Count == 0) return false;
+
+ for (int i = 0; i < things.Count; i++)
+ {
+ Thing t = things[i];
+ if (t == null || t.Destroyed) continue;
+ anyThing = t.GetInnerIfMinified();
+ if (anyThing != null && !anyThing.Destroyed) return true;
}
- if (Event.current.type == EventType.Layout)
+ anyThing = null;
+ return false;
+ }
+
+ private static void DrawStorageTransferRow(Rect rect, Tradeable trad, int index)
+ {
+ if (index % 2 == 1)
{
- viewHeight = Mathf.Max(curY, outRect.height);
+ Widgets.DrawLightHighlight(rect);
}
- Widgets.EndScrollView();
+ Text.Font = GameFont.Small;
+ Widgets.BeginGroup(rect);
+ try
+ {
+ float width = rect.width;
+
+ int globalCount = SafeCountHeldBy(trad, Transactor.Trader);
+ if (globalCount != 0 && trad.IsThing)
+ {
+ Rect countRect = new Rect(width - TradeUI.CountColumnWidth, 0f, TradeUI.CountColumnWidth, rect.height);
+ Widgets.DrawHighlightIfMouseover(countRect);
+ Text.Anchor = TextAnchor.MiddleRight;
+ Rect labelRect = countRect.ContractedBy(5f, 0f);
+ Widgets.Label(labelRect, globalCount.ToStringCached());
+ TooltipHandler.TipRegionByKey(countRect, "TraderCount");
+ }
+
+ width -= TradeUI.CountColumnWidth + TradeUI.PriceColumnWidth;
+
+ Rect adjustRect = new Rect(width - TradeUI.AdjustColumnWidth, 0f, TradeUI.AdjustColumnWidth, rect.height);
+ int min = -SafeCountHeldBy(trad, Transactor.Colony);
+ int max = SafeCountHeldBy(trad, Transactor.Trader);
+ TransferableUIUtility.DoCountAdjustInterface(adjustRect, trad, index, min, max, flash: false);
+ width -= TradeUI.AdjustColumnWidth;
+
+ int beaconCount = SafeCountHeldBy(trad, Transactor.Colony);
+ if (beaconCount != 0)
+ {
+ Rect countRect = new Rect(width - TradeUI.CountColumnWidth, 0f, TradeUI.CountColumnWidth, rect.height);
+ Widgets.DrawHighlightIfMouseover(countRect);
+ Text.Anchor = TextAnchor.MiddleLeft;
+ Rect labelRect = countRect.ContractedBy(5f, 0f);
+ Widgets.Label(labelRect, beaconCount.ToStringCached());
+ TooltipHandler.TipRegionByKey(countRect, "ColonyCount");
+ }
+
+ width -= TradeUI.CountColumnWidth + TradeUI.PriceColumnWidth;
+
+ Rect infoRect = new Rect(0f, 0f, width, rect.height);
+ TransferableUIUtility.DrawTransferableInfo(trad, infoRect, Color.white);
+ }
+ finally
+ {
+ GenUI.ResetLabelAlign();
+ Widgets.EndGroup();
+ }
+ }
+
+ private static int SafeCountHeldBy(Tradeable trad, Transactor transactor)
+ {
+ if (trad == null) return 0;
+
+ List list = (transactor == Transactor.Colony) ? trad.thingsColony : trad.thingsTrader;
+ if (list == null || list.Count == 0) return 0;
+
+ int count = 0;
+ for (int i = 0; i < list.Count; i++)
+ {
+ Thing t = list[i];
+ if (t == null || t.Destroyed) continue;
+ count += t.stackCount;
+ }
+
+ return count;
+ }
+
+ private static void PruneTradeableThingLists(Tradeable trad)
+ {
+ if (trad == null) return;
+ trad.thingsColony?.RemoveAll(t => t == null || t.Destroyed);
+ trad.thingsTrader?.RemoveAll(t => t == null || t.Destroyed);
}
private void DrawBottomButtons(Rect rect)
@@ -160,27 +250,109 @@ namespace WulaFallenEmpire
private void ExecuteTransfers()
{
- bool changed = false;
+ if (storage == null || table?.Map == null)
+ return;
- foreach (var trad in tradeables)
+ bool changed = false;
+ Map map = table.Map;
+ IntVec3 dropSpot = DropCellFinder.TradeDropSpot(map);
+
+ for (int i = 0; i < tradeables.Count; i++)
{
+ Tradeable trad = tradeables[i];
if (trad == null) continue;
if (trad.CountToTransfer == 0) continue;
- changed = true;
- trad.ResolveTrade();
+ PruneTradeableThingLists(trad);
+ if (!trad.HasAnyThing) continue;
+
+ int storeCount = trad.CountToTransferToDestination; // 信标 -> 全局(CountToTransfer<0)
+ int takeCount = trad.CountToTransferToSource; // 全局 -> 信标(CountToTransfer>0)
+
+ if (storeCount > 0)
+ {
+ changed |= TransferToGlobalStorage(trad, storeCount);
+ }
+ else if (takeCount > 0)
+ {
+ changed |= TransferFromGlobalStorage(trad, takeCount, map, dropSpot);
+ }
+
trad.ForceTo(0);
}
- if (changed)
- {
- SoundDefOf.ExecuteTrade.PlayOneShotOnCamera();
- RebuildTradeables();
- }
- else
+ if (!changed)
{
SoundDefOf.Tick_Low.PlayOneShotOnCamera();
+ return;
}
+
+ SoundDefOf.ExecuteTrade.PlayOneShotOnCamera();
+ RebuildTradeables();
+ }
+
+ private bool TransferToGlobalStorage(Tradeable trad, int count)
+ {
+ if (trad == null || count <= 0 || storage == null) return false;
+
+ bool changed = false;
+ TransferableUtility.TransferNoSplit(trad.thingsColony, count, (Thing thing, int countToTransfer) =>
+ {
+ if (thing == null || thing.Destroyed || countToTransfer <= 0) return;
+
+ Thing split = thing.SplitOff(countToTransfer);
+ if (split == null) return;
+
+ if (ShouldGoToOutputStorage(split))
+ {
+ storage.AddToOutputStorage(split);
+ }
+ else
+ {
+ storage.AddToInputStorage(split);
+ }
+
+ changed = true;
+ });
+
+ return changed;
+ }
+
+ private bool TransferFromGlobalStorage(Tradeable trad, int count, Map map, IntVec3 dropSpot)
+ {
+ if (trad == null || count <= 0 || storage == null || map == null) return false;
+
+ bool changed = false;
+ TransferableUtility.TransferNoSplit(trad.thingsTrader, count, (Thing thing, int countToTransfer) =>
+ {
+ if (thing == null || thing.Destroyed || countToTransfer <= 0) return;
+
+ Thing split = thing.SplitOff(countToTransfer);
+ if (split == null) return;
+
+ if (split.holdingOwner != null)
+ {
+ split.holdingOwner.Remove(split);
+ }
+ if (split.Spawned)
+ {
+ split.DeSpawn();
+ }
+
+ TradeUtility.SpawnDropPod(dropSpot, map, split);
+ changed = true;
+ });
+
+ return changed;
+ }
+
+ private static bool ShouldGoToOutputStorage(Thing thing)
+ {
+ ThingDef def = thing?.def;
+ if (def == null) return false;
+ if (def.IsWeapon) return true;
+ if (def.IsApparel) return true;
+ return false;
}
private void RebuildTradeables()
@@ -227,8 +399,12 @@ namespace WulaFallenEmpire
}
tradeables = tradeables
- .Where(t => t != null && t.HasAnyThing)
- .OrderBy(t => t.ThingDef?.label ?? "")
+ .Where(t => t != null && TryGetAnyThing(t, out _))
+ .OrderBy(t =>
+ {
+ TryGetAnyThing(t, out Thing anyThing);
+ return anyThing?.def?.label ?? "";
+ })
.ToList();
}
@@ -257,81 +433,16 @@ namespace WulaFallenEmpire
{
public override bool TraderWillTrade => true;
public override bool IsCurrency => false;
+ public override bool Interactive => true;
+ public override TransferablePositiveCountDirection PositiveCountDirection => TransferablePositiveCountDirection.Source;
}
private class Tradeable_StorageTransferPawn : Tradeable_Pawn
{
public override bool TraderWillTrade => true;
public override bool IsCurrency => false;
- }
-
- private class GlobalStorageTransferTrader : ITrader
- {
- private readonly Map map;
- private readonly GlobalStorageWorldComponent storage;
- private readonly TraderKindDef traderKind;
-
- public GlobalStorageTransferTrader(Map map, GlobalStorageWorldComponent storage)
- {
- this.map = map;
- this.storage = storage;
-
- traderKind =
- DefDatabase.GetNamedSilentFail("Orbital_ExoticGoods") ??
- DefDatabase.GetNamedSilentFail("Orbital_BulkGoods") ??
- DefDatabase.AllDefs.FirstOrDefault();
- }
-
- public TraderKindDef TraderKind => traderKind;
- public IEnumerable Goods => Enumerable.Empty();
- public int RandomPriceFactorSeed => 0;
- public string TraderName => "WULA_GlobalStorageTransferTitle".Translate();
- public bool CanTradeNow => true;
- public float TradePriceImprovementOffsetForPlayer => 0f;
- public Faction Faction => Faction.OfPlayer;
- public TradeCurrency TradeCurrency => TradeCurrency.Silver;
-
- public IEnumerable ColonyThingsWillingToBuy(Pawn playerNegotiator) => Enumerable.Empty();
-
- public void GiveSoldThingToTrader(Thing toGive, int countToGive, Pawn playerNegotiator)
- {
- if (storage == null) return;
- if (toGive == null || countToGive <= 0) return;
-
- Thing thing = toGive.SplitOff(countToGive);
- thing.PreTraded(TradeAction.PlayerSells, playerNegotiator, this);
-
- if (ShouldGoToOutputStorage(thing))
- {
- storage.AddToOutputStorage(thing);
- }
- else
- {
- storage.AddToInputStorage(thing);
- }
- }
-
- public void GiveSoldThingToPlayer(Thing toGive, int countToGive, Pawn playerNegotiator)
- {
- if (storage == null) return;
- if (map == null) return;
- if (toGive == null || countToGive <= 0) return;
-
- Thing thing = toGive.SplitOff(countToGive);
- thing.PreTraded(TradeAction.PlayerBuys, playerNegotiator, this);
-
- IntVec3 dropSpot = DropCellFinder.TradeDropSpot(map);
- TradeUtility.SpawnDropPod(dropSpot, map, thing);
- }
-
- private static bool ShouldGoToOutputStorage(Thing thing)
- {
- ThingDef def = thing?.def;
- if (def == null) return false;
- if (def.IsWeapon) return true;
- if (def.IsApparel) return true;
- return false;
- }
+ public override bool Interactive => true;
+ public override TransferablePositiveCountDirection PositiveCountDirection => TransferablePositiveCountDirection.Source;
}
}
}
diff --git a/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/CompMaintenancePod.cs b/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/CompMaintenancePod.cs
index ccb1671c..d552fad8 100644
--- a/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/CompMaintenancePod.cs
+++ b/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/CompMaintenancePod.cs
@@ -16,24 +16,24 @@ namespace WulaFallenEmpire
public SoundDef exitSound;
public EffecterDef operatingEffecter;
- // ʱ
- public int baseDurationTicks = 60000; // άʱ䣨1죩
- public float ticksPerNeedLevel = 120000f; // ÿҪʱ
+ // 时间相关
+ public int baseDurationTicks = 60000; // 基础维护时间(1天)
+ public float ticksPerNeedLevel = 120000f; // 每点需求降低需要的时间
- //
+ // 电力消耗
public float powerConsumptionRunning = 250f;
public float powerConsumptionIdle = 50f;
- //
+ // 组件消耗
public float componentCostPerNeedLevel = 2f;
public int baseComponentCost = 1;
- // άЧ
- public float minNeedLevelToMaintain = 0.3f; // ڴֵҪά
- public float needLevelAfterCycle = 1.0f; // άˮƽ
- public bool healInjuries = true; // Ƿ
- public bool healMissingParts = true; // Ƿȱʧλ
- public int maxInjuriesHealedPerCycle = 5; // ÿάƵ
+ // 维护效果
+ public float minNeedLevelToMaintain = 0.3f; // 低于此值才需要维护
+ public float needLevelAfterCycle = 1.0f; // 维护后的需求水平
+ public bool healInjuries = true; // 是否治疗损伤
+ public bool healMissingParts = true; // 是否修复缺失部位
+ public int maxInjuriesHealedPerCycle = 5; // 每次维护最多治疗的损伤数量
public CompProperties_MaintenancePod()
{
compClass = typeof(CompMaintenancePod);
@@ -57,20 +57,18 @@ namespace WulaFallenEmpire
public MaintenancePodState State => state;
public Pawn Occupant => innerContainer.FirstOrDefault() as Pawn;
public bool PowerOn => powerComp != null && powerComp.PowerOn;
- public float RequiredComponents
+ public float RequiredComponents => GetRequiredComponentsFor(Occupant);
+
+ public float GetRequiredComponentsFor(Pawn pawn)
{
- get
- {
- var occupant = Occupant;
- if (occupant == null) return Props.baseComponentCost;
+ if (pawn == null) return Props.baseComponentCost;
- var maintenanceNeed = occupant.needs?.TryGetNeed();
- if (maintenanceNeed == null) return Props.baseComponentCost;
+ var maintenanceNeed = pawn.needs?.TryGetNeed();
+ if (maintenanceNeed == null) return Props.baseComponentCost;
- // ڵǰˮƽ
- float needDeficit = 1.0f - maintenanceNeed.CurLevel;
- return Props.baseComponentCost + (needDeficit * Props.componentCostPerNeedLevel);
- }
+ // 计算基于当前需求水平的组件需求
+ float needDeficit = 1.0f - maintenanceNeed.CurLevel;
+ return Props.baseComponentCost + (needDeficit * Props.componentCostPerNeedLevel);
}
public int RequiredDuration
{
@@ -82,7 +80,7 @@ namespace WulaFallenEmpire
var maintenanceNeed = occupant.needs?.TryGetNeed();
if (maintenanceNeed == null) return Props.baseDurationTicks;
- // ڵǰˮƽάʱ
+ // 计算基于当前需求水平的维护时间
float needDeficit = 1.0f - maintenanceNeed.CurLevel;
return Props.baseDurationTicks + (int)(needDeficit * Props.ticksPerNeedLevel);
}
@@ -127,17 +125,17 @@ namespace WulaFallenEmpire
{
base.CompTick();
if (!parent.Spawned) return;
- // µ
+ // 更新电力消耗
if (powerComp != null)
{
powerComp.PowerOutput = -(state == MaintenancePodState.Running ? Props.powerConsumptionRunning : Props.powerConsumptionIdle);
}
- // ά
+ // 运行维护周期
if (state == MaintenancePodState.Running && PowerOn)
{
ticksRemaining--;
- // Ч
+ // 更新效果器
if (Props.operatingEffecter != null)
{
if (operatingEffecter == null)
@@ -160,29 +158,29 @@ namespace WulaFallenEmpire
public void StartCycle(Pawn pawn)
{
if (pawn == null) return;
- // Ƿ㹻
+ // 检查组件是否足够
float requiredComponents = RequiredComponents;
if (refuelableComp.Fuel < requiredComponents)
{
Messages.Message("WULA_MaintenancePod_NotEnoughComponents".Translate(requiredComponents.ToString("F0")), MessageTypeDefOf.RejectInput);
return;
}
- //
+ // 消耗组件
if (requiredComponents > 0)
{
refuelableComp.ConsumeFuel(requiredComponents);
}
- // pawn
+ // 将 pawn 放入容器
if (pawn.Spawned)
{
pawn.DeSpawn(DestroyMode.Vanish);
}
innerContainer.TryAddOrTransfer(pawn);
- // ʼά
+ // 开始维护周期
state = MaintenancePodState.Running;
ticksRemaining = RequiredDuration;
- // ŽЧ
+ // 播放进入音效
if (Props.enterSound != null)
{
Props.enterSound.PlayOneShot(new TargetInfo(parent.Position, parent.Map));
@@ -197,10 +195,10 @@ namespace WulaFallenEmpire
state = MaintenancePodState.Idle;
return;
}
- // ִάЧ
+ // 执行维护效果
PerformMaintenanceEffects(occupant);
- // pawn
+ // 弹出 pawn
EjectPawn();
Messages.Message("WULA_MaintenanceCycleComplete".Translate(occupant.LabelShortCap), MessageTypeDefOf.PositiveEvent);
@@ -209,17 +207,17 @@ namespace WulaFallenEmpire
{
var maintenanceNeed = pawn.needs?.TryGetNeed();
- // 1. ָά
+ // 1. 恢复维护需求
if (maintenanceNeed != null)
{
maintenanceNeed.PerformMaintenance(Props.needLevelAfterCycle);
}
- // 2. ˣã
+ // 2. 治疗损伤(如果启用)
if (Props.healInjuries)
{
HealInjuries(pawn);
}
- // 3. ȱʧλã
+ // 3. 修复缺失部位(如果启用)
if (Props.healMissingParts)
{
HealMissingParts(pawn);
@@ -249,7 +247,7 @@ namespace WulaFallenEmpire
int partsHealed = 0;
foreach (var missingPart in missingParts)
{
- if (partsHealed >= 1) // ÿһȱʧλ
+ if (partsHealed >= 1) // 每次最多修复一个缺失部位
break;
pawn.health.RemoveHediff(missingPart);
partsHealed++;
@@ -264,15 +262,15 @@ namespace WulaFallenEmpire
var occupant = Occupant;
if (occupant != null)
{
- // Ԫ
+ // 弹出到交互单元格
innerContainer.TryDropAll(parent.InteractionCell, parent.Map, ThingPlaceMode.Near);
- // ˳Ч
+ // 播放退出音效
if (Props.exitSound != null)
{
Props.exitSound.PlayOneShot(new TargetInfo(parent.Position, parent.Map));
}
- // жϣӦøЧ
+ // 如果被中断,应用负面效果
if (interrupted)
{
occupant.needs?.mood?.thoughts?.memories?.TryGainMemory(ThoughtDefOf.SoakingWet);
@@ -281,7 +279,7 @@ namespace WulaFallenEmpire
innerContainer.Clear();
state = MaintenancePodState.Idle;
- // Ч
+ // 清理效果器
if (operatingEffecter != null)
{
operatingEffecter.Cleanup();
@@ -300,7 +298,7 @@ namespace WulaFallenEmpire
var maintenanceNeed = Occupant.needs?.TryGetNeed();
if (maintenanceNeed != null)
{
- // ֱʾ CurLevelȷ Need ʾһ
+ // 直接显示 CurLevel,确保与 Need 显示一致
sb.AppendLine("WULA_MaintenanceLevel".Translate() + ": " + maintenanceNeed.CurLevel.ToStringPercent());
}
}
@@ -316,7 +314,7 @@ namespace WulaFallenEmpire
{
yield return gizmo;
}
- // άյİť
+ // 进入维护舱的按钮
if (state == MaintenancePodState.Idle && PowerOn)
{
yield return new Command_Action
@@ -327,7 +325,7 @@ namespace WulaFallenEmpire
action = () => ShowPawnSelectionMenu()
};
}
- // ȡάİť
+ // 取消维护的按钮
if (state == MaintenancePodState.Running)
{
yield return new Command_Action
@@ -362,19 +360,19 @@ namespace WulaFallenEmpire
foreach (var pawn in map.mapPawns.AllPawnsSpawned)
{
- // ȼǷά
+ // 首先检查是否有维护需求
var maintenanceNeed = pawn.needs?.TryGetNeed();
if (maintenanceNeed == null)
{
- // Pawnûά
+ // 这个Pawn没有维护需求,跳过
continue;
}
- // ǷҪά
+ // 检查是否真的需要维护
if (maintenanceNeed.CurLevel > Props.minNeedLevelToMaintain && !DebugSettings.godMode)
continue;
- // ѡ
+ // 创建选项
var option = CreatePawnOption(pawn, maintenanceNeed);
if (option != null)
options.Add(option);
@@ -387,12 +385,12 @@ namespace WulaFallenEmpire
{
string label = $"{pawn.LabelShortCap} ({need.CurLevel.ToStringPercent()})";
float requiredComponents = RequiredComponents;
- // Ƿ㹻
+ // 检查组件是否足够
if (refuelableComp.Fuel < requiredComponents)
{
return new FloatMenuOption(label + " (" + "WULA_MaintenancePod_NotEnoughComponents".Translate(requiredComponents.ToString("F0")) + ")", null);
}
- // ǷԵ
+ // 检查是否可以到达
if (!pawn.CanReach(parent, PathEndMode.InteractionCell, Danger.Deadly))
{
return new FloatMenuOption(label + " (" + "CannotReach".Translate() + ")", null);
@@ -401,7 +399,7 @@ namespace WulaFallenEmpire
{
if (pawn.Downed || !pawn.IsFreeColonist)
{
- // Ҫ
+ // 需要搬运
var haulJob = JobMaker.MakeJob(JobDefOf_WULA.WULA_HaulToMaintenancePod, pawn, parent);
var hauler = FindBestHauler(pawn);
if (hauler != null)
@@ -415,7 +413,7 @@ namespace WulaFallenEmpire
}
else
{
- // Լ
+ // 自己进入
var enterJob = JobMaker.MakeJob(JobDefOf_WULA.WULA_EnterMaintenancePod, parent);
pawn.jobs.TryTakeOrderedJob(enterJob);
}
diff --git a/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/WorkGiver_DoMaintenance.cs b/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/WorkGiver_DoMaintenance.cs
index 563cb1d6..457e5648 100644
--- a/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/WorkGiver_DoMaintenance.cs
+++ b/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/WorkGiver_DoMaintenance.cs
@@ -22,6 +22,20 @@ namespace WulaFallenEmpire
if (podComp == null || podComp.State != MaintenancePodState.Idle || !podComp.PowerOn)
return false;
+ // 检查是否有足够的燃料(零部件)
+ // 如果是强制工作(玩家右键),我们允许通过检查,让 JobDriver 去处理(可能会提示燃料不足)
+ // 这样玩家能知道为什么不能工作,而不是默默失败
+ if (!forced)
+ {
+ float requiredFuel = podComp.GetRequiredComponentsFor(pawn);
+ var refuelable = t.TryGetComp();
+ if (refuelable != null && refuelable.Fuel < requiredFuel)
+ {
+ JobFailReason.Is("WULA_MaintenancePod_NotEnoughComponents".Translate(requiredFuel.ToString("F0")));
+ return false;
+ }
+ }
+
// 检查当前pawn是否有维护需求且需要维护
return PawnNeedsMaintenance(pawn);
}
diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs
index 47481dea..f0cd6a9d 100644
--- a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs
+++ b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs
@@ -10,6 +10,7 @@ namespace WulaFallenEmpire
public class WulaFallenEmpireMod : Mod
{
public static WulaFallenEmpireSettings settings;
+ private string _maxContextTokensBuffer;
public WulaFallenEmpireMod(ModContentPack content) : base(content)
{
@@ -38,6 +39,12 @@ namespace WulaFallenEmpire
listingStandard.Label("Wula_AISettings_Model".Translate());
settings.model = listingStandard.TextEntry(settings.model);
+ listingStandard.GapLine();
+ listingStandard.Label("Wula_AISettings_MaxContextTokens".Translate());
+ listingStandard.Label("Wula_AISettings_MaxContextTokensDesc".Translate());
+ Rect tokensRect = listingStandard.GetRect(Text.LineHeight);
+ Widgets.TextFieldNumeric(tokensRect, ref settings.maxContextTokens, ref _maxContextTokensBuffer, 1000, 200000);
+
listingStandard.End();
base.DoSettingsWindowContents(inRect);
}
diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs
index d0ccbf64..04a0e90b 100644
--- a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs
+++ b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs
@@ -7,13 +7,15 @@ namespace WulaFallenEmpire
public string apiKey = "sk-xxxxxxxx";
public string baseUrl = "https://api.deepseek.com";
public string model = "deepseek-chat";
+ public int maxContextTokens = 100000;
public override void ExposeData()
{
Scribe_Values.Look(ref apiKey, "apiKey", "sk-xxxxxxxx");
Scribe_Values.Look(ref baseUrl, "baseUrl", "https://api.deepseek.com");
Scribe_Values.Look(ref model, "model", "deepseek-chat");
+ Scribe_Values.Look(ref maxContextTokens, "maxContextTokens", 100000);
base.ExposeData();
}
}
-}
\ No newline at end of file
+}