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;