diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index 9b6a84a5..387a0dc0 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ
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 3b4278e9..a467df55 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
@@ -151,25 +151,38 @@ 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).
+ 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.
@@ -266,13 +279,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;